重标一次,2018 年那条就没了
把一部电影删掉重标,站点上 2018 年的短评、标签、那条广播的链接和封面一起消失。而 git 里零新增、零删除——这种消失是看不见的。
一次很普通的增量抓取:59 个页面,全部正常,零缺口,26 条路线全都连续。 档案本身干净得没什么可说的。
重新生成一遍样张站,diff 里有 325 个页面变了。绝大多数是「最后一次看到」 那一行的时间戳——这次抓取又看见了它们,仅此而已。 真正有内容变化的只有一小撮,而其中一页是这样的:
movie/3541415.html 《盗梦空间》
- <span> · 2018-01-03</span>
+ <span> · 2026-09-04</span>
- 真好看啊,之前看了一下剧情解密才理解了这个结局。 //2015-08-26 …
+ 精彩,真的精彩,开放式结局竟有一丝丝哀伤,下周二去看奥德赛!
- <h2>说过什么</h2>
- <h3>2018-01-03 21:15:28</h3>
- <p><em>广播 · 看过 · ★★★★★</em></p>
- <p>真好看啊,之前看了一下剧情解密才理解了这个结局。 //2015-08-26 …</p>
换掉的那部分是对的:这部电影确实是这天重新标记的,短评和标签都换了新的。 但「说过什么」整栏没了。 那是 2018 年发出去的一条广播, 豆瓣自己早就不显示它了—— 这份档案里留着它,正是这个工具存在的理由。
同一时刻,2018 年那个月的广播页上也少了点东西:
broadcast/2018-01.html
- <a href="../movie/3541415.html"><img src="../covers/p513344864.jpg"…></a>
- 看过 <a href="../movie/3541415.html">盗梦空间 / Inception</a> ★★★★★
+ 看过 盗梦空间 Inception (2010) ★★★★★
链接没了,封面没了,剩下一行纯文字。
而 git 说:零新增,零删除。 页数一张不多一张不少。要不是那天正好在逐页读 diff, 这三处消失不会有任何东西提醒任何人。
两条标记是从哪来的
用户在豆瓣上把《盗梦空间》删掉,然后重新标了一次。 豆瓣的做法是发一个全新的条目编号:
1299196346 2018-01-03 ★5 「真好看啊…」 ← 上一次抓取还看得到
4937138397 2026-09-04 ★5 「精彩,真的精彩…」 ← 这次新出现的
解析器把它们记成两条记录。这是对的,而且是刻意的:
两个不同的上游编号就是两次不同的标记,
这恰好是豆瓣页面上那个 data-cid 唯一能看见、
而降级方案看不见的东西。档案层是一本事件日志,
「他 2018 年标过一次、2026 年又标了一次」是两件真实发生过的事,
必须都留着。
网页不能跟着分
生成站点的那一层不是事件日志,它是一张「当前是什么样」的缓存, 而一部电影只有一个网址。它对「两条标记」这件事一句规矩都没写,于是同时踩了两个坑, 两个都不出声:
- 两条标记算出同一个文件名,后写的把先写的整个盖掉。 谁活下来取决于它们在数组里的先后,不取决于任何判据。 页数没变,所以 git 上看不出来——2018 年的短评和标签就是这么没的。
- 「一个作品编号出现了两次」被当成了编号撞车, 于是拒绝把广播接回作品页。链接、封面、整栏时间线一起消失。
「撞车」那条规矩,本来问的是另一个问题
广播卡片上只有一个数字编号,没有说这是电影还是书。而不同类别的编号各排各的, 理论上会撞。撞了还硬接,页面上就会出现一条指向另一部作品的链接—— 那比不接严重得多:不接只是少个链接,接错了是档案在说假话,而且看不出来。 所以规矩是「撞了就不接」,这条完全正确。
问题出在它怎么判撞车:数一数这个编号出现了几次。 在「一个作品最多一条标记」的年头里,这个数法和「跨类别撞车」是一回事。 用户第一次删掉重标,两者就分了家—— 同一部电影的两条标记指向的是同一页, 哪来的歧义。
这是个替身判据:真正要问的是「类别一样吗」, 而代码里问的是「编号出现几次」。 两者曾经等价,所以没人发现替身是替身—— 这个项目已经在同一个形状上栽过几次了。
修法
在生成站点的入口按「类别 + 作品编号」把标记归一次组:
- 页头取「最后一次看到」的那条。 豆瓣现在还留着的,才是最近一次抓取里出现过的那条。 按标记日期挑是错的——补标一部老片可以填一个更早的日期, 而它在豆瓣上仍然是现存的那条。
- 被顶掉的那几条并进时间线。 于是 2018 年那次的短评、评分、日期都还在页面上,位置也对, 而不是假装从没发生过。
- 页脚那句「这条记录有 N 个版本」把它们都算上。 说「只有 1 个版本」而下面明明列着两次标记,是自相矛盾。
修完之后那一页是这样的:
<h2>说过什么</h2>
2026-09-04 17:37:44 广播 · 看过 · ★★★★★ 精彩,真的精彩…
2018-01-03 21:15:28 广播 · 看过 · ★★★★★ 真好看啊,之前看了一下剧情解密…
这条记录有 2 个版本
2018 年那个月的广播页则一个字节都没变——链接和封面回来了。
我加的那道守卫是废的,突变测试当场抓到
顺手把「撞车」的判据也从「出现几次」改成了「类别一样吗」,看起来更对。 照惯例做突变测试:把它改回旧写法,看有没有测试变红。
一条都没红。
原因很简单:归组已经保证了每个「类别 + 编号」只剩一条, 到了那一步还能撞上的,只可能是跨类别—— 我新加的那句比较永远为真。 它不防任何东西,只是读起来像有人在看着。 删掉, 换成一行注释把这个上游保证写清楚。
但同一次测试也告诉我:另一处判据是真的要改。 广播往作品页里送时间线走的是另一条路,它读的是原始档案, 确实会看见两条标记。这两个方向必须分开测—— 一边接得回、另一边接不回的话,广播卡片上有链接、作品页上却没有那段历史, 两边对起来还是「平」的,谁都看不出是谁错了。
测的判据是秒:标记只精确到天,只有广播带秒。 那一行要是退化成「天」,就说明广播根本没接进来。
它是怎么被发现的
不是靠评审,也不是靠任何一条报警。是把一份没见过的新数据 喂进去跑一遍,然后一页一页读 diff。
这已经是第三回了: 解析器那两个 bug、 放错目录的那四份档案, 都是换一份数据跑出来的,不是看代码看出来的。 而这一次尤其说明问题——它没有任何征兆: 不报错,不告警,页数不变,零新增零删除。 唯一的痕迹是某一页里少了三行。
所以「重新生成之后要数一数有没有页面被删掉」这个习惯, 这次没有救到我们:一页都没被删。真正救回来的是逐页读内容。
顺带:首页的广播预览从 2 条变成 5 条
改的时候正好在看首页。广播是首页上唯一能看见原话的地方—— 作品那几栏是一排封面,日记和影评是标题加 80 字摘要。 两条撑不起「这份存档里有话」这件事。
它原来和长文共用一个数字,现在分成两个:长文一篇就占掉一屏, 给五篇等于把下面所有栏目推下去。共用一个常量,调其中一个必然连累另一个。