零脚本、零跟踪、零网络请求:kage sanitize层如何把页面的JavaScript彻底"切除"
【免费下载链接】kageShadow any website for offline viewing, with the JavaScript stripped out项目地址: https://gitcode.com/gh_mirrors/kage6/kage
kage是一个开源的离线网页镜像工具:它用真实无头浏览器渲染页面,然后把JavaScript 彻底移除(kage sanitize 层),连同 CSS、图片、字体一起落到本地,得到一份零脚本、零跟踪、零网络请求的离线副本。本文带你走读它的 sanitize/ 包,看看这个"切除"到底是怎么做到的。
为什么"另存为"不等于"拥有这个网页"
现代网页早已不是一张"纸",而是一个薄客户端:你看到的文字和图片,大半是浏览器现场执行 JavaScript 之后拼出来的。用浏览器"另存网页"保存下来的,往往是一个依赖脚本和网络的残骸——
- 打开后是一屏空白,或一个永远转圈的加载动画;
- 页面上的统计脚本仍会尝试回连已经不存在的服务端;
- 任何埋点、Cookie 追踪代码都原封不动地跟着文件走了。
换句话说:你保存的不是页面,而是一把需要对方服务器配合才能打开的锁。
kage 选择了另一条路:让页面先把 JS 跑完、把最终形态定格下来,然后再把 JavaScript 整体切除。留下来的,就是一张照片(a photograph, not a program)——它长得和原站一样,但不运行任何代码。负责这一步的,就是 sanitize 层。
kage sanitize 层在克隆流水线中的位置
kage 的完整流水线只有五步,sanitize 处于关键的第四环:
种子URL ─▶ 无头Chrome渲染 ─▶ 快照最终DOM ─▶ 切除JS(sanitize) ─▶ 资源本地化 ─▶ 写入磁盘实现上非常克制:sanitize不是用正则"搜罗"HTML 字符串,而是用golang.org/x/net/html把文档解析成一棵 DOM 树,然后逐节点遍历、精准删除,最后再渲染回 HTML。这个差别很重要——正则方案永远防不住嵌套和变形,而树遍历不会。
入口就两个函数,位于 sanitize/sanitize.go:
Strip:接收原始 HTML 字节,解析→清理→渲染,一步到位;CleanTree:接收一棵已解析的 DOM 树,原地清理。克隆器用它,这样 HTML 只解析一次,资产链接重写(asset/ 包)和 JS 切除可以共享同一棵树,省掉一次昂贵的解析。
调用点在克隆器的页面写入路径上,见 clone/cloner.go:
asset.RewriteHTML(root, j.u, sink) // 先把链接改成本地路径 sanitize.CleanTree(root, sanitize.Options{ KeepNoscript: c.cfg.KeepNoscript, MobileReadable: c.cfg.MobileReadable, Banner: "cloned by kage from " + j.u.String(), })注意顺序:先本地化链接,再切除脚本。这样即使某个资源是脚本加载的"影子",页面里已经留下的 DOM 结构也安然无恙。
六类"切除":sanitize 到底删了什么
核心遍历逻辑在clean函数中(sanitize/sanitize.go),它自顶向下走遍整棵树,对每个节点做判定。以下是它处理的六类目标:
1.<script>标签:连根拔起
所有<script>元素被整节点删除,不管它是内联代码还是src引用外部 JS。这是"零脚本"的主力。
2.on*事件处理器:逐属性剥离
<div onclick="...">、<body onload="...">这类内联事件处理器是最容易漏掉的 JS 藏身地。stripHandlers(sanitize/sanitize.go)扫描每个元素的全部属性,凡是on开头的一律删除——onclick、onerror、onmousewheel,一个不留。
3.javascript:URL:改写为无害形式
<a href="javascript:doEvil()">是第三种执行路径。neutralizeJSURLs(sanitize/sanitize.go)检查 8 种可能携带 JS 的属性(href、src、action、poster、background等),命中javascript:前缀的:
- 是
href→ 改写为#,链接还在但不再执行; - 其他属性 → 整个属性直接丢弃。
4. IE 条件注释:堵住"暗渡陈仓"的通道
这是 sanitize 层最见功力的一处。老式 IE 条件注释<!--[if lt IE 9]>...<![endif]-->在标准解析器眼里只是一条评论,而评论里可以藏一个完整的<script src="...">——纯按元素遍历的清理器永远"看不见"这个脚本。
sanitize 的策略干脆利落:识别到条件注释标记,整条评论直接删掉(sanitize/sanitize.go)。而条件注释"公开显示"的那部分内容存放在兄弟节点里,不受影响,照留不误。
5.<meta refresh>跳转:JS 版本必删
<meta http-equiv="refresh">是无需脚本的跳转/刷新机制。目标里含javascript:的无论如何都删;普通定时跳转默认也删,除非你显式开启KeepMetaRefresh选项——离线副本不该自己"动"起来。
6. 离线死链:preconnect/preload等提示符
<link rel="preconnect">、dns-prefetch、modulepreload,以及预加载.js文件的preload/prefetch——这些"资源提示"在线上是性能优化,离线后全是指向空气的请求。isDeadLink(sanitize/sanitize.go)把它们识别出来并移除,从根源上保证"零网络请求"。
切除之外:sanitize 还在默默"修补"
sanitize 不只是减法,还有几处贴心的加法,让离线副本在任何阅读器里都自洽:
| 操作 | 作用 |
|---|---|
补charset声明 | 若源站只在 HTTP 头里声明了编码,保存为独立文件后会丢失、导致乱码。sanitize 会在<head>顶部补一个<meta charset="utf-8">,让页面自我描述 |
<noscript>解包(可选) | 默认删除<noscript>;开启KeepNoscript后,会把其中的真实内容解包保留——专为那些把正文藏在 JS 检测后面的老站 |
| 移动端可读注入(可选) | MobileReadable会为 90/00 年代表格布局老站注入viewport和一段 CSS:放大字体、让固定像素表格变流式、隐藏离线无用的图片地图导航 |
| 克隆横幅 | 在文档头部插入一条 HTML 注释,标注"由 kage 从某 URL 克隆",方便日后溯源 |
保留清单同样明确:样式表、图片、字体、表单、以及全部语义化标记原封不动——布局完整存活,只是不再"运行"。
一次切除的"账单":Report 统计结构
每次清理都会生成一份Report(sanitize/sanitize.go),逐项计数:
ScriptsRemoved:删了多少<script>;HandlersRemoved:剥了多少on*属性;JSURLsNeutralized:改写/丢弃了多少javascript:URL;MetaRefreshRemoved、DeadLinksRemoved、CondCommentsRemoved:跳转、死链、条件注释各删了多少;NoscriptRemoved/NoscriptUnwrapped:<noscript>的去向;CharsetAdded:是否补了编码声明。
这份账单既会出现在克隆运行的汇总统计里,也是测试断言的依据——每一项"切除"都有据可查,而不是"感觉删干净了"。
测试如何保证"切得干净"
sanitize/sanitize_test.go 覆盖了全部关键路径,用例名本身就是一份清单:
TestStripRemovesAllJS—— 脚本、处理器、javascript:URL 一网打尽;TestConditionalCommentScriptRemoved—— 条件注释里的暗藏脚本被清除;TestConditionalCommentRevealedContentKept—— 而公开显示的内容完整保留;TestKeepNoscriptUnwraps—— 解包模式工作正常;TestDoctypePreservedAndBannerFollowsIt—— 横幅永远插在<!DOCTYPE>之后,避免把老阅读器逼进怪异模式;TestCharsetAddedWhenMissing/TestCharsetNotDuplicated—— 编码声明补而不重。
想动手验证?克隆一个站点后打开任意.html文件搜索<script,结果应当为空——而页面的图片、排版、链接却都在原位。
结语:一张照片,而不是一个程序
回到开头的问题:为什么"另存为"保存不住网页?因为网页的生命在 JavaScript 里,而脚本既不能离线存活,也带着跟踪与回连的副作用。
kage 的 sanitize 层给出的答案是:先让真实浏览器把页面"活完",定格下人类实际看到的那个 DOM,再用树遍历把这个 DOM 里的所有执行能力——脚本标签、内联处理器、javascript:链接、条件注释后门、meta 跳转、死掉的资源提示——逐类切除。最终落盘的.html文件:
- ✅零脚本:
<script>与on*属性一个不剩; - ✅零跟踪:埋点代码根本不存在于文件里;
- ✅零网络请求:预连接与预加载提示已清除,CSS/图片/字体全部本地化;
- ✅自洽可读:编码声明补齐,任何离线阅读器(kage 自带 viewer、Kiwix 等)打开即所得。
正如 sanitize/sanitize.go 包注释里那句话——
"a photograph, not a program"(一张照片,而不是一个程序)
这就是离线镜像的完整体。更多使用方式,可参考 clone/ 目录下的爬虫实现与 docs/content/ 中的完整指南。
【免费下载链接】kageShadow any website for offline viewing, with the JavaScript stripped out项目地址: https://gitcode.com/gh_mirrors/kage6/kage
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考