校验器一直都在,只是没人跑
规范仓库那个参考校验器覆盖得几乎全,成本也几乎为零。可 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, 对着同一份规范。这不是浪费, 自己跟自己对账永远是平的。