豆备 DOUBAK
← 开发日志

校验器一直都在,只是没人跑

规范仓库那个参考校验器覆盖得几乎全,成本也几乎为零。可 24 份真实档案里有 4 份永远报错——于是它一次都没被用起来。

「你怎么知道这份档案里的字节没坏?」这个问题,直到这一天为止, 这个项目的回答是「不知道」

尴尬的地方在于:工具早就有了。规范仓库里的 validate.py 从第一天起就在, 它查段文件的摘要、查索引的行数、查每一条捕获在不在它自称的位置上。 覆盖得几乎全,跑一次几秒钟,而它一次都没被用起来。

为什么没人跑

对着那 24 份真实档案跑一遍就知道了:

20 份通过
 4 份失败 —— 45 个错误

那 45 个错误全部是同一件事:七月的两个生产者 bug, 让三份档案在 8 条路线上写下了「这条线整份都枚举过了」, 而实际上只抓到 15 条里的一小部分。 那两个 bug 早就修了, 但档案是冻住的,那几句错话再也改不掉。

一个永远有内容的失败列表,就是一个没人看的失败列表。 这句话在这个项目里已经踩到第五次了。

所以缺的从来不是一个检查。缺的是让已有的检查变得能看。 错误现在分成两类,各有各的退出码:

退出码意思下一步
完整性2 字节不是它自称的那些 重新拷一份 / 重新导一次。健康的档案这里永远是零。
合规性1 字节没问题,但某句声明不对 部分永远修不掉——档案是冻的

加一个 --integrity-only,就只拿第一类当门槛。 != 0 仍然是「有问题」,所以已经在用它的地方一行都不用改。

然后是解析器这一侧:默认就查

解析器多了一个 bin/verify.js,而且 bin/parse.js 默认就会先跑它

「默认开」这件事是被上一段自己教会的:一个要人主动去跑的完整性检查等于没有。 默认关着的开关,是把「要不要相信这些字节」这个问题,推给一个此刻根本不知道有这回事的人。 代价是那份 619 MB 的档案上多 8.3 秒(基线解析 16.3 秒)。

唯一的绕开方式是 --no-verify,而它刻意没有并进 --ignore-warnings。后者管的是「混了两个账号还要不要继续」,那是个致命条件的闸门; 而完整性发现根本不致命——坏的那几条排除掉,其余照常摄取。 一个开关管两件性质不同的事,用它的人就不知道自己关掉了什么。

动手前先量一量,两条设想当场被否掉

写这个功能之前列的方案里,有两句话听起来都很有道理,而两句都是错的。

①「最要紧的检查是 content_sha256

错。两个生产者都是把即将写出去的那段缓冲区拿去做摘要—— 也就是说这个字段只有在记账出错时才可能和正文对不上。 而「记账出错」早就被另一条检查精确地挡住了:索引这一行声称的记录编号, 必须真的出现在它指向的那段字节里。

模拟一个像模像样的生产者 bug——把每条记录的偏移量整体前移一格, 再把清单的摘要改对、让整份档案自洽——单靠那一条检查就报出 42 个错误。 摘要一个都不多报。

这一条之所以关键,是因为它是索引与字节之间唯一的交叉引用。 偏移错位一格时,gzip 解得开、CRC 也过、取出来还是一个合法的豆瓣页面——除了它,谁也看不出来指错了地方。 实测把一份真实档案错位一格再解析,解析器只报了 status_mismatch: 93, 读起来像是抽取器坏了。

不核字节的代价不是「没人发现」,是「发现的人会去查错的地方」。

content_sha256 还是查——那一段解压出来了,顺手的事—— 但它守的是一个很窄的情形:生产者写下了合法的字节、合法的记录编号,却给出了另一份的摘要。

②「解析的时候正文已经在手上了,所以顺带摘一下几乎不要钱」

这句是这个项目自己的文档里写着的,而它是假的。量一下:

一份真实档案  23962 条捕获
解析器实际打开   15377 条
其中图片行         0 条   ← 一共 6134 条,一条都不读

assets-*.warc.gz 里翻三个字节,再解析一遍: 告警一模一样,条数一模一样,产出逐字节相同。

图片是盲区,而图片恰恰是这份档案里唯一没法重新算出来的东西。 坏消息要等到生成站点那一步才冒出来,而且是一句 zlib 的栈回溯,离起因隔着两个工具。

所以校验本来就只能是单独的一趟,那它也就该是一条单独的命令。 顺带还有个技术原因:解析器取正文的那个接口返回的是解码过的字符串, 而摘要是对字节算的——这份档案 26% 是 JPEG, 解码成 UTF-8 再编回去根本不是原来那串字节。

那 42 倍

解析器为了能在浏览器扩展里跑,自带了一份手写的 SHA-256(当时量的是「比内建的慢 4.2 倍」)。 拿来做完整性校验时重新量了一次:

node:crypto   2133 MB/s
自带的那份      50 MB/s      ← 42 倍,不是 4.2 倍

4.2 倍那个数是拿 8 万条短字符串量的,那种场景里每次调用的固定开销占大头。 换成 2.6 GB 连续正文,差距就是真实的算力差距。

所以做法和压缩那边一样:把摘要函数和解压函数当参数传进去。 命令行这边塞 node:crypto,扩展那边塞浏览器自带的—— 而 verify.js 本身仍然一个 node: 模块都不碰,照旧能被扩展逐字节抄走。

两个校验器,各答各的问题

validate.py 没有被替代,也不该被替代。它问的是「这份档案合不合规」verify.js 问的是「这份档案的字节是不是它自称的那些」

故意不重叠:schema 的形状、crawl_state 的不变量、覆盖率的可追溯性、 README、检查点,verify.js 一样都不碰—— 那些是规范那一侧的事,两边各写一份只会长出两套会互相走偏的说法。 而反过来,解析器这边需要一份自己的,也有两个量出来的理由:图片盲区(上面那条), 以及 validate.py 要 Python,而这条流水线其余每一环都是零依赖的 Node。

而且它们是两个人写的两份实现——一份 Python、一份 JavaScript, 对着同一份规范。这不是浪费, 自己跟自己对账永远是平的

← 更新:一个目录里十份档案,只读了一份 更早:接上 2022 年的老档案:多的不是记录,是二十个月的编辑史 →