豆备 DOUBAK
← 开发日志

叫「MewX的豆瓣存档」,以及多账号公开归档

不只是用户名与 UID:从数据规范、解析器到站点生成器,全链路沉淀用户昵称;支持账号封禁或未登录下的公开主页紧急抢救,并确立全局单任务互斥与凭据隔离底线。

在过去几版的豆备中,生成的离线静态站点首页顶部赫然印着一行大字: 154611037 的豆瓣存档(或者一个冷冰冰的拼音用户名 slug)。 对人来说,这是令人出戏的:我们在网络空间里被朋友呼唤、在书影音讨论区被认出的名字, 从来都不是那一串阿拉伯数字或 URL 路径,而是我们亲手填写的昵称。

「能不能直接叫『MewX的豆瓣存档』?」 一句看似简单的直觉需求,落到多仓库、跨链路的体系里,其实是一次系统性的演进。

三位一体的身份体系:UID、用户名与昵称

豆瓣的账号标识长期存在三种形态,各自承担完全不同的职责:

标识形态特点在豆备中的职责
数字 UID 154611037 全局唯一、终身不可变 档案链(chain)、增量比对与去重的唯一权威锚点。
用户名(Slug) mewx 可自定义、可注销再占用 构成个人主页 URL,用于定位与翻页入口路由。
昵称(Nickname) MewX 用户展示名、富文本、可频繁修改 人机可读标识;站点标题、NDJSON 导出与归档人称呼。

早先为了追求确定性,数据格式与解析器将数字 UID 奉为唯一真神,而丢弃了展示昵称。 现在我们为它补齐了完整的生命周期:

  • 规范先行(doubak-data-specs #5): 在 Bundle v1 规范与 Canonical NDJSON 规范中,正式加入 account.nickname 字段; 明确昵称为 string | null,空值不假造。
  • 抽取与增量合一(doubak-data-parser #8): 从主页 #db-usr-profile 区域的 <h1> 文本或头像 alt 中稳定抽取昵称, 经过单趟 HTML 实体解码还原。在增量合并(mergeRecords)时,按数字 UID 将最新确认的昵称向下游稳定继承。
  • 站点动态标题(doubak-site-generator #5): 站点生成器按级联规则确定站点名:优先取 --site-title 显式指定值, 其次取档案中的 <nickname>的豆瓣存档,无昵称时优雅回退为 <username>的豆瓣存档, 最后兜底为「豆瓣存档」。同时在全站导航栏、页头与 HTML <title> 中保持一致。

公开归档的起点:账号被封禁时的抢救生命线

在豆备最初的路线规划里,公开归档甚至根本不在优先级清单上。

早在今年 8 月,就有用户在 GitHub 上提了 Issue #2(请求加入备份他人账号的功能), 希望能备份其他豆友公开的书影音记录。当时我们的回复很直截了当: 「设计之初没有考虑这个功能,可能短期内不会加。」 彼时豆备的定位非常单纯:全心服务于个人登录态,把属于你自己的全部数据(包括仅自己可见的私密日记、状态与私密标记)原汁原味搬回家。

但转折点很快到来——或者说,豆瓣生态的现实给我们结结实实提了个醒: 如果用户的账号被封禁、停用,或者因为各种原因无法登录了呢?

Issue #19(依然需要支持备份未登录的账号,例如自己账号被封禁) 的提出,彻底重塑了这个设计的优先级。 经历过账号停用或被封的豆友都知道,账号一旦被封,你便彻底失去了登录后台的可能;但在很长一段时间内, 个人主页上公开展示的读过、看过、书评和公开日记依然可以被外部直接访问。 如果豆备死守「必须登录目标账号才能启动备份」的假设,那么在用户最绝望、最需要抢救自己多年心血的至暗时刻, 工具反而成了摆设。

支持无需登录的公开归档,不再只是「帮好友做镜像」的锦上添花, 而是变成了一根抢救被封禁或无法登录账号的唯一生命线。

为此,我们在扩展面板中加入了统合的账号输入能力:不仅支持数字 UID,还支持直接粘贴完整的个人主页 URL (如 https://www.douban.com/people/mewx/)。扩展会自动探测主页、核实 UID 与昵称, 并为未登录账号或公开目标建立独立的档案集合。

凭证隔离:避免操作者会话污染

但在技术实现上,未登录与公开归档潜藏着一个极其隐蔽且危险的陷阱:操作者会话污染。

当你在日常使用的浏览器里登录着小号 A,去抢救被封的主号 B(或归档他人主页)时: 如果扩展发出的所有请求都自然带上浏览器里的登录 Cookie,豆瓣对某些作品详情页会返回小号 A 的「在读标记」、「短评打分」或仅好友可见内容。 最终产出的「B 的公开档案」,里面会悄悄混入「A 的个人私密数据」——这是一次灾难性的数据污染与隐私泄露。

为此,扩展引擎(doubak-extension)建立了操作者与目标账号的隔离契约: 在公开归档模式(visibility: 'public')下,请求作品目录、通用条目时主动剥离操作者的身份凭据, 并在 manifest 中显式声明 operator_user_id 与 visibility, 保证公开档案里只存真正的公开可见数据,绝不发生跨账号的身份串标。

铁律:绝不允许同时抓取多个账号

随着多账号支持的完善,有人问过:「既然我可以为不同的账号备份,那能不能开两个任务同时抓?」

答案在设计文档里被定为了不可逾越的绝对铁律:全局单抓取互斥,即使是不同账号也坚决不允许并发。

理由非常现实:

  1. 豆瓣的风控是以 IP、会话与设备特征综合衡量的。 即使在不同账号间切分任务,来自同一个浏览器实例的并发请求在豆瓣眼里就是翻倍的频率攻击。 自适应退避器(Pacer)好不容易维持的平稳节奏会被瞬间打乱。
  2. 风控关联风险。 如果账号 B 的抓取动作踩中了豆瓣的人机验证或软封锁,浏览器 Cookie 容器中的账号 A 极可能随之一同被标记甚至封号。保护用户的豆瓣资产安全,永远高于跑得快。

在代码实现上,全局互斥锁横跨 Service Worker 调度层、Offscreen 驱动层与面板 UI: 任何新抓取启动前,必须确认无任何处于活跃或未收尾状态的任务; 已有任务必须显式「收尾」或「中止」后,才允许开启下一个目标。

这一系列底层体系已全部合并进主线并在扩展中铺开。下一篇日志,我们将聊聊在实地抓取中踩到的另一个隐蔽坑: 为什么正常页面里的脚本源码会让抓取器如临大敌。

← 更新:脚本里的验证码不是风控:假阳性、降速与恢复原速 更早:断点恢复只记下了未完成的,漏掉了已经抓过的 →