接上 2022 年的老档案:多的不是记录,是二十个月的编辑史
前代那个命令行工具 2022–2024 存下 782 MB 页面。转成档案接进来之后,记录只多了 7 条——而「改过不止一次」的标记从 8 条变成 147 条。
2020 年那一版 its-my-data/doubak 是个 Go 写的命令行抓取工具,早就归档了。但它抓下来的东西还在硬盘上: 782 MB、7353 个 HTML、2022-12-25 到 2024-08-11 之间的四次抓取。
新写的 doubak-import-adapters 把它们转成豆备的档案格式,于是解析器一行都不用改就能读——它们从此是一等的档案, 而不是下游某处的特例。
先说值不值
把那 7 份导入档案和扩展 2026 年抓的 17 份放进同一个目录一起解析:
| 只有 2026 那 17 份 | 加上导入的 7 份 | ||
|---|---|---|---|
| 标记 | 2950 | 2955 | +5 |
| 广播 | 3411 | 3413 | +2 |
| 标记修订 | 2959 | 3105 | +146 |
| 广播修订 | 3480 | 3856 | +376 |
| 改过不止一次的标记 | 8 | 147 | ×18 |
记录数只多了 7 条。要是冲着「多备份点东西」去,这件事根本不值得做。
值钱的是下面那两行。2026 年的档案里整部编辑史只有 9 条修订——因为第一次抓取就是 2026 年 7 月,在那之前没有任何观测。导入之后多出来的,是那 20 个月里真实发生过的编辑, 而豆瓣自己不保存这些:
movie/26869078
2022-12-26 → 2023-12-18 想看 「又来了,先听歌再马克剧」
2024-08-11 在看 「开始看了,想起来是 Aimyon 的主题曲……
//又来了,先听歌再马克剧」
2026-07-31 → 2026-08-01 看过 ★3 「感觉是东京郊区宣传片……//开始看了……」
一条完整的 想看 → 在看 → 看过,跨四年。豆瓣页面上只剩最后那一行。
这不是「解析老 HTML」
页面本身一个字节都不用动,它们是真的。缺的是那个工具从来没记录过的元数据: 这个 URL 当初是为什么抓的、什么时候抓的、它的地址是什么、响应可不可信。
其中响应头是彻底没有的。而 WARC 的 response 记录结构上必须有个状态行—— 也就是说导入的时候必须编一个出来。
规范为此加了一个新的成色取值,
bundle/1.3:
capture_fidelity: "decoded_body+synthesized_headers"
↑ 正文是真的 ↑ 响应头是编的
规范同时明令不许把它降级成 decoded_body+filtered_headers——
那个取值宣称响应头来自服务器、只是被浏览器过滤过。
「被过滤的真头部」和「凭空造的假头部」之间的区别,恰恰是取证时唯一要问的那个问题。
同一条原则往下推:index 里的 http_status 字段是可选的,那就不写——
能不编就不编。
判定不能编,因为它读的是正文
头部可以编,判定不行——判定读的是正文,而正文是真的。
这不是个理论问题。前代工具把登录页按数据文件名写在了磁盘上,没有任何标记。 下游看到的只会是「文件在,里面 0 条」。把抓取器那份分类器原样跑一遍,7353 个页面:
ok 5051 login 2270 soft404 25 0 字节 7
那 2270 个里包含整整一次抓取:2023-01-27 那天是登出状态跑的。 而未登录看到的列表页看起来完全正常,只是标签那一栏根本不渲染。照单全收的话:
天真摄取 3049 条修订 / 2534 条记录 ← 其中 2856 条是凭空捏造的
诚实摄取 193 条修订 / 2526 条记录
拒掉那 2270 个页面的全部代价,是 1 条别处没有的标记。 换来的是不捏造 2856 条「这天他把所有标签都删了」。这个不对称大到没什么可讨论的。
链:七个起点,而这是正常的
每一份档案都记着上一份是谁(previous_bundle_id),
增量抓取就是这么串起来的。导入器也照做:它把老工具那四次抓取按日期串成自己的一条链。
于是现在那个目录里是这样的——
98e6c6 ← 589b88 ← 8a5722 ← e91450 ← 67535d ← ac3bcb ← 4983ef ← 导入的(2022-12 → 2024-08)
d40c1d
786e5c ← afb38b ← a54168
3eef52 ← 627045 ← f72157 ← 354a1d ← 9f5719 ← c34601 ← ba57a3
0fb09c ← 157e63 ← 73f85f ← b10d6c ← 4b82f3 ← 0ddb70
b3c2b6 ← 0484cb
六条链,互不相连。这件事以前写过: 合并的结果恰好是并集,所以「该挑哪一条链」这个问题不存在—— 挑任何一条都会丢掉别的链上的东西。删掉重抓、换台机器、同一天跑两次增量, 都会产生新的起点,而这全都正常。
但导入的档案绝不许当基准
链还有第二个用途:增量抓取要靠上一份档案的水位线决定从哪儿开始读。 「这条线以上全都抓到了」这句话,是下一次抓取可以少读几千页的唯一依据。
所以导入器有一条硬规矩:
导入的档案永远 advanced: false、floor_time: null。
它绝不设水位线。
道理很直接:老工具最后一次抓取是 2024-08-11。要是那份导入档案设了水位线, 下一次增量抓取就会从 2024 年开始读,然后停下——2024 到 2026 之间的两年, 再也不会被读第二遍。一次为了省事的「继承」,换来两年的空洞, 而且是安静的:抓取会正常结束,报告会说一切正常。
同一条思路还决定了另外两件事:
- 按自然日切档案,不按时间戳。判据只有一条: 一条路线的页面不能被劈进两份档案——劈了的话,它的连续性证明在两边都不成立, 一次完整的枚举会被记成两次不完整的。实测这批数据里没有任何一条路线跨过日界。
- 档案编号由「适配器 + 那一天」算出来,不是随机的。 一次抓取是一次观测,值得一个新编号;而一次导入是对同一批冻结字节的转换, 导两次不该长出第二份平行档案。(不承诺产物逐字节相同,也不该承诺: WARC 要求记录编号全局唯一,重导一次那些 uuid 必然不同。)
它撬出来一个一直都在的解析器 bug
豆瓣的标记列表页上有个 data-cid 属性,是那条标记自己的编号。
解析器拿它当身份;没有的时候退回一个「账号 + 媒介 + 作品号」拼出来的降级键。
而 data-cid 是 2023 年 12 月前后才开始出现的。
也就是说,任何一个同时装着这条线两侧档案的目录,都会让每一条跨过那条线的标记裂成两半——
两套键根本对不上。实测:
2526 个作品 → 4050 条标记
而编辑史——这次导入的全部价值——正好被砍掉一半。
失败形状是最坏的那一种:不报错,而且看起来像数据变多了。
修法是让降级键当「这条记录目前落在哪个键下」的锚:后来出现了真编号,
记录就迁移过去。而两个不同的真编号仍然各成一条——
那是真的删掉重标,也正是 data-cid 唯一能看见、降级键看不见的东西。
这个 bug 不是评审看出来的,是喂一份没见过的数据跑出来的。 上一次也是, 上上次还是。
一句说错了的话,以及它错在哪
导入刚做完时,README 上写着一句:「救不回被豆瓣删掉的作品名。实测 0/8。」
档案里有 8 个「墓碑」——最后一次看到时,豆瓣连名字都不给了,只剩一个占位符。 当时看了其中一个(《返校》),发现它在 2022-12-25 的广播卡片上就已经是「未知条目」, 于是写下了 0/8。
那句话是从 n=1 推出来的,而它是错的。回头对着合并后的档案重新数:
movie/11611021 在这世界的角落 / この世界の片隅に
game/24299254 瘟疫公司 Plague Inc.
game/27054820 瘟疫公司:物竞天择 Plague Inc: Evolved
3 个救回来了。老档案在豆瓣删掉它们之前看见过。 《返校》那一条仍然成立——它被删得更早——但「0/8」不成立。
拿小样本推一个封闭结论, 是这个项目重复得最多的一个错误。相应的测试也改了:断言写成下界 (「至少救回 3 个」),因为墓碑的意思是「变成了无名」, 写成恒等会让「又多救回一个名字」表现成测试失败。
做不到的,写在这里
- 一张图都没有。老工具只存 HTML。摄取广播会让结构化数据引用 20 个没有字节的图片地址—— 这不是缺陷,是那个生产者根本没有图片这条路线,而且现在有一条测试专门断言这 20 个必须没有字节: 哪天有了,说明按成色做的那个划分已经不成立了。
- 没有日记 / 影评 / 豆列 / 相册——老工具没有这些路线。
- 未登录抓的那一整代丢掉,理由见上。
样张站已经按合并后的档案重新生成过:多了 5 个作品页, 一页都没有减少——那正是「合并即并集」这条性质该有的样子。