游戏评分成了访问者的:条目边界、缺席权威,与抓取中途的覆盖率
在公开或未登录归档中,游戏列表把访问者的「修改」按钮错当成了主人的评分;公开观测中缺失标签绝不能草率判定为用户删除了标签;而抓取中途的覆盖率面板,更不应因当前任务尚未收尾就扣下所有历史账号的成绩单。
随着多账号支持、主页输入以及未登录抢救机制在主线铺开,越来越多的真实边界被激荡了出来。
在纯粹的「单账号、本人登录态」时代,许多潜在的代码暗病被掩盖在「操作者恰好等于数据主人」的假象下。 一旦跨入「公开归档」与「并发状态共存」的真实场景,我们遭遇了三个极富启发性的技术隐患:
- 游戏标记的「李代桃僵」:为什么公开归档时,目标游戏评分竟然被读成了访问者的?
- 数据契约的严谨性:当公开页面缺失标签等字段时,为什么绝不能授予它「缺席权威」(Absence Authority)?
- 抓取中途看覆盖率:当前任务正在抓取尚未收尾,怎么能把所有已归档账号的成绩单全都扣下?
案发一:游戏标记的「李代桃僵」与结构解析陷阱
豆瓣各个媒介(书影音、游戏、舞台剧)的前端结构并非一套统一的标准体系,而是十余年历史地质层堆叠的结果。
书影音在列表页的星级评分,统一带有语义清晰的类名(如 class="rating4-t");
但游戏列表(douban.com/game/...)却沿用了一套完全独立的遗留结构:
- 条目主人的星级写在:
<div class="rating-info"><span class="rating-star allstar40"></span></div>; - 条目主人的短评在一个没有任何 class 的裸
<div>中; - 而在登录用户访问时,条目底部还会多出一行属于当前访问者的操作栏:
<div class="user-operation"><a data-rating="4">修改</a></div>。
早先解析器(extract.js)在提取游戏标记时,走了一条自以为聪明的捷径:
它直接去读 data-rating="(\d)",并用正则把「user-operation 前面那个 div」判定为短评。
当用户登录自己的账号备份自己时:
操作者就是主人,user-operation 里显示的就是自己的评分,解析结果碰巧完全正确,隐患被天衣无缝地掩盖了。
但当开启未登录归档或公开抓取时,灾难降临了:
1. 如果是纯匿名公开页面,页面根本没有 user-operation 这个操作栏!解析器找不到 data-rating,导致全部游戏评分直接报废清零;
2. 如果用户在浏览器里登录着小号,user-operation 展现的是小号访问者对该游戏的操作控件!解析器不仅没读到目标账号的真实评分,反而把小号访问者的打分甚至单字短评塞进了目标档案中——发生了荒诞的「李代桃僵」;
3. 末项越界:如果最后一项原本没有短评,正则一路往后贪婪扫描,甚至能把页面底部的访问者表单、推荐栏强行当成最后一项的内容补上。
在刚刚发布的 doubak-data-parser/0.15.1 中,我们彻底重构了游戏标记的抽取基石:
- 正文星级才是真身:严格从
class="rating-info"下的allstar([1-5])0提取目标主人的真实评分; - 裸 div 短评脱钩:直接提取
common-item内唯一的独立正文<div>,彻底废除对user-operation操作栏的依赖; - 条目闭合边界守卫(
gameItem):引入基于<div>嵌套深度的闭合截断,确保提取逻辑严格锁死在当前common-item内,绝不向后贪婪借取,不完整的页面宁可不解释。
案发二:公开观测的「缺席权威」(Absence Authority)
解决了解析器的字段提取之后,一个更深层次的数据哲学问题浮出了水面: 在公开页面上「没有看到 tags 标签」,到底意味着「用户没打标签」,还是「豆瓣没展示标签」?
答案显而易见:豆瓣出于反爬与界面简洁考量,在外部公开列表页上直接裁剪掉了用户的个人私密标签(tags)与备忘信息。
在规范体系(doubak-data-specs #5)中,我们必须界定这种「视界缺失」的权威性:
| 观测类型 | 适用场景 | 缺席权威(Absence Authority) | 合并(Merge)策略 |
|---|---|---|---|
| 本人登录态观测 | 用户登录目标账号本人抓取 | full(完全权威) |
页面无 tags,确证为用户已删除或从未添加 tags,下游覆盖同步清空。 |
| 公开/未登录观测 | 抢救被封账号、归档他人主页 | none(无缺席权威) |
页面无 tags,视为「不可观测」,绝不能覆盖抹除历史上由登录态记录下的私密标签。 |
如果合并引擎(mergeRecords)不加区分,一次被封账号的公开紧急抢救,就会成为一块残酷的橡皮擦,
把用户多年来在登录态下沉淀的私密标签全部洗劫一空。
明确了 absence_authority: 'none' 后,公开归档即便在不完整的视野下推进,也能确保既有高权限档案的数据安全。
案发三:抓取中途看覆盖率:不能因当前未收尾,就扣押全部账号的成绩单
在扩展层面,随着大家开始尝试备份多个不同账号,有用户提出了一个看似微小却令人窒息的体验问题:
「后台开着一个账号的抓取,我随手切到『覆盖率』Tab 想看看之前备份完的其他账号的覆盖率。 结果页面直接显示『备份来源待确认 / 暂无已收尾的档案』,而且账号下拉框直接不见了! 连我早就已经备份好的账号的覆盖率也全看不了了?这符合设计吗?」
这当然不符合设计,而是一个典型的实现层短路:
面板在加载覆盖率时,默认读取 OPFS 磁盘上按时间戳排序的最新目录。
而当前正在进行的抓取任务,恰好就是磁盘上最新的目录!
但这个任务正在热跑中,只存在实时增量的 checkpoint.json 与 WARC 数据块,
根本还没有经过最终的双向对账,没有生成 manifest.json
(覆盖率是收尾阶段生成的权威连续性证据)。
旧的覆盖率前端代码一旦发现最新的目录 hasManifest: false,便仓促判定「整台机器没有任何有效档案」,
隐藏了账号切换下拉框,直接给用户贴上了「暂无已收尾档案」的冷板凳。
在刚刚合并入主线的 PR #24 中,我们将覆盖率展现层做了彻底解耦:
- 收尾驱动的选择器:账号与来源选择框只由已收尾且具备 manifest 的有效档案驱动(
items.filter(e => e.manifest)),未收尾的进行中目录不参与扰动选择项; - 随时自由审查历史:无论当前后台是否正在热火朝天地抓取,用户随时可以切换到任何历史账号,完整查阅其覆盖率与归档链;
- 进行中状态友好告知:如果用户选中的恰好是当前正在抓取的账号,面板给出明确提示: 「本次抓取正在进行中……覆盖率基于上一份已收尾档案计算」,若全盘仅有这一份未收尾档案,则提示「本次抓取尚未收尾——覆盖率证据在收尾时生成,目前尚不可用」。
从解析器的字符边界,到规范层的观测权威,再到面板上的多任务状态共存—— 每一次深水区的交汇,都在让豆备离真正的稳健与可靠更近一步。