article_image

20240712 更新:将删除多余元素的小书签(bookmarklet)更换为更新的版本,以兼容当前的浏览器。感谢读者 Flez 提供的代码。 注意,由于 Safari 的限制,需要先授予 Keyboard Maestro 运行 Javascript 的权限,才能获取网页标题以作为文件名称。具体方法见 《一种几乎永不失效的网页中英对照翻译方案》

《一种将线上内容精简格式后保存到本地的方法》 源自 Hum 和我的一系列讨论,我们意识到,互联网上的文章都是不可靠的。我们主动提供保存网页的工具,在这个随处都是内容订阅的时代,或许有幸被戴上屠龙勇士的桂冠;但是没有人能够保证,勇士自己不会成为下一个魔王。所以从一开始,我就不会要求任何人信任某个平台(包括我们);相反,我直接把屠龙的武器分发到每一个自由民手中。

我在 《剪藏网页到本地的自动化思路(以 Keyboard Maestro 为例)》 中将 Hum 的方法转为自动化流程。不过,该方案仍然和 DEVONthink 相连甚密,对于多数读者来说都是一个不小的门槛——遑论不用 macOS 的人了。尽管配套工具是我编写的,但在制作完成后,我很快开始寻找更加轻便的方法,遂有此文。

Alt text
用轻量级方法导出的网页 PDF 示例文章 作者:Elenor Botoman

反思 DEVONthink 剪藏思路

首先就是贵。DEVONthink 要一千多块钱,恐怕没有急迫需求的人是买不下手的。而更根本的问题在于,DEVONthink 毕竟太重,需要把网页源码下载到本地,然后渲染回网页,再删删减减一番,最后导出为 PDF。这时候,我会怀念 Evernote 那轻巧的操作,不用离开浏览器,点几下就能剪藏好页面——尽管虽然经常失效,这也是我自己研究工具的起因之一。

打个比方,DEVONthink 之于网页剪藏,正如 Word 之于文字编辑,或者 Photoshop 之于图片处理。你基本可以用 Word 做出任何花哨的排版,或者用 Photoshop 创造出令人惊叹的图片,但这一般都是个大工程。有些人——包括我——更喜欢轻便的工具,而 DEVONthink 则适合作为兜底的备用工具。

另一个问题——并不全是 DEVONthink 的缘故——则是我和 Hum 在偏好上的分歧。他喜欢 Safari 阅读模式之干净白练,宛如纸张;而我更希望保留网页原始排版(只要别太烂),因为网页排版也是文章的一部分。The Verge 排版刺激视觉,各种野兽派配图和赛博朋克配色,旨在挑动读者神经;Long Now 博客试图让读者沉下心来阅读,两者截然不同。如果都用同一个排版,尽管整洁,但也丢失了文章的个性。

一句话,DEVONthink 是非常可靠的兜底方案,并且能够一股脑儿地解决问题;但它的用起来有些笨重,而用阅读模式导出的文章又像一个模子里出来的。我决定另寻出路,直接在浏览器里解决剪藏问题,并且保留网页原始排版。以下方案并不能代替 DEVONthink,但可能是一个有用的补充。

另一套网页剪藏方案

DEVONthink 一次性解决了两个难题,第一是清理网页上的繁杂元素,比如二维码和广告;第二则是导出不分页的 PDF,以免把文章中的配图拦腰截断。别辟门户之后,首先要重新解决这两个问题;此外,才是针对浏览器环境做一点修修补补。

删除多余元素

就删除多余元素而言,DEVONthink 的解决方案是提供一个完整的 HTML 编辑环境——当然在 Hum 的整个流程中,还要先用阅读模式过滤一遍——以便你像编辑 Word 文档一样轻松删除不需要的信息。在浏览器中搭建一个 HTML 编辑器显然很困难,但是有一个取巧的方法:直接删掉 HTML 元素。熟练的网页开发者固然知道在调试界面中删除元素,而我发现了一个更方便的 Bookmarklet,可以把光标变成剪刀,径直在页面上删除多余内容(但我找不到这个 Bookmarklet 的原始作者,知道的读者请告知我)。诸如悬浮目录、返回按钮、广告横幅或页面底部的推广二维码,凡是不乐意在 PDF 中看到的,均可点击删除。

你可以把下方代码收藏为书签(或先随便创建一个书签,然后改其链接为下方代码),日后需要剪除网页元素时,点一下即可。感谢读者 Flez 提供的改进版本,以下内容目前是 Flez 的版本。

javascript: (function%20()%20%7B%20%20%20var%20isIe%20%3D%20false%3B%20%20%2F*%40cc_on%20isIe%3Dtrue%3B%20%40*%2F%20%20function%20fe(a%2C%20fn)%20%7B%20%20%20%20%20var%20i%2C%20l%20%3D%20a.length%3B%20%20%20%20%20for%20(i%20%3D%200%3B%20i%20%3C%20l%3B%20i%2B%2B)%20%7B%20fn(a%5Bi%5D)%3B%20%7D%20%20%20%7D%3B%20%20%20function%20ae(el%2C%20n%2C%20fn%2C%20ix)%20%7B%20%20%20%20%20function%20wfn(ev)%20%7B%20%20%20%20%20%20%20var%20el%20%3D%20(isIe%20%3F%20window.event.srcElement%20%3A%20ev.target)%3B%20%20%20%20%20%20%20if%20(n%20%3D%3D%20%27click%27)%20%7B%20%20%20%20%20%20%20%20ev.stopPropagation()%3B%20%20%20%20%20%20%20%20ev.preventDefault()%3B%20%20%20%20%20%20%20%7D%20%20%20%20%20%20if%20(ix%20%7C%7C%20!el.xmt)%20%7Bfn(el)%3B%7D%20%20%20%20%20%7D%20%20%20%20%20if%20(isIe)%20%7B%20%20%20%20%20%20%20n%20%3D%20%27on%27%20%2B%20n%3B%20el.attachEvent(n%2C%20wfn)%3B%20%20%20%20%20%7D%20else%20%7B%20%20%20%20%20%20%20var%20isUseCapture%20%3D%20(n%20%3D%3D%20%27click%27)%3B%20%20%20%20%20%20el.addEventListener(n%2C%20wfn%2C%20isUseCapture)%3B%20%7D%20%20%20%20%20if%20(!el.es)%20el.es%20%3D%20%5B%5D%3B%20%20%20%20%20el.es.push(%20%20%20%20%20%20function%20()%20%7B%20%20%20%20%20%20%20%20%20if%20(isIe)%20%7B%20el.detachEvent(n%2C%20wfn)%3B%20%7D%20%20%20%20%20%20%20%20%20else%20%7B%20el.removeEventListener(n%2C%20wfn%2C%20isUseCapture)%3B%20%7D%20%20%20%20%20%20%20%7D)%3B%20%20%20%20%20el.re%20%3D%20function%20()%20%7B%20fe(el.es%2C%20function%20(f)%20%7B%20f()%20%7D)%3B%20%7D%3B%20%20%20%7D%3B%20%20function%20sce(el)%20%7B%20%20%20%20%20var%20oldclick%20%3D%20el.onclick%2C%20oldmu%20%3D%20el.onmouseup%2C%20oldmd%20%3D%20el.onmousedown%3B%20%20%20%20%20el.onclick%20%3D%20function%20()%20%7B%20return%20false%3B%20%7D%3B%20%20%20%20%20el.onmouseup%20%3D%20function%20()%20%7B%20return%20false%3B%20%7D%3B%20%20%20%20%20el.onmousedown%20%3D%20function%20()%20%7B%20return%20false%3B%20%7D%3B%20%20%20%20%20el.rce%20%3D%20function%20()%20%7B%20el.onclick%20%3D%20oldclick%3B%20el.onmouseup%20%3D%20oldmu%3B%20el.onmousedown%20%3D%20oldmd%3B%20%7D%3B%20%20%20%7D%20%20%20if%20(!window.r_)%20window.r_%20%3D%20%5B%5D%3B%20%20%20var%20r%20%3D%20window.r_%3B%20%20%20var%20D%20%3D%20document%3B%20%20%20ae(D.body%2C%20%27mouseover%27%2C%20function%20(el)%20%7B%20el.style.backgroundColor%20%3D%20%27%23ffff99%27%3B%20sce(el)%20%7D)%3B%20%20%20ae(D.body%2C%20%27mouseout%27%2C%20function%20(el)%20%7B%20el.style.backgroundColor%20%3D%20%27%27%3B%20if%20(el.rce)%20el.rce()%3B%20%7D)%3B%20%20%20ae(D.body%2C%20%27click%27%2C%20function%20(el)%20%7B%20%20%20%20el.style.display%20%3D%20%27none%27%3B%20%20%20%20%20r.push(el)%3B%20%20%20%7D)%3B%20%20%20function%20ac(p%2C%20tn%2C%20ih)%20%7B%20var%20e%20%3D%20D.createElement(tn)%3B%20%20%20%20%20if%20(ih)%20e.innerHTML%20%3D%20ih%3B%20%20%20%20%20p.appendChild(e)%3B%20%20%20%20%20return%20e%3B%20%20%20%7D%20%20%20var%20p%20%3D%200%3B%20%20%20var%20bx%20%3D%20ac(D.body%2C%20%27div%27)%3B%20%20%20bx.style.cssText%20%3D%20%27position%3A%27%20%2B%20(isIe%20%3F%20%27absolute%27%20%3A%20%27fixed%27)%20%2B%20%27%3Bpadding%3A2px%3Bbackground-color%3A%2399FF99%3Bborder%3A1px%20solid%20green%3Bz-index%3A9999%3Bfont-family%3Asans-serif%3Bfont-size%3A10px%27%3B%20%20%20function%20sp()%20%7B%20%20%20%20%20bx.style.top%20%3D%20(p%20%26%202)%20%3F%20%27%27%20%3A%20%2710px%27%3B%20%20%20%20%20bx.style.bottom%20%3D%20(p%20%26%202)%20%3F%20%2710px%27%20%3A%20%27%27%3B%20%20%20%20%20bx.style.left%20%3D%20(p%20%26%201)%20%3F%20%27%27%20%3A%20%2710px%27%3B%20%20%20%20%20bx.style.right%20%3D%20(p%20%26%201)%20%3F%20%2710px%27%20%3A%20%27%27%3B%20%20%20%7D%20sp()%3B%20%20%20var%20ul%20%3D%20ac(bx%2C%20%27a%27%2C%20%27%20Undo%20%7C%27)%3B%20%20%20ae(ul%2C%20%27dblclick%27%2C%20function%20()%20%7B%20var%20e%20%3D%20r.pop()%3B%20if%20(e)%20e.style.display%20%3D%20%27%27%3B%20%7D%2C%20true)%3B%20%20%20var%20ual%20%3D%20ac(bx%2C%20%27a%27%2C%20%27%20Undo%20All%20%7C%27)%3B%20%20%20ae(ual%2C%20%27dblclick%27%2C%20function%20()%20%7B%20var%20e%3B%20while%20(e%20%3D%20r.pop())%20e.style.display%20%3D%20%27%27%3B%20%7D%2C%20true)%3B%20%20%20var%20ml%20%3D%20ac(bx%2C%20%27a%27%2C%20%27%20Move%20%7C%27)%3B%20%20%20ae(ml%2C%20%27dblclick%27%2C%20function%20()%20%7B%20p%2B%2B%3B%20sp()%3B%20%7D%2C%20true)%3B%20%20%20var%20xl%20%3D%20ac(bx%2C%20%27a%27%2C%20%27%20Exit%20%27)%3B%20%20%20ae(xl%2C%20%27dblclick%27%2C%20function%20()%20%7B%20D.body.re()%3B%20bx.parentNode.removeChild(bx)%3B%20%7D%2C%20true)%3B%20%20%20fe(%5Bbx%2C%20ul%2C%20ml%2C%20xl%2C%20ual%5D%2C%20function%20(e)%20%7B%20e.style.cursor%20%3D%20%27pointer%27%3B%20e.xmt%20%3D%201%3B%20%7D)%3B%20%7D)()
Alt text
修剪前后的 PDF 示例文章来源:知产力

诚然,编辑页面元素的 Bookmarklet 只能删而不能改,更不能增加图文,不如 DEVONthink 那么全能。但这一方案胜在小巧,在网页上随手点几下,原本杂乱的页面就变得清爽怡人。此外,Bookmarklet 不限于特定浏览器,Chrome、Firefox 和 Edge 等主流浏览器的用户均可受惠——甚至,手机也可以用 Bookmarklet 清理网页,配合 iOS 近几个系统的全网页截图,很容易导出流畅干净的网页,试想在 iPad 上阅读文章,顺手编辑一下就能导出,如同自由处置买回家的杂志一样,令人愉快。

导出连续的 PDF

至于第二个问题 ,即导出不断页的、连续的 PDF(DEVONthink 谓之 Continous PDF)。部分读者可能尚未读过 Hum 的文章,这里解释一下,所谓断页就是分页的 PDF 在遇到图片时,可能会将其直接截成两端,塞在不同的页面上,很是粗鲁。

Alt text
被截断的图片

事实上,Safari 本身就可以导出连续的 PDF。通常情况下,直接打印页面得到的 PDF 固然是分页法的;而使用“Export as PDF…”选项导出 PDF,则可以得到连续的 PDF。这一选项类似于 iOS 设备上的全网页截图,即在 Safari 中按下截图后,旋即点击缩略图,就能看到的“整页”选项。

Alt text
在电脑和手机上都可以导出整个网页为 PDF

导出连续 PDF 的好处,不仅在于能够避免图片被截断,还可以最大程度保留原网页的排版。很多页面的题图(banner)非常养眼,但 Safari 的阅读模式不一定能够抓取它,或者不认为它是文章的一部分,因此用阅读模式导出的 PDF 多多少少缺了一些原文的风味。而导出连续的 PDF,通常可以连同题图等特殊部分一起导出。此外,很多人阅读技术类文章时希望配有代码高亮,这也是阅读模式的软肋,而直接导出 PDF 则不存在破坏原网页高亮效果的担忧。偶尔遇到不能正常获得题图的,很可能是原网页将其设置为背景,因此在打印时勾选背景选项即可;部分网页会提示这一细节,避免打印时丢失背景图片。许多英文博客就把题图当作背景图处理,因此不能直接打印,阅读模式亦不逮,碰上这些空白图片,试试打印背景往往会有惊喜。

Alt text
打印背景是打印题图的一种方式 示例文章作者:Elenor Botoman

不过,导出连续 PDF 也不是完美的方案,其旨在保留原网页视觉风格,但在其他方面有所欠缺。显而易见的就是体积占用,如果直接打印的分页 PDF 只有 2mb(下图左),那么连续 PDF 通常就有两三倍的大小(下图中),要是再带上背景图(下图右),占用就更多一些。此外,如果页面太长,连续的 PDF 恐怕也不便于翻阅和加书签——毕竟只有一页,连“翻”和加书签的动作都做不到——要查找资料,就得依靠搜索,或干脆上下来回滚动。其中利弊,还是交给读者自己权衡。

Alt text
不同的导出方案,文件体积有所不同

后记

就我个人而言,DEVONthink 一直在后台运行,时不时就要打开一下,其实并没有必要批评 DEVONthink 剪藏方案多么笨重。改良的初衷其实是希望保留网页特色。最初的想法,是在 DEVONthink 剪藏的基础上增添一些花样,比如加上网页原有配色,或者插入网站图标……

Alt text
搁置的剪藏方案

差不多实现了之后,最初的剪藏工具变得越发庞大,而且花样一多,也就不再鲁棒,频频遇到无法正常剪藏的网页——和我不得不割爱的 Instapaper 一样。此时我才决定换个方向,舍弃 DEVONthink,转而直接在网页上操作。此时我发现了前文介绍的网页编辑 Bookmarklet,又正好读到 Eclectic Light 的 这篇 文章,对 Safari 导出 PDF 的机制有了更多认识——之后的事情,就很简单了。

🛍 我撰写的付费栏目《信息管理,文件为本位的方案》正在 UNTAG 售卖,对本文话题有进一步讨论,欢迎选购。

🔗 付费栏目链接


author_avatar

Lawyer, macOS/iOS Automation Amateur