整站抓下来、路径一条条对上、链接改成本地能点的;顺手发现它把 404 页面也当页面存了
这篇的图和大部分数字来自我自己跑的一次:打包好的 exe 在 8123 端口,抓的是本机 8801 上的一个测试小站。另有一处 212 万页的规模数据,来自这台机器上以前那次任务的记录。没跑过的部分逐条标注【本人未实测】。
今天下午我把打包好的 exe 双击开,让它把一个本地小站整个搬了一遍。6 个页面、5 个资源、10 个文件,0.6 秒。跑完我干了件更笨的事:把原站文件和镜像出来的文件一行一行对了一遍。【本人实测】
对完才知道它到底值不值。
左边填网址和参数,中间看进度和日志,右边是已保存的文件和预览。三栏挤在一个屏幕里,1280 宽刚好
它干的活,是把网址翻译成文件夹
规则不难,难在一一对应。我拿一个带参数的网址试了它那个「预览落盘路径」:
https://example.com/news?id=12
落成 example.com/news/@id=12/index.html。
填两个网址,点一下,它先告诉你第一个会落到哪。这一步不联网,纯本地算
?id=12 变成 @id=12 这一层目录,/news 后面补 index.html。目录斜杠换成真实目录,问号换成能当文件名的字符。
那个「预览」按钮我觉得比看上去有用。抓之前先确认路径,比抓完几千个页面发现全错了要省事得多。
端口也会进目录名。我这次抓的是 127.0.0.1:8801,落下来一级目录叫 127.0.0.1_8801。不是默认端口就带后缀,不然两个站会撞在一起。
这次跑出来的九个数字
九个格子:6 个页面、5 个资源、11 个文件、2.9 KB、0.6 秒、失败 0。速度那两个 11 是 6 加 5 除出来的
看板写「文件总数 11」,报告里写「文件总数 10」。
差一个。我猜是报告文件自己算进看板没算进报告,也可能反过来【个人看法】。我没去翻代码确认,就这么放着。这种小口径不一致,用的时候要留意一下,别拿去跟别人报数。
真正要盯的是「失败 0」这四个字。后面有一节专门说它。
目录长什么样
- href="/css/style.css"
+ href="css/style.css"
- href="/about/"
+ href="about/index.html"
- href="/news?id=12"
+ href="news/@id=12/index.html"
- url('/img/logo.png')
+ url('img/logo.png')跟源站的结构是对得上的,多了一层主机名当根目录。
img 里那两个文件值得看一眼:logo.png 和 logo@size=small.png。源站同一个地址带不同查询参数,落下来是两个文件、两个名字。要是它把参数丢掉,这两个就会互相覆盖,剩一个。
预览区能直接打开,不是看源码
预览里就是镜像下来的首页:Site Index、四个导航链接、下面那段 inline style 的背景图都在
这一步比我想的顺。右边点开 index.html,中间那块用 iframe 直接把页面渲染出来,标题、导航、连那段写在 style 属性里的背景图都还在。
能渲染出来,说明两件事都成了:CSS 抓到了,链接改成相对路径了。少一样都是白页。
页面上有个「新窗口打开镜像页面」,点开是按原路径提供文件的方式。这样页面里相对引用的图片、样式才能正常加载。
47 行文件,改了 13 行
这是我这次最想说的部分。
四个文件,一共 47 行【本人实测】。我把原站的和镜像的摆一起 diff:index.html 21 行改了 8 行,style.css 7 行改了 2 行,page1.html 8 行改了 2 行,about 的 index.html 11 行改了 1 行。合起来 13 行。
改的只有下面这几处链接属性:
- <link rel="stylesheet" href="/css/style.css">
+ <link rel="stylesheet" href="css/style.css">
- <a href="/about/">关于我们</a>
+ <a href="about/index.html">关于我们</a>
- <a href="/news?id=12">新闻(带参数)</a>
+ <a href="news/@id=12/index.html">新闻(带参数)</a>
- <div style="background:url('/img/logo.png')">
+ <div style="background:url('img/logo.png')">全是 href、src、url() 这三类。属性顺序没动,缩进没动,单引号双引号没动,<img> 也没被补成自闭合。
CSS 里那两行也一样:@import url("/css/extra.css") 改成 url("extra.css"),.hero 的 url("/img/logo.png") 改成 url("../img/logo.png")。绝对路径变相对路径,层级还算对了。
外站链接原样留着,一个字没改。
【个人看法】这件事听着枯燥,但它是这类工具最容易翻车的地方。用解析库读进来再整体输出,标签会被重排、属性会被排序、空元素会被补斜杠——页面看着一样,源码已经不是原样了。它选择只动该动的字节,代价是要自己算偏移,好处是镜像下来的东西还叫镜像。
README 里记着一个更极端的例子:对 example.com 做字符级 diff,全页只改了 src=/s.js 到 src=s.js 一处。【本人未实测】我没复现那个站,只复现了自己这个小站。
那个 404,和那句「0 失败」
五行日志。第四行是 [404],最后一行写着「页面 6 成功 / 0 失败」。两个数字挨着,看着别扭
日志里有一行 [404] http://127.0.0.1:8801/news?id=12,最后一行统计是「0 失败」。两句话在同一屏上。
我把落盘的那个文件打开了:
127.0.0.1_8801/news/@id=12/index.html
<title>Error response</title>
<h1>Error response</h1>
<p>Error code: 404</p>
<p>Message: File not found.</p>它把 404 页面本身当成一个正常页面存下来了,失败计数不算它。【本人实测】
为什么会这样,我大概能想明白:服务器回了内容,工具就按内容存了,只有连接断了、超时了才算失败【个人看法】。逻辑上说得通,用起来得留神——镜像里可能混着几张错误页,而看板上写着 0 失败。
要判断得看日志那几行 [404],或者回原始站点对一遍。这一点我记下了。
样式全丢这个坑,是设计出来的
很多站的 CSS 和图片不在页面上那个域名,在兄弟子域或者 CDN 上。工具里「页面范围」和「资源范围」是分开的两个设置:
same_host 只抓同一个主机 same_site 同主域,允许兄弟子域(默认) any 任意主机,含第三方 CDN
默认那个是我认为合理的选择。要是按「同主机」理解,一整站页面抓下来会是纯文本——一个字都不少,一点样式都没有。
页面链接仍然受同域限制,放开资源范围不会让它爬到别的站去。
抓漏了也不用重抓:repair_assets.py 会扫一遍已有的 HTML,把还指向外部的 CSS、JS、图片补下来,再改链接。这个我没跑【本人未实测】,只是读到有这么个东西。
断点续抓,和大小写这个坑
关窗口、崩溃、重启,重新指到同一个输出目录就能接着抓。靠三个文件:
_mirror_queue.tsv 入队流水账,一行一个 URL _mirror_manifest.ndjson 已落盘清单,URL 到本地路径 _mirror_state.json 进度快照,只放小标量
续抓时的算式很朴素:待抓 = 入队流水 − 已落盘。
Windows 有个躲不开的问题:文件系统不认大小写。源站要是同时存在 id_ab.html 和 id_AB.html,两个会互相覆盖。
README 里记着一次事故的量级:189,343 组冲突,208,163 个页面被覆盖【本人未实测】。后来的做法是给命中的文件名加 8 位哈希后缀,改成 id_ab-1a2b3c4d.html。这个设计对不对,我觉得对——但它是被覆盖了二十万个页面之后才补上的。
越抓越慢,原因在快照上
这条我读的时候印象很深。
早期版本把已访问、队列、映射三个集合整个塞进快照文件。抓到 28.5 万个网址的时候,快照 87.2 MB,序列化一次 1.34 秒,而且是在事件循环线程上做的——每 5 秒把所有下载线程冻住 1.34 秒。
改完之后,87.2 MB 降到 3.7 KB,吞吐从 3.59 涨到 5.32 个每秒。同一个任务,加 48%。
写盘那边也有个数:默认线程池只有 8 个槽位,写一个 7KB 的页面中位延迟 1.8 秒、最长超过 3.7 秒(单目录里堆到过 149 万个文件)。槽位放大到 64 个以后,吞吐从 4.6 涨到 19.4 文件每秒。
【本人未实测】这几个数都是当初跑出来的记录,我没复现。我这次抓的站太小了,10 个文件 0.6 秒,什么都验不出来。
两个速度,别只看平均
看板上并排放着「实时速度」和「平均速度」。
我这次两个都是 11,看不出差别。但记录里有个更典型的例子:一个跑了 25 小时的任务,修完之后真实吞吐已经到 5.2 个每秒,面板上的平均速度还挂着 3.3。分子分母都被前面那段低谷拖着。
这种时候看平均会得出「提速没生效」的错误结论。所以它把两个都摆出来了,实时那个还标了蓝色。
我跑了它的测试,58 项里挂了 1 项
python -m pytest tests -q 1 failed, 57 passed in 2.52s
挂的那项叫 test_restore_rebuilds_visited_saved_pending,测的是续抓时怎么还原队列。它往 _mirror_queue.ndjson 里写测试数据,而代码现在读的是 .tsv 那个名字。
名字对不上,所以待抓列表是空的。
这是测试没跟上重构,不是功能坏了。我给这个判断有依据:磁盘上实际生成的就是 .tsv,我这次跑完看到的也是 _mirror_queue.tsv,31 字节。
顺带一句:README 上写「全部 44 项」,实际收到 58 项。文档也落后了。
【个人看法】这两处都算不上大毛病,但一个工具说自己有测试,你就顺手跑一遍,比只看 README 靠谱。
打包这件事,我踩了个坑
上一轮把它打成免安装包的时候,单文件模式在这台机器上根本起不来,报错是 Could not create temporary directory。
我一度以为是临时目录权限、磁盘、短文件名的问题,换 C 盘、D 盘、系统 Temp 都不行。最后拿一个空程序做对照,同样是这句报错——说明是单文件模式本身在这台机器上不工作,跟代码无关。
改成目录模式:不解压到临时目录,启动 2.3 秒,双击就开浏览器。
包 22.5 MB,解压后 737 个文件,不用装 Python。
打包时改了两处源码,都留了备份:一是把数据目录指到 exe 同级,否则下载的文件会落进临时解包目录,退出就清空;二是静态目录兼容打包后的路径。
它做不到的
全靠在 JavaScript 里渲染的站点,抓下来是空壳。它不做浏览器渲染。
由脚本在运行时拼出来的资源路径也抓不到,比如变量加个文件名的写法,静态扫描看不见,得手动补。
字符集是另一套麻烦。它统一按 UTF-8 落盘,并把文件里的编码声明一起改掉,浏览器打开正常;但 txt 和 json 用老版记事本开可能乱码,因为没写 BOM。
默认不校验 TLS 证书,为了兼容自签名的站。要严格校验得自己改代码。
需要登录才看得到的页面,我没试过【本人未实测】。
合规这件事得自己管。有遵守 robots.txt 的开关,有并发和请求间隔,但默认并发 8、间隔 0。抓别人的站,这两个值得手动调一下。
下载
Windows 10 或 11,64 位,免安装。解压双击 WebMirror.exe,浏览器会自己打开 127.0.0.1 的 8000 端口。
详情页 www.7tan.com/game/999/,直链 WebMirror_v1.0.0_win64.zip。
包 22.5 MB,MD5 是 ac080ad587501ed5bef329f6753bfa57。
值不值得留
我想了想自己会拿它干什么。
整站备份、把文档站扒下来离线看、把要关掉的老站存一份,这三件事它干得比手工利索。路径一一对应这一点,是它跟「另存为」最大的区别——另存为给你一堆平铺的文件,它给你一棵跟原站一样的树。
抓单页应用、抓登录后才有的内容,这两件它不擅长,或者说我这次没验到能行。
几百万页的大站不在这条里。这个真跑过:一个邮编站,212 万页,跑了一百二十四小时,落下来 14 GB,失败 37 页【本人实测】。停下的时候队列里还剩六万七千条没走完,是我自己停的——所以是「走到了 212 万页」,不是「跑完了」。
那批文件现在不在了,任务记录还留着。
用之前记住两句:先点「预览落盘路径」确认落到哪,跑完翻一眼日志里有没有 404 和 5xx。
你打算拿它搬哪个站?如果是不带脚本的老站,它大概一次就能过。