news 2026/9/26 18:02:58

零脚本、零跟踪、零网络请求:kage sanitize层如何把页面的JavaScript彻底“切除“

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
零脚本、零跟踪、零网络请求:kage sanitize层如何把页面的JavaScript彻底“切除“

零脚本、零跟踪、零网络请求: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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 18:02:13

SQLite 工具类实战:用反射把 Cursor 封装到 VO 对象

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/26 18:02:10

2024一人公司必备AI工具:从电商详情页到图文创作全自动化实战指南

2024 一人公司必备 AI 工具&#xff1a;从电商详情页到图文创作的全自动化实战指南 在“一人公司”&#xff08;One Person Company, OPC&#xff09;的浪潮下&#xff0c;独立创作者和微型创业者面临的最大挑战不再是寻找商机&#xff0c;而是**生产力瓶颈**。 当你需要同时兼…

作者头像 李华
网站建设 2026/9/26 18:01:40

给AI编码代理装上“辅助轮”:Trellis框架的规范约束与工程实践

最近在折腾AI编程代理的时候&#xff0c;我越来越觉得一个事儿不对劲&#xff1a;Cursor、Copilot这些工具&#xff0c;单点补全确实香&#xff0c;但一旦让代理去跑一个跨多文件的完整任务&#xff0c;经常会出现“自以为懂了&#xff0c;结果跑偏”的情况。上下文一多&#x…

作者头像 李华
网站建设 2026/9/26 18:01:27

二手交易网站源码全栈解析:从论文到可运行系统

简介&#xff1a;这份资源是二手交易网站项目的论文与源码合集&#xff0c;面向电子商务、网络开发方向的学习者与研究者&#xff0c;尤其适合需要完成课程设计、毕业设计或想深入理解二手电商运作机制的中高级开发者。压缩包为zip格式&#xff0c;整体约53.32MB&#xff0c;文…

作者头像 李华
网站建设 2026/9/26 18:00:42

10 个最佳 Golang 库:用 TaoToken 统一 Key 打通 AI 编码工具链

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华