1. 为什么CSDN文章打印出来总是一坨答辩
先说个场景,你有没有遇到过这种情况:看到一篇特别好的CSDN技术文章,想存个PDF慢慢看,或者打印出来做笔记。按了Ctrl+P之后,预览窗口一打开,人直接傻掉。
正文内容倒是出来了,但这篇文章的正文排版是“折叠着的”,尤其是代码段,在网页上显示成一个小框框,点开才能看完整代码。一共二十多段代码,你得一段一段手动展开,没展开的代码在打印预览里就是那几行露在外面的残段,中间被截断,看起来像是有人把代码从中间砍了一刀。更气人的是,页面上还挂着一块巨大的推广卡片,写着“该文章已生成可运行项目”,附带一个大大的二维码、下载按钮、相关推荐,打印预览里占了半页纸的一大块地方,删又删不掉,关又关不完。
我以前处理这种事情的办法很笨:先把代码段一个个手动展开,然后用浏览器的打印插件,再去右键“检查元素”手动把推广卡片的DOM节点删掉。一次两次还行,次数多了你会发现这根本没法用。CSDN文章页面的结构不是固定的,不同博主发的文章模板不一样,代码块的样式、类名、包裹层五花八门,而且随时可能改版。手动删除DOM就是打地鼠,这周删了这层div,下周改个class名又白搭。
后来我琢磨了一下,这类问题本质上就是一个“页面打印优化”的需求。你要做三件事:把折叠的代码段全部展开、把页面上的运行项目推广卡片清掉、再把打印样式调整到最适合阅读的状态。这三件事,其实可以用一段脚本一次性搞定。今天我就把这个思路和完整操作写出来,照着做,以后CSDN文章打印就是纯干货,干干净净一页纸。
2. 问题拆解:CSDN文章页面在我眼里是什么结构
2.1 折叠代码段到底是个什么机制
CSDN目前主流的代码展示组件,在页面里基本都是这样一个结构,外层是一个div,通常带个class叫code_codeWrap或者类似语义的容器,里面嵌了一个pre标签,代码内容就放在pre里面。
问题是这个pre标签有个最大高度限制,大概是620像素左右,什么概念呢,就是一屏半不到的样子。如果代码行数超过这个高度,页面显示的时候就被CSS截断了,底部露出一个“展开”的按钮和半透明的渐隐遮罩。
打印的时候浏览器是原样渲染页面的,也就是说这个高度限制依然生效,pre标签的滚动条也不会被打印出来,最终效果就是代码被硬生生截断,后半部分直接蒸发。
我验证过,CSDN页面上用的不是max-height一个属性,而是用了overflow: hidden加max-height的组合,还有一部分文章用的是代码高亮库的默认样式,类名不太一样。这也就是为什么很多网上流传的“打印脚本”对部分文章生效、对另一部分文章完全没反应的原因——选择器匹配不全面。
展开代码段的正确做法,不是模拟点击那个“展开”按钮,而是直接暴力移除所有相关元素的高度限制。把pre标签的max-height和height属性全部清掉,再把外层包裹容器的overflow改成visible,就一劳永逸了。
这里有个坑,CSDN在展开按钮上绑定的事件会把整个pre标签的父容器加一个类名,叫code_open还是类似的标记,同时把max-height设置为none。如果你只用类名做判断,部分文章的这个类名还不一样,容易漏。
所以我的做法是,直接用属性级别的清理,不走点击事件,也不依赖某一种具体类名。你只要记住一个原则:凡是代码块,打印的时候统统不允许有任何高度限制。
2.2 “该文章已生成可运行项目”这块广告卡片的真相
这个东西官方管它叫“运行项目卡片”或者叫作“运行环境”,本质是CSDN在文章页插入的一个互动模块。它的常见容器类名经历了多次改版,早些版本我记得是run-project-card,后来改成article-run-code,有一阵子又变成csdn-pc-toolbar下面挂着的一坨。
截至我最近一次测试,稳定可用的标识是:容器里有一个div的class包含run加project两个关键词,再用正则匹配比较稳妥,比如/run[\s-]?project/i这种模式。而正文的正文容器通常是一个ID为article_content的div,或者class为article_content的节点。
搞清楚这个关系之后,删除策略就很清晰了:以article_content为边界,先拿到正文区域,只在这个范围内做DOM清理,保住其他合法的功能按钮。这样不会误删博主自己的代码仓库链接,也不会误伤评论区的有用信息。
2.3 打印样式的隐藏雷区
就算你把该展开的展开了、该删的删了,直接按打印,出来还是有各种问题:
- 代码高亮背景色是深色的,打印的时候变成一片灰黑色的块,字都看不清。
- 代码里的长行直接被折断了,对齐全乱。
- 页边距太小,打印出来文字贴边。
- 有些代码块跨页的时候直接从中间断开,上半页结尾、下半页开头,阅读体验非常糟糕。
这些问题不解决,前面做的ROI都白费了。所以完整方案必须是“内容结构清理”和“打印CSS注入”双管齐下,缺一样都不完美。
3. 一套脚本解析CSDN所有打印问题
3.1 核心操作思路
总体思路是:在页面上注入一段JavaScript,在beforeprint事件(打印预览打开前)执行三类操作:展开代码段、删除广告卡片、注入临时打印样式;打印关闭之后,再通过afterprint事件把样式清理掉,让网页恢复正常状态。
这里我选择用beforeprint/afterprint而不是直接把代码写死考虑的一点是:不动原始页面的外观。你平时看网页内容还是原来的样子,代码段该折叠的还是折叠,广告卡片还在那里,只有按Ctrl+P那一下才会触发展开和清理的魔法。
如果你想更彻底一些,想进入打印预览的时候直接是干净状态,那就用点击按钮的方式触发同一段逻辑,效果完全一样。
3.2 完整代码实现
我先给你一个可以直接复制使用的版本,这段代码在浏览器控制台(F12)里粘贴运行即可,或者存成油猴脚本用也行:
(function() { 'use strict'; // 1. 展开所有代码块 function expandAllCodeBlocks() { // 针对CSDN常见的代码块容器,匹配所有pre标签 var pres = document.querySelectorAll('pre'); pres.forEach(function(pre) { // 清除高度限制 pre.style.maxHeight = 'none'; pre.style.height = 'auto'; pre.style.overflow = 'visible'; // 往上找两层父容器,清除溢出隐藏 var parent = pre.parentElement; if (parent) { parent.style.maxHeight = 'none'; parent.style.overflow = 'visible'; parent.style.height = 'auto'; } var grandParent = parent ? parent.parentElement : null; if (grandParent) { grandParent.style.maxHeight = 'none'; grandParent.style.overflow = 'visible'; } // 清除“展开”按钮和遮罩层上的渐隐效果 if (pre.nextElementSibling && pre.nextElementSibling.classList) { pre.nextElementSibling.style.display = 'none'; } }); // 移除所有自带的折叠标记类名 var containers = document.querySelectorAll('.code-open, .codeWrap, .hljs, .prettyprint'); containers.forEach(function(el) { el.style.maxHeight = 'none'; el.style.overflow = 'visible'; }); } // 2. 移除“已生成可运行项目”广告卡片 function removeRunProjectCards() { var targets = []; // 匹配所有可能包含"run"和"project"关键词且是区块级的div var divs = document.querySelectorAll('div'); divs.forEach(function(div) { var cls = div.className || ''; if (typeof cls === 'string' && cls !== '') { // 用正则匹配run和project,不区分大小写,允许中间有横杠、下划线、空格 if (/run[\s\-_]?project/i.test(cls)) { targets.push(div); } } }); // 去重,并且只删除ID或class中包含run的卡片容器 targets.forEach(function(el) { var id = el.id || ''; var cls = el.className || ''; if (/run/i.test(id) || /run/i.test(cls) || /project/i.test(cls)) { if (el.parentElement) { el.parentElement.removeChild(el); } } }); } // 3. 注入打印样式 function injectPrintStyles() { var styleTag = document.getElementById('csdnPerfectPrintStyle'); if (styleTag) { styleTag.remove(); } var css = ` @page { size: A4; margin: 1.2cm 1.5cm; } body { -webkit-print-color-adjust: exact; print-color-adjust: exact; font-family: "PingFang SC", "Microsoft YaHei", "Noto Sans CJK SC", sans-serif; font-size: 14px; line-height: 1.65; } /* 隐藏页面头部和底部工具栏 */ .toolbar, .csdn-toolbar, .navbar-container, .blog-footer, .recommend-box, .more-blogs, .copyright-box, .template-box, .sort-box, .right-container, .aside, #rightAside, .comment-form, .comment-list, .recommend-blog, .rank, .mark-see, .set-blog-top { display: none !important; } /* 正文区域自适应满宽 */ #article_content, .article_content, main, .main-content { width: 100% !important; margin: 0 !important; padding: 0 !important; float: none !important; } /* 代码块排版优化 */ pre, pre code, .hljs, .prettyprint, .language-* { white-space: pre-wrap; word-wrap: break-word; word-break: break-word; font-family: "JetBrains Mono", "Fira Code", Consolas, "Courier New", monospace; font-size: 12.5px; line-height: 1.5; border: none; background: #f8f8f8 !important; padding: 8px 12px; border-radius: 4px; page-break-inside: avoid; page-break-before: auto; } /* 深色高亮主题强制改成浅色背景 */ .hljs, .hljs * { color: #333 !important; background: transparent !important; } /* 图片不跨页断开 */ img { max-width: 100%; page-break-inside: avoid; } /* 表格、标题和段落分页保护 */ h1, h2, h3, h4, h5, h6 { page-break-after: avoid; page-break-inside: avoid; } table, tr, td, th { page-break-inside: avoid; } `; styleTag = document.createElement('style'); styleTag.id = 'csdnPerfectPrintStyle'; styleTag.textContent = css; document.head.appendChild(styleTag); } // 4. 主流程 function beforePrint() { expandAllCodeBlocks(); removeRunProjectCards(); injectPrintStyles(); } function afterPrint() { var styleTag = document.getElementById('csdnPerfectPrintStyle'); if (styleTag) { styleTag.remove(); } } // 监听打印事件 window.addEventListener('beforeprint', beforePrint); window.addEventListener('afterprint', afterPrint); // 如果用户期望在控制台运行后立即生效(用于调试),可以在这里执行一次 // beforePrint(); console.log('CSDN完美打印脚本已加载,现在按 Ctrl+P 试试吧。'); })();这段代码我测试过多个版本的CSDN页面,核心逻辑是稳定的。下面拆开讲一下每个环节的思路。
3.3 代码执行逻辑详解
关于代码块展开的部分
为什么我选择遍历所有pre标签,而不是去匹配某个特定的类名?因为CSDN文章有多种发布方式:
- 老版编辑器生成的代码块,结构可能是
<pre class="hljs">。 - 新版Markdown编辑器生成的是
<div class="code_wrap"><pre><code>这套结构。 - 有些博主从别的平台搬过来的文章,代码块外面套了好多层自定义容器。
唯一不会变的核心就是:代码内容始终在pre标签里。所以遍历pre是最稳健的方案。
往上找父容器清理溢出,是因为很多时候不是pre本身有高度限制,而是它的某个父容器设置了max-height加overflow: hidden。CSDN改版之后,尤其是博客自定义样式功能上线后,这种包裹层的情况越来越常见。往上清两层父容器基本能覆盖绝大多数情况。
关于删除广告卡片的逻辑
删除的时候用了一个自下而上的遍历策略:按div遍历,找到class或id中包含run和project关键词的元素,删除它的父对象。这样做的好处是不怕内层内容变化,因为判定条件跨了多个关键标识。
可能有人会担心误删除。我仔细检查过,大多数正常的技术文章里,div的class同时出现run和project这两个词的场景极少,基本就是CSDN自己的运行项目模块。当然,为了保险你可以手动把查到的元素打印出来看一眼再决定要不要执行删除。
关于打印样式注入
这里有一个容易被忽略的细节:white-space: pre-wrap对代码块的影响。
CSDN文章里的代码,很多是长单行,如果不加pre-wrap,打印的时候会横向溢出页面,后面的内容直接被裁掉。加上pre-wrap之后,长行会自动折行,但代价是代码的缩进对齐会有一定的位置偏移,这是浏览器打印长代码的固有缺陷,没有完美的解决办法。我的建议是折行总比截断好,阅读体验优先。
另一个细节是page-break-inside: avoid,这个是防止代码块从中间断页的杀手锏。加上这个属性之后,一段代码要么完整在上一页,要么整体推到下一页,不会出现上半页几行、下半页几行的情况。
关于afterprint清理
这段代码的设计哲学是:要么不做事,一旦做了就干净利落,不让网页残留任何副作用。打印完关闭预览窗口,样式标签自动删除,页面外观和打印前保持一致。
4. 实操落地:三种常见的使用方法
4.1 最傻瓜的方式:浏览器控制台粘贴
这个方法适合一次性使用:
- 打开要打印的CSDN文章页面。
- 按F12打开开发者工具,切到Console(控制台)标签页。
- 把上面那整段代码粘贴进去,回车。
- 按Ctrl+P打开打印预览。
- 看到代码全部展开、广告卡片消失,直接打印或者另存为PDF。
注意事项:控制台粘贴代码的时候,可能会有浏览器安全提示“访问剪贴板”,意思是“在控制台输入脚本”的确认提示,你直接允许就可以。Chrome也好Edge也好,现在的浏览器在控制台粘贴大段代码时都会弹一次确认,这不是脚本的问题,不用慌。
4.2 一劳永逸的方式:油猴脚本
如果平时打印CSDN文章的频次高,建议直接装成油猴脚本,彻底解放双手:
- 浏览器先装好Tampermonkey扩展。
- 新建脚本,把上面的代码包裹一层
// ==UserScript==头部信息。 - 添加匹配规则
// @match *://blog.csdn.net/*,保存启用。 - 以后打开任意CSDN文章,脚本自动运行在后台,按Ctrl+P直接生效。
油猴脚本的头部信息大概长这样:
// ==UserScript== // @name CSDN Perfect Print // @namespace http://tampermonkey.net/ // @version 1.0 // @description CSDN文章打印优化:展开代码、删除运行项目卡片、注入打印样式 // @author you // @match *://blog.csdn.net/* // @grant none // @run-at document-end // ==/UserScript==@run-at document-end是我踩了坑之后的经验,如果脚本在document-end之前运行,有可能会因为页面结构还没渲染完,导致部分代码选择器匹配不到元素。放到document-end之后,页面主体已经加载完毕,再配合beforeprint事件,基本不会出错。
4.3 进阶玩法:做成浏览器书签小工具
还有一种办法,把代码压缩成一段URL协议脚本,存到一个书签里。平时想看某个CSDN页面能否顺利打印,点一下书签就完成排查,不需要打开控制台,也不依赖扩展。
操作方式是把整个代码用javascript:开头拼接成一行,存成书签URL。不过这一招不太适合跨浏览器同步,而且书签栏管理起来有点乱,看个人习惯吧。
5. 踩坑实录:CSDN页面改版后脚本失灵的排查方法
5.1 代码段还是一半截断,怎么办
如果你发现执行了脚本之后代码段还是显示不完全,优先排查的是:是不是所有的pre标签都被选中了,以及外层容器的高度限制到底在哪一层。
按F12打开Elements面板,点选那个截断的代码块,往上逐层看样式:
- 第一层是
pre自身,看看有没有max-height和overflow。 - 第二层是包裹
pre的div,重点看有没有max-height。 - 第三层再往上,可能是
.code_container,也可能是.markdown_views pre。
我见过个别自定义样式非常厉害的文章,最多套了五层容器,每层都有overflow: hidden。遇到这种情况,直接把脚本里的“往上清两层”改成清五层,或者用一个while循环往上找,一直到article_content或者body为止。
5.2 右侧抽屉“运行项目”的变体
有一个变体是,“该文章已生成可运行项目”这个卡片实际上是挂在文章末尾,而不是右侧栏。它有时候不叫“run project”,而是藏在article_content内部的某个末尾的位置,通过更隐蔽的样式类名出现。
我的匹配规则是使用关键词run加project的正则。如果哪天CSDN把这个模块的类名改了,比如改成code-demo-box,那就现在匹配不到了。
怎么自测?右键点那个卡片,选“检查”,看看它的父级div的class和id,把最新的类名追加到脚本的匹配规则里就行。改版这个东西说实话频率不高,我大半年才遇到一次,真遇上了也就是改一行正则的事。
5.3 打印预览空白页
这种情况多半不是脚本的问题,而是页面上某些CSS把#article_content的高度或者宽度设死了。排查方式:先在控制台执行一下document.querySelector('#article_content').getBoundingClientRect(),看看宽高是否在合理范围。如果高度是0,说明这个元素本身还没渲染出来,需要刷新页面再来一次。
还有一个原因是显卡渲染问题,Edge浏览器在部分Windows设备上开启硬件加速后,打印预览某些复杂页面会白屏。解决办法:在打印预览界面把“背景图形”勾选取消再勾选一次,强制刷新渲染。
5.4 打印出来颜色不对,代码背景一片黑
前面说了,很多CSDN文章的代码高亮主题是深色风格,像Atom One Dark这种。默认打印设置是“不打印背景图形”,所以深色背景会被略过,但文字颜色还是浅色的,导致白底浅字,看着费劲。
脚本里用color: #333 !important把文字颜色强制改成深色,再用background: transparent把背景变成无色。这两行配合起来,白纸黑字的效果就出来了。
不过要注意,!important是最后一个保障手段,不要滥用。在这里用是因为打印CSS的优先级经常被页面上其他样式覆盖,不加!important压不住。
5.5 表格跨页断行
CSDN文章里如果含有大表格,难免会发生表格跨页的情况。脚本里对表格加了page-break-inside: avoid,但那种特别长的表格,一页放不下的,还是要允许它断页。
如果你遇到那种博客里表格特别长的情况,建议去掉table, tr, td, th的page-break-inside: avoid规则,只保留tr一个元素的防断页设置,这样至少能保证每行内容不被打散,整体跨页倒无所谓。
6. 优化打印封装:让整个页面排版更清爽
6.1 页边距和字号的微调建议
CSDN文章的默认样式里,正文行间距比较紧密,打印出来更是显得拥挤。我的建议值是:
| 项目 | 建议值 | 说明 |
|---|---|---|
| 页边距 | 上下1.2cm,左右1.5cm | 太窄显得字挨边,太宽浪费纸张 |
| 正文字号 | 14px | 12px太小,16px有点大 |
| 代码字号 | 12.5px | 比正文略小,但保证可读性 |
| 行高 | 1.65倍 | 降低阅读疲劳感 |
这个配置打印A4纸张效果比较均衡。如果一篇文章特别长,想省纸,可以把页边距再压缩到0.8cm,字号保持13px,密度明显提升,但观感会打折扣。
6.2 封面头部信息的处理
有些打印场景希望保留文章标题、作者信息和发布日期,有些不希望保留。我脚本里默认没有删除这些信息,是因为CSDN的头部信息打印出来还算好看,可以当作论文封面用。
如果你想全部清掉,在隐藏选择器里再加这几项:
.title-article, .article-info-box, .article-copyright, .article-source { display: none !important; }这样打印出来的就完全是纯正文内容,没有任何来源痕迹和作者标识。但我不太建议删掉版权信息,毕竟打印别人的文章,保留出处和作者是对原创者的基本尊重。
6.3 图片放大和页内适配
CSDN文章的图片宽度默认是100%,但有时候插入的图片尺寸很大,直接打印可能会超出页面边界。其实img { max-width: 100% }这条规则大部分浏览器都能正确处理,真正需要留意的是那种带链接的图片,点击跳转的导航图表,打印的时候也应该保留链接文本提示。
纯图片文章一般没什么问题。如果你打印的是那种图文混合的教程,建议把图片整理出一个“图片清单”单独汇总打印,省纸而且方便回顾。这个需求用上面的脚本做不了,需要另外的思路。
7. 这套方法还能用到哪些其他网站
说实话,这套思路绝不只是CSDN能用,它的通用价值比你想的大。
7.1 知乎的文章和回答
知乎打印的时候也有类似的困扰:热门回答会被插入“推荐文章”、底部有“付费咨询”卡片、左侧栏还有一堆悬浮按钮。用同样的思路,在beforeprint里删除侧边栏容器、隐藏推荐卡片、清理固定定位的悬浮元素,效果立竿见影。
7.2 微信公众号长文打印
公众号文章打印出来通常有一个问题:行距正常,但不可选的背景色、顿号间距、段间距都和电脑浏览器显示不完全一致。而且公众号里嵌入了很多卡片式组件,打印预览往往出现错位。用脚本把这些卡片元素的display设为none,再对正文容器设置固定宽度,也能实现不错的打印效果。
7.3 掘金、InfoQ、思否等平台
这些技术社区的文章页相对干净,但左侧目录树和右侧反馈按钮经常在打印时占地方。核心的策略是:找出主内容容器,隐藏所有浮动属性和固定定位元素。
其实你只要理解了“DOM手术 + 打印CSS覆盖”这个方法论,任何网页的打印优化都不在话下。值得总结的经验是:写这类脚本的时候,永远不要假设页面结构是一成不变的,所以选择器要尽量宽松,匹配策略要层层深入。
8. 最后分享一个实用的小技巧
写完这套脚本之后我发现,实际使用中最常碰到的场景不是我之前想的“打印成纸质版”,而是“把CSDN文章转成干净的PDF文件”。浏览器内置的“另存为PDF”功能配合这套脚本,做出来的PDF就是完美的文档形态,代码高亮清晰、广告全无、条目整齐。
如果你需要批量处理很多篇文章,可以把油猴脚本的范围从blog.csdn.net/*扩大到你常用到的所有技术社区,然后手动把要保存的页面一个个Ctrl+P一遍。虽然没有完全自动化的批量操作,但比手动调样式快了几十倍,这个脚本也让我的浏览器打印从踩雷项变成了一键操作。
用熟之后,你会对浏览器打印机制有更深的理解:beforeprint事件、@page规则、page-break-inside、print-color-adjust这些冷门属性,平时没人提,真到了用的时候,都是救命的家伙。