豆备 DOUBAK
← 开发日志

档案是全的,声明是错的

抓取报「有 1 处缺口」,点了重试,抓完还是「有 1 处缺口」。 而那张图明明就在档案里,542163 字节。

一次真实的全量抓取,中途有一张广播配图连着三次超时。面板如实报了 「有 1 处缺口」,用户点了「重试」,抓取继续,最后跑完。 而收尾写下的档案里,那一行还是「有 1 处缺口」。

把导出的那份档案打开,两行就说完了:

manifest.json  crawl_state[asset.status_photo]
  contiguous: false
  gaps: [{ reason: "fetch_failed",
           detail: "…f042d8249db3650.jpg:重试 3 次仍失败(请求超时)" }]

index.ndjson
  #000716  同一个 URL  ·  verdict ok  ·  200  ·  image/jpeg
           542163 字节  ·  content_sha256 齐全

重试成功了。图就在 assets-…warc.gz 里,偏移量 27823442,字节、类型、摘要一样不缺。缺的只是有人去把那句话收回。

档案是全的,声明是错的。 而 bundle 一旦产出就是冻住的——这句错话再也改不了了。

为什么会这样

缺口是只增不减的。这不是疏忽,在绝大多数地方它都是对的:

  • 「抓到一半中途停下」——后面某一页抓成功了,并不能证明中间那段没有洞。
  • 「页面声称有 30 条,一个 ID 都没抽到」——那是抽取器坏了, 再抓成一页也说明不了什么。

这些缺口讲的都是一整段的事,后来的成功冲不掉它们。 但「这一页反复抓不下来」不一样:它是一句关于某一个 URL 的断言, 而同一个 URL 的一次成功捕获,恰好把它证伪——用的还是这份档案 自己的证据,不需要相信任何外部数字。

所以这一处、而且只有这一处,必须能被收回。

不能拿那句话去匹配

最省事的做法是拿 detail 里那句中文去搜 URL——它就在里面。 但 detail给人看的一句话,靠子串把 URL 抠出来, 判错的方向有两个,而两个都比「漏报」贵得多:

  • 抹掉一处真缺口。两条 URL 可以互为前缀, 一次不相干的成功就把真的洞盖上了——而那正是档案里最不能出的错。
  • 诬告一次真实的失败。反过来匹配上了不该匹配的, 等于说「你这处失败是假的」。

所以缺口现在多存一份结构化的 URLdetail 照旧给人读,url 给机器用来精确比对。 成功捕获时按完全相等去抹,只在本次抓取内生效。 抹掉缺口等于放宽完整性声明,判据必须严。 不带 URL 的那些缺口(中途停下、抽取器坏了)永远不会被碰到—— 它们说的本来就不是某一个 URL。

于是这成了一次格式修订

给缺口加一个字段,就是改档案格式。而 gap 这个对象在 schema 里写着 additionalProperties: false——多一个字段就是不合规。 规范自己的规矩很明确:新增可选字段 = 小版本,于是有了 bundle/1.4

顺手把这条判据也写进了校验器:一处带 URL 的 fetch_failed 缺口,如果同一份档案的索引里就有那个 URL 的 verdict: ok 捕获,报错。它跟规范里另一条老规矩是同一枚硬币的两面—— 豆瓣自己报的数字不能用来证明抓全了, 但拿来推翻一句离谱的声明是另一回事。授予要严,证伪可松; 何况这一次用来证伪的,是生产者自己写下的另一句话。

校验器故意不去解析老档案 detail 里的 URL。 1.3 及更早的缺口只有那句话,猜错的代价上面已经算过了——所以对它们,这条检查 干脆不适用。已经写下的档案仍然合法。

三件值得记下来的事

  • 早年那几次修的都是眼前那一半。 2026-07-30 修的是「一个抓不下来的页面会葬送九成档案,而且档案被静默标成 已完成」,从那时起才开始记缺口;一周后做的是「重试」这个按钮。 而「重试成功之后,那处缺口怎么办」从来没有人写过—— 两个功能各自都对,中间那道缝没有名字,也就没人去看。
  • 校验器跳过了一层,而它说得不够响。 写这个补丁时,产出的档案有一小段时间是不合规的(多了个 1.3 不认的字段), 而本机的校验器什么都没说——它先打了一句 「未安装 jsonschema,跳过 schema 层校验」,然后打了一句「通过」。 后面那两个字才是会被读进去的。
  • 这是「假完整性声明」的镜像。 这个项目早就记过一次相反方向的:几份档案声称某条路线「整份都枚举过了」, 而实际只抓到 15 条中的一小部分。那次是多报,这次是少报—— 而少报是安全的那个方向,所以反而没有任何东西拦它。

那份已经写下的档案呢

它照旧带着那句错话,冻住的就是冻住的——这正是这个项目对 bundle 的态度,也是它值得信的原因:档案不会被事后改写,哪怕改写的是我们自己 的一处口误。

代价是可以数清楚的,而且不大:

  • 覆盖率页上那一行永远写着「有 1 处缺口」;
  • 这条路线的水位线不推进,所以下次增量会重新走一遍它—— 而它本来每次都会走(配图是从广播页派生的),已经抓到的图会被跳过。

档案本身一张图都没少。 这也正是那条新校验的意义:往后再有人拿到这样一份档案, 校验器会当场告诉他「这句话别当真」,而不是让他照着一处并不存在的洞去重抓。

← 更新:v1.3.2,以及为什么格式版本跳到了 1.4 更早:响应头到了,不等于这次请求完了 →