Skip to content

About

豆备 (Doubak) 的浏览器插件,用于实际从豆瓣 (Douban.com) 下载网页,整理数据,导出等。整个项目的所有功能都可以在这个插件里找到入口。

Resources

Stars

29 stars

Watchers

1 watching

Forks

Repository files navigation

doubak-extension

中文 · English README

test Coverage Status

本仓库为项目源码。 如需了解功能概览与详细介绍,请访问 https://doubak.com; 如需查看生成的静态站点演示,请访问 https://sample.doubak.com。

豆备(Doubak)的浏览器扩展,提供一站式的豆瓣数据备份、解析、静态站点构建与外部格式导出功能。

扩展在用户本地浏览器中以本机的网络环境与安全速率抓取豆瓣,将网页原样保存为标准 WARC 档案格式。用户的登录凭据与会话 Cookie 始终保留在本地设备上,不经过任何外部服务器。核心设计目标是:即便在完全离线或豆瓣服务不可用的情况下,扩展依然能完全基于本地已捕获的数据,产出一份独立完整且可浏览的归档。

抓取完成后无需额外安装任何环境或工具:「导出」界面可即时将归档计算转换为结构化数据、NeoDB 导入包,或包含完整图片的 Markdown 静态站点。底层核心复用了下游三个仓库的代码逻辑(src/vendor/,保持代码一致并通过 CI 校验同步),确保扩展生成的归档结果与命令行工具完全一致。

v1.0 版本特性:全量与增量抓取均已基于真实豆瓣数据验证通过,完整支持个人自建相册(相册列表、相册内容与全尺寸原图)、豆瓣小组(小组主页、加入的小组、发起的讨论与正文附图)、自定义上传图片、作品封面、长文正文、豆列、数据导出与回传导入。针对移动应用(app),豆瓣官方已完全废弃该业务分类;针对相册,上传至作品相册的照片属于公共条目资产不予收录。

多账号与公开归档

Firefox 打包、账号切换及导入导出实测见兼容性核对。

设计逐项覆盖与尚未实现的部分见实现核对记录。

概览页留空即可备份当前登录账号;输入豆瓣主页 HTTPS 网址、用户名或数字 UID 后点击 「查看账号」,核对昵称与可备份范围后再开始抓取。输入本人 UID 时自动使用本人视图。本地账号气泡 按数字 UID 选择目标;输入框预填主页网址前缀,仅保留前缀或清空均表示备份已登录账号。 仅检测到的登录账号标注「自己的账号」;探测只作预告,开始与继续均会实时核对身份。 数字主页即使不重定向,也会从目标头像的主页链接识别域名,沿用同 UID、域名与视角的 增量基准;已有数字地址断点继续使用原地址。广播正文引用「页面不存在」不会视为 整页 404,topic 动态的独立标题也会保留到 canonical、Markdown 和站点搜索。

公开模式默认跳过作品详情页,可选择保留列表封面;若开启详情页,目录页面和封面请求初始不携带登录凭据、 不复用浏览器缓存,间隔至少 5 秒,防止把操作者的“我的评价”收进目标的作品目录。 若遇到算力验证(PoW),会自动求解并换取全站通行凭据(dbsawcv1)继续抓取;作品详情解析器被严格限定在 #info 范围内仅提取客观元数据,不会摄取操作者的个人评价。 求解过程可随时取消,默认最多 30 秒或 200 万次尝试。 超出预算或仍需验证码时暂停等待人工处理;提交返回限流或错误时保留该响应,不盲目重发。 按路线核对与真实 bundle 证据。匿名访客基础间隔 5 秒,登录操作者 3 秒,仍保留抖动、退避与人工处理风控。公开模式不抓小组发帖及其正文;广播隐私截断页 停止翻页并记录范围缺口。目标不存在、身份取证冲突或无法识别的停用主页会拒绝开工, 不会用用户名代替数字 UID,也不会产出空档案。

豆瓣的「该用户账号状态异常」专用错误页会明确说明目标账号异常,拒绝开工或暂停 正在抓取的任务,不自动重试;恢复前重新检查主页。正常正文或脚本引用这句话不会 触发该判定,异常页上的登录入口也不会被误当成操作者退出登录。

同时只允许一项抓取,包括暂停的任务。恢复始终绑定原目标 UID、域名、视角、操作者、 路线范围、下界与上游;切换登录账号不能把 A 的断点续进 B 的档案。同 UID 改域名时, 应中止旧任务,再以数字 UID 探测并建立新的全量基准。旧断点只从已存主页补身份,取证 不足时拒绝恢复;中止可解除任务占用并保留原始捕获。

覆盖率按账号展示,记录旁标注「本人登录时备份」或「公开页面备份」。同一账号有多种备份来源时,默认全部展示,也可按来源筛选;各来源的连续性分别计算。公开增量基准仍按操作者 UID 隔离,档案页提供账号筛选(默认全部账号),按 UID 分组;未收尾但有身份凭据的档案归入对应账号,其余保留在「归属待确认」。 导出页始终显示账号,单账号自动选中;多账号派生导出必须先显式选择 一个账号。单账号库保留原目录与 ZIP 名(如 doubak-markdown/、doubak-markdown.zip); 多账号库导出时追加所选 UID(如 doubak-markdown-154611037/、同名 ZIP)。内部文件名保持原格式, 未知归属不混入多账号派生数据。混合账号文件夹可 批量导入,仍逐份检查重复编号、摘要与回滚,原始 WARC 字节保持不变。OPFS 沿用已有 bundle 目录布局,以 manifest/断点的 UID 归属分组,不迁移或重写已有档案。

公开观测的 absence_authority 恒为 none;匿名页面未展示的标签不会清空历史标签。 登录状态提示有 5 分钟会话缓存,并在登录 Cookie 变化或会话守卫失败时失效;概览的 2 秒状态轮询不重复请求豆瓣会话。

目标账号头像随正式主页抓取归档;每轮重新下载,恢复时保留原目标与父捕获。 Markdown 导出含本地头像与站点资料;Hugo 生成 favicon 和页眉图,缺图时保留文字站名。 旧档案仍可导出,未保存的头像不会联网补抓。见头像设计与实现。 文档核对记录列出本轮校准范围与能力边界。

文档

  • DESIGN.md —— 功能规划、抓取边界模型与三份实测附录
  • docs/toolchain.md —— 工具链技术选型与背景考量
  • docs/ui.md —— 界面交互设计:信息反馈、准确进度呈现与反虚假设计
  • docs/permissions.md —— 权限审计:权限声明、最小必要原则以及各执行上下文的 API 映射表
  • docs/release.md —— 发布流程与版本号管理的注意事项
  • 档案格式规范定义详见独立仓库:doubak-data-specs

开发

项目遵循零依赖、零编译构建步骤的设计。仓库中的源码即为浏览器运行时直接执行的代码。

npm test                                       # 运行测试套件(使用 Node 内置 test runner,无需 npm install)
node tools/package.mjs                         # 构建 Chrome / Edge 压缩包 → dist/doubak-<版本>.zip
node tools/package.mjs --firefox               # 构建 AMO 专用压缩包 → dist/doubak-<版本>-firefox.zip
node tools/package.mjs --list                  # 仅查看包含的文件清单
node tools/package.mjs --stage dist/unpacked   # 生成可直接在浏览器中加载的解包目录
node tools/make-manifest.mjs                   # 重新生成 manifest.firefox.json(--check 参数仅作比对校验)

需要 Node ≥ 20。

两个安装包,共享同一份文件清单。 两者的差异仅存在于 tools/package.mjs 中 TARGETS 配置项所定义的三项属性:使用的 manifest 配置文件、生成包的文件名后缀,以及在 Firefox 中无需包含的 offscreen.html。host-offscreen.js 是共享宿主入口的静态依赖,两个包均须包含;Firefox 不会调用其中的 Chrome API。其中 manifest.firefox.json 是由构建脚本自动生成的衍生文件,禁止手动编辑(CI 与单元测试会校验一致性并阻断不一致的修改)。这种独立生成的策略是为了防止 Firefox 意外继承 Chrome 独占的全新权限,否则可能出现扩展能够安装成功但部分功能静默失效的隐蔽缺陷。

在浏览器中安装加载

有四种安装途径,最终运行的均为同一套扩展功能。Firefox 需使用专用的安装包(即 -firefox 后缀的打包文件)。由于两个平台的 manifest 配置不同,若在 Firefox 中加载了 Chrome 版本的清单文件,浏览器会直接报错中断,且报错信息不会显式指出安装包不匹配。

方式一:从官方应用商店安装(推荐普通用户使用,支持后续自动更新):

平台 商店地址
Chrome / Edge https://chromewebstore.google.com/detail/hilmaopahndgbiolohgefnbeedobpafe
Firefox https://addons.mozilla.org/firefox/addon/doubak/(需要 Firefox 140 及以上版本)

Edge 浏览器同样可直接访问 Chrome 应用商店进行安装。上述两个商店链接均仅保留永久有效的固定路径:Chromium 链接仅保留唯一的扩展 ID 而省略扩展名称(避免未来重命名导致旧链接失效);AMO 链接则去除了语言前缀(未指定语言时,AMO 会通过 301 重定向将访客引导至其浏览器匹配的语言页面,避免将中文用户硬性引导至 /en-US/ 英文页面;扩展的 _locales 目录下已预备了简体中文与繁体中文两套本地化文案)。

方式二:通过 Release 安装包离线加载(无需访问商店):

从 Releases 页面下载对应 zip 文件,先解压到本地目录,随后根据浏览器类型进行加载:

平台 安装包名称 加载方式
Chrome / Edge doubak-<版本>.zip 打开 chrome://extensions → 开启右上角「开发者模式」 → 点击「加载已解压的扩展程序」 → 选择解压后的目录
Firefox doubak-<版本>-firefox.zip 打开 about:debugging#/runtime/this-firefox → 点击「临时载入附加组件…」 → 选择解压目录中的 manifest.json

Chrome 版本与 Web Store 上的发布包完全相同:由同一套 CI 工作流编译、时间戳统一归零处理,二进制逐字节完全一致。 Firefox 的「临时载入」属于开发者调试机制,重启浏览器后扩展会被卸载,下次启动需重新载入;但存储在 OPFS 中的已归档数据会持久保留,重新载入后依然支持无缝增量抓取。该途径主要用于验证尚未发布的特性,日常使用建议优先选择商店安装。

方式三:直接基于源码仓库加载:

在 Chrome / Edge 中,可直接点击「加载已解压的扩展程序」并选择本仓库根目录。由于项目不设任何预编译步骤,源码可直接在浏览器环境中运行。此方式的副作用是解压环境中会包含 test/ 与 docs/ 等非运行时文件。 Firefox 无法直接从源码根目录加载:因为仓库根目录的 manifest.json 为 Chrome 规范,Firefox 遇到 service_worker 后会直接拒绝加载:

Could not install add-on: background.service_worker is currently disabled.
Add background.scripts.

在 Firefox 中需先执行 node tools/package.mjs --firefox --stage dist/unpacked-firefox 产出解包目录,再载入该目录下的 manifest.json。

方式四:从 GitHub Actions 下载主分支最新构建:

进入 Actions → package 工作流页面,在最近的构建记录中下载产物 doubak-<版本>-unpacked(Firefox 对应 -firefox-unpacked),解压后按上述方式加载。该构建产物仅包含运行时必需的文件。注意:从 GitHub Actions 下载产物需要登录 GitHub 账号(未登录状态下会返回 401),因此对外用户指引统一推荐从 Releases 页面获取。

完整的发版流程与规范参见 docs/release.md。

浏览器环境自检

部分底层 API 在纯 Node.js 测试环境中无法模拟,例如 OPFS 实际读写、持久化存储权限以及浏览器配额机制。扩展加载完成后,可直接在浏览器地址栏访问:

chrome-extension://<扩展ID>/selftest/index.html

点击「开始自检」,测试程序将在真实浏览器环境中运行:

  • FileStore 契约验证(复用 Node 内存实现的同一套断言),分别执行两轮:一轮直接读写 OPFS,另一轮通过 Worker RPC 代理通道进行(与生产抓取调用路径完全一致);
  • IndexedDB KvStore 契约:覆盖实际浏览器的 IndexedDB 读写逻辑;
  • Bundle 完整写入与崩溃恢复演练;
  • 运行环境探测:包含持久化存储授予状态、可用配额与磁盘写入吞吐量测试。

测试结束后可点击「复制报告」导出纯文本结果,报告中的每项结果均以 [PASS] / [FAIL] 前缀标注,任何异常项均会在开头显著汇总。

扩展面板无头截图工具

由于扩展面板完全由前端 JS 动态渲染构建,直接读取静态 panel.html 无法呈现真实界面。项目提供了基于无头 Firefox 的测量渲染工具,无需安装扩展即可直接执行并截取面板页面:

node tools/panel-shot.mjs formats --out=/tmp/export.png
node tools/panel-shot.mjs overview --state=crawling      # 模拟抓取中 / paused / failures 等运行状态
node tools/panel-shot.mjs coverage --state=coverage-sources --bundles=4 --wait=15000 # 同账号多来源
node tools/panel-shot.mjs archive --bundles=14           # 模拟向 OPFS 中写入 14 份测试归档
node tools/panel-shot.mjs help --height=5200 --theme=light
node tools/panel-shot.mjs overview --measure             # 仅测量并输出各个页面的渲染高度

该工具依赖本地 Firefox 环境(可通过环境变量 FIREFOX=... 指定二进制路径)。此脚本属于辅助开发工具,不属于基础测试套件,本地未安装 Firefox 不会影响 npm test 的通过。

在界面迭代过程中,该工具用于精确度量页面排版效果(例如导出页卡片紧凑化调整、帮助页面在长篇幅下引入目录导航等布局优化评估)。 为了防止 Mock 后台实现疏漏导致界面渲染误报,test/panel-shot.test.js 会自动化校验面板支持的所有消息契约均具备完整的对应处理器。

移动端浏览器的桌面版 User-Agent 适配

在 Android 版 Chromium 浏览器(如 Edge Android、Kiwi 等)中,豆瓣服务端会根据请求 Header 中的移动端特征,将各类网页强制重定向至 m.douban.com:

www.douban.com/people/<u>/            → m.douban.com/people/<u>/
movie.douban.com/people/<u>/collect   → m.douban.com/people/<u>/movie/done
book.douban.com/subject/4820710/      → m.douban.com/book/subject/4820710/

而移动端页面(约 12.5 KB 的轻量外壳)完全缺失了登录标识与用户数字 ID,导致抓取引擎无法判断会话登录状态(曾导致此前报错信息含糊未指明根本原因,详见 #12)。

因此,扩展在此类移动浏览器环境中会注册一条 declarativeNetRequest 动态规则,将发往豆瓣域名的请求中的移动端 User-Agent 标记安全移除。该处理机制遵循四项严格的边界原则:

  • 最小修改原则,绝不伪造系统指纹:Android Chromium 在移除 Mobile 标识后,其 User-Agent 正好等同于该浏览器在 Android 平板设备上的默认 UA —— 保持了操作系统、浏览器内核及 TLS 网络指纹的完全一致。数据规范明确禁止虚构客户端指纹(伪造的 UA 与真实网络指纹不符极易触发风控),随意拼凑如 (X11; Linux x86_64) 会直接破坏指纹一致性。对于无法安全拆解的格式(例如 iOS 包含具体版本号的 Mobile/15E148 字符串),系统保持不作修改,宁缺毋滥。
  • 暂不支持 Android 版 Firefox 的技术原因:Android 版 Firefox 的移动标识(Mobile; 与 Tablet;)均会被豆瓣服务端重定向至移动版,若要获取桌面版页面必须完全伪装成桌面系统 UA,这直接违反了指纹一致性安全原则。因此该方案在 Firefox Android 端不成立,保留由 #11 在后续真机环境中继续探索。
  • 仅拦截修改扩展自身发起的请求(指定 tabIds: [-1]):用户在常规标签页中正常浏览豆瓣时依然维持移动版体验。这也是浏览器自带的“请求桌面版网站”功能无法奏效的原因(该开关按特定标签页生效,而后台抓取任务运行于独立的离屏文档中)。
  • 桌面端浏览器完全不注册该 User-Agent 规则:发出的 User-Agent 与原始浏览器值逐字节一致。

生成的归档会在 manifest.notes 中如实记录此项适配;producer.user_agent 中记录的依然是浏览器真实的 User-Agent。在调试页面的「环境自检」面板中会完整列出这两项 UA 以供比对核验。

此项适配仅解决网页模板结构的一致性,确保解析器与抽取规则能够准确命中。移动设备在长时间后台执行离屏文档抓取时的系统调度与资源限制,仍有待真机长周期测试验证。

界面

点击浏览器工具栏图标即可打开全屏管理面板(若已开启则自动聚焦激活对应标签页):

标签 内容
概览 各爬取路线的执行进度、捕获失败页面清单(支持重试或确认终止收尾)、抓取前环境与登录状态预检
覆盖率 支持归档链全量聚合与单份归档独立统计两个视图。默认采用聚合视图 —— 在多次增量后,归档完整性体现为整条版本链的累积状态
档案 集中管理所有归档生命周期:导入、导出、校验完整性、删除与磁盘配额分析;支持捕获记录预览与跨链版本历史比对。选中特定归档后提供两种分析视图:原始捕获记录(查看请求 URL、校验判定与响应原始字节)与解析内容结构(提取为结构化条目,支持浏览短评、广播及豆列评语等用户内容)
日志 本地实时记录网络请求 URL、重试行为、中断原因与错误栈信息,支持一键复制导出
调试 提供离线模拟抓取演练(零外部请求)、小批量样本抽样测试及浏览器环境能力检测

现状

核心功能实现状态

模块 状态
脚手架、工具链、基础类型 ✅
bundle 写入器(分段轮转 / index / manifest) ✅ 产物符合规范校验器要求
崩溃恢复 ✅ 在真实 OPFS 环境中通过校验
OPFS 存储后端 + Worker RPC ✅ 与内存模拟实现共用契约测试
会话守卫、分类器、路线注册表、frontier ✅ 分类器经过 6341 个真实页面校准验证
抓取引擎、主循环、工作流编排 ✅ 在真实豆瓣站点上验证通过
offscreen 架构 ✅ 实际抓取在 offscreen document 中执行,service worker 仅负责生命周期调度
生命周期自恢复(心跳、崩溃哨兵机制) ✅
抓取状态持久化(基于 IndexedDB) ✅
增量抓取 ✅ 时间水位线沿归档链按路线回溯(数据规范 §5.5);遇到存在缺口的路线会自动降级重爬;已成功捕获的作品详情、长文正文与多媒体资源均自动跳过
完整独立管理面板 ✅ 替代原有弹出层界面,提供单页管理面板(参考 docs/ui.md §1.1)
bundle 读取器与数据校验 ✅
导出模块(基于 File System Access API)+ 回读一致性校验 ✅
导入恢复功能(还原已导出的归档) ✅ 严格基于 bundle_id 匹配而不是依赖目录名称;写入前先提供变更预览;自动识别并拦截数据残缺与 ID 冲突;混合账号按 UID 分组导入
存储配额管理(检索 / 清理 / 配额统计) ✅ 整合进档案视图,统一维护
「导出」页面多格式转换(canonical / NeoDB / Markdown) ✅ 统一数据流解析,中间派生数据不落盘,生成后直接写入目标目录
系统通知与扩展角标状态联动 ✅

路线覆盖

路线 状态
广播 ✅
标记列表(书 / 影视 / 音乐 / 游戏 / 舞台剧) ✅
作品详情页 ✅ 基于列表页动态派生,受广播门控规则约束
日记 / 影评书评 ✅ 列表页仅包含截断摘要,正文需独立抓取;支持两类日记 URL 模式
用户上传的图片 ✅ 广播图片与日记正文配图,仅归档用户本人上传的原图
作品封面图 ✅ 随作品详情页自动抓取,独立存储于资产分段
相册(个人相册) ✅ 个人主页自建相册、分页照片列表与全尺寸原图
相册(作品公共相册) ❌ 不收录:上传至影视/游戏等公共相册的照片归属于条目公共图库,不纳入个人档案(相关上传广播动态仍会如实归档)
豆列 ✅ 仅归档用户创建的豆列(doulists/all)及其内容;重点保留用户在条目上填写的评语(实测样本 134 个条目中 62 条包含个人批注)
豆瓣小组 ✅ 发起的主题、正文附图与加入的小组;他人回帖与社区外围不收录,回复的讨论暂缓
移动应用(app) ❌ 不支持:豆瓣官方已完全下线移动应用业务(列表页虽可访问,作品详情页均返回 404;2026-08-14 验证)。用户的历史 app 标记记录仍会随广播内容一并归档

已移除的规划项

项目 说明
移动应用(app) 已从待办事项中移除 —— 并非功能未实现,而是上游服务已彻底下线,详见上方路线覆盖说明

归档边界与历史决策记录

关于相册与小组的边界判定:

  • 相册划分:严格区分“个人相册”与“作品相册”。上传至影视/游戏等作品相册的照片属于对公共条目的内容贡献,其生命周期与可见性由公共条目维护,属于条目目录数据(Catalog Data),照片实体不纳入个人档案(“上传动作”作为广播动态如实记入档案);而用户在个人主页创建的个人独立相册(Personal UGC)属于个人数字资产,已正式列入开发计划,待收集脱敏真实页面样本后排期实现。
  • 小组划分:豆备坚持个人 UGC 归档定位,严格排除无边界的社交外围(Social Periphery)。豆瓣小组社区广场、他人回帖及讨论串由于体量无限且涉及他人隐私,不予收录;用户本人发起的讨论主帖(publish)与加入的小组列表(joins)属于有界的个人资产,已确定纳入后续支持规划;在他人帖子下的回帖因定位复杂度高暂缓。

长广播正文关联机制: 针对广播列表中被截断的长文本,实测表明截断广播附带的“(全文)”链接实际均指向独立的日记页面,而相关日记已通过日记抓取路线完整收录于归档中。因此无需新增冗余的抓取路径,仅需在规范化数据(canonical)中准确标记 text_truncated 与 full_text_url 映射。数据设计原则要求:在为“缺失数据”设计额外抓取逻辑前,应优先确认该内容是否已存在于归档体系的其他链路中。

  • 图片归档与长文正文捕获:完整支持离线查看,彻底消除备份对远程图床的依赖;
  • 全量抓取压力验证:单次连续抓取 4 小时 16 分钟,成功捕获 5880 个资源对象,零异常失败;
  • 断点续导机制(F-08i):导出过程中断后再次触发,将自动检测已有文件并仅增量补齐缺失部分;
  • 双向归档导入恢复:支持在更换设备或清理浏览器存储后,将外部备份完整导入回系统并无缝继续执行增量抓取。

自动化测试覆盖:执行 npm test(零额外依赖,覆盖 90 个测试文件、1931 个测试用例)。在安装可选开发依赖后,还会额外调用 Webrecorder 的 warcio 对生成的 WARC 文件执行跨实现合规性校验;若本地同级目录存在 doubak-data-specs 仓库,将自动运行跨仓库一致性比对。

发布到应用商店

此处仅维护固定的商店主页链接,不记录暂态审核状态(详细提交元数据参见 docs/firefox.md)。

完整发布流程(更新版本号 → 推送 Git Tag → CI 自动触发构建 Release → 校验哈希值)参见 docs/release.md。打上 v* 版本的 Git Tag 后,GitHub Release 会自动生成并附加面向两个平台的 zip 分发包,无需手动重复打包。若需在本地构建分发包:

node tools/package.mjs            # dist/doubak-<版本>.zip
node tools/package.mjs --firefox  # dist/doubak-<版本>-firefox.zip

构建打包的核心规范与注意事项:

  • 打包采用显式白名单机制:包含清单定义于 tools/package.mjs 中的 INCLUDE 常量。黑名单机制一旦疏漏,极易在无感知状态下将敏感文件或开发调试代码打包入库;而白名单若有缺失,会在加载阶段立即可见。测试用例严格校验 test/、tools/ 与 docs/ 绝不进入分发包。
  • selftest/ 目录必须包含在打包列表中:该目录虽未在 manifest.json 中直接静态声明,但调试界面的自检功能会通过 chrome.runtime.getURL() 动态调用,遗漏将导致自检入口失效。
  • _locales/ 目录的完整性至关重要:manifest.json 通过 default_locale 声明了默认语言,若缺失该目录会导致 Chrome 直接拒绝加载扩展;CI 包含专用测试 test/locales.test.js 确保多语言目录结构与键名完整无缺。
  • 内置确定性 ZIP 打包实现:无需依赖系统的 zip 命令行工具,且统一将 ZIP 内文件时间戳置为 0,确保在相同源码下产出逐字节完全相同的二进制分发包(确定性构建),便于独立审计与哈希校验。
  • 版本号严格单调递增:修改 manifest.json 时需同步更新 package.json,测试套件会强制检查两者版本号的一致性。

应用商店提交信息规范

各权限项的声明依据与审计说明参见 docs/store-listing.md。隐私政策 URL 统一使用 https://doubak.com/privacy/。

应用商店的展示名称与简要说明由 manifest.json 中的 __MSG_extName__ 与 __MSG_extDescription__ 国际化占位符提供,实际文案统一维护于 _locales/<语言>/messages.json(当前支持 zh_CN、zh_TW 与 en)。应用商店仅对配置了对应 _locales/<语言> 的地区开放本地化页面。扩展管理面板本身仍仅提供简体中文;_locales 负责本地化扩展名称和说明等清单元数据,不覆盖面板界面。

抓完之后

数据产出与导出

抓取完成后,在扩展面板的「导出」页面可直接导出三种数据产物,无需额外安装 Node.js 环境或克隆相关仓库:

产出 格式说明 主要用途
结构化数据(canonical) 九个 NDJSON 文件,每行单条 JSON 使用 jq 等工具直接查询;同时作为后续转换流程的标准输入
NeoDB 导入包 标准 ZIP 压缩包 上传至 NeoDB「设置 → 数据 → 导入 NeoDB 备份」
Markdown 静态站点 包含 Front Matter 的 Markdown 目录树 + 本地图片资源 + 客户端搜索索引 交由 Hugo / Astro / Eleventy / Jekyll 等静态站点生成器渲染发布

三种格式均直接基于 WARC 归档实时流式解析生成:派生数据不预先落盘(避免冗余数据源导致状态不一致),直接写入用户指定的本地目录。生成效果预览可访问:sample.doubak.com。

新抓取将展示昵称写入 manifest.account.nickname,旧归档可缺省。导出账号选择器显示 昵称、个人域名与稳定 UID;同一 UID 的昵称取最新批次的非空值。Markdown 首页站名 优先为「<昵称>的豆瓣存档」,缺失时依次回退到个人域名、UID 与「我的豆瓣存档」。 标题逻辑与命令行生成器共用一份纯函数,使用的是被导出档案的身份资料。

如果需要自定义导出参数、对接自动化脚本或仅提取部分数据子集,亦可直接使用对应的命令行模块:

工具仓库 核心功能
doubak-data-parser bundle → canonical(结构化数据提取、版本修订历史追溯、纯离线运行)
doubak-site-generator canonical → Markdown + 本地图片 → 静态站点构建
doubak-export-adapters canonical → NeoDB / Letterboxd / Goodreads 格式导出适配

为何采用源码同步而非 Git Submodule

扩展仓库的 src/vendor/ 目录同步维护了下游三个核心仓库中 29 个纯函数模块的代码,并通过 tools/sync-vendor.mjs --check 结合 CI 自动化检测各模块与上游的一致性。

跨仓库协同遵循“上游优先”准则:CI 在构建和测试时基于各个兄弟仓库的主分支进行校验。若有跨仓库特性变更,必须先将核心逻辑合并至对应的上游专业仓库,随后再同步至扩展端并更新 vendor 清单,从而确保规范与底层定义先行。

未采用 Git Submodule 的主要考量:若开发者或构建环境执行 git clone 时未附加 --recursive 参数,Submodule 对应路径将表现为一个已存在但内容为空的空目录。在此情况下,打包脚本 collect() 无法将其识别为“缺失路径”,从而导致构建打包虽能成功完成,但产物中却静默缺失了关键的解析逻辑。采用白名单配合显式文件复制机制,能够有效阻断此类静默构建缺陷。

系统遵循明确的架构分层边界:“I/O 数据源因环境而异,数据解析与业务语义保持统一”。文件系统读写相关的模块(如 Node 环境下的 bundle-source.js、canonical.js 等)不直接同步,而是在扩展的 src/pipeline/ 中实现对应的 OPFS 异步存储接口适配。

抓取被打断了怎么办

抓取过程异常中断的处理方案

无需进行额外的人工干预,更切忌删除当前已生成的归档文件。

网络爬取可能因多种客观原因中断(例如遇到验证码风控、触发临时频控限流、登录状态失效、网络连接断开、用户手动暂停或直接关闭浏览器等),任何意外中断均不会导致已成功捕获的数据丢失。系统采用实时持久化机制,每个页面请求成功后会立即落盘写入 WARC 分段,因此在中断发生前捕获的全部内容均已安全封存。即便因异常断电导致文件末尾写入不完整,崩溃恢复机制(src/bundle/recovery.js)也会在下次启动时自动向后截断至最近的合法记录边界,并在后续流程中安全补抓该页面。

未完成收尾的归档(Incomplete Bundle)尚未生成最终的 manifest.json(该清单仅在收尾时写入)。在此状态下,账号统计、总大小与覆盖率校验凭据尚未固化,但这并不代表数据文件本身损坏:

操作 未收尾归档的可用状态
数据解析 ✅ 正常支持 —— 解析引擎直接读取分段索引与原始页面数据(src/pipeline/opfs-bundle-source.js 的 status)
导出至磁盘 ✅ 正常支持
重新导入 ✅ 正常支持(由于缺少 manifest,校验仅比对物理文件大小,触发 src/bundle/importer.js 的 no_manifest 提示)
作为下次增量基准 ❌ 无法接入增量链 —— 缺失元数据会导致下次抓取退回为全量模式
覆盖率证明 ❌ 缺少完整的覆盖率审计证据

显式中止(status: aborted)与异常“未收尾”具有本质区别:当用户主动中止或触发安全熔断时,系统会生成包含当前状态的 manifest.json,忠实记录已有范围并保留受影响路线的水位线,因此该归档依然具备作为增量基准的能力,后续执行时仅需重试未完成的局部区间。

对于因网络或源站原因导致的个别页面单点捕获失败,概览面板会明确展示失败列表及具体错误原因,用户可选择针对性重试或确认直接以此状态收尾。

唯一可能导致数据永久遗漏的场景在于尚未抓取前已被远端删除:已删除的广播动态在豆瓣官方服务中不会留下公开痕迹。

导出被打断了怎么办

导出过程异常中断的恢复机制

若导出过程因故中断,只需重新触发导出并选择相同的目标文件夹。系统会自动检测目标目录的当前状态,并仅增量补齐缺失的文件:

检测到上次导出未完成:12 个文件已写入完整(180.4 MB),尚缺 8 个文件。
继续执行将仅补齐缺失文件,已完整的物理文件保持不变。

无独立状态记录文件的断点恢复设计:系统不依赖 export-progress.json 这类单独的状态标记文件,而是将目标目录的实际物理文件作为唯一的真实状态源。这种设计的优势在于避免了状态文件与磁盘真实内容的不一致(如用户手动清理了部分文件、或磁盘满载导致状态无法落盘);在系统发生意外崩溃的边界场景下,往往没有机会保存独立的状态记录文件,因此依赖目标目录本身的物理校验才是最鲁棒的方案。

该判定逻辑之所以可靠,是因为底层的 createWritable() 在写入阶段使用临时文件操作,仅在调用 close() 成功时才执行原子替换:意外中断时,目标路径要么保留上一次的完整内容,要么文件仍不存在,而绝不会残留损坏的「半截残缺文件」。

即便如此,系统依然会执行严格的完整性校验,而非仅仅检查文件名是否存在。如果发生磁盘空间不足、外接存储设备意外拔出,或者目录中存在来自其他归档的历史同名文件,系统一旦校验出大小不匹配均会自动重新写入,保证导出数据绝对完整。

增量抓取跳过什么

增量抓取的核心假设是已归档的历史内容在上游具有静态稳定性。因此,跳过策略的界限取决于远端内容是否具备可变性:

资源类别 增量策略 设定依据
列表页(标记、广播、长文、豆列、相册列表、发起的讨论) 抓取至上次水位线为止 列表按时间倒序排列,回溯至上次记录位置即可停止
作品详情页 仅抓取新增条目 评分与短评可能随时间更新,但详情页占归档整体体积的九成,默认不重复抓取
日记 / 影评 / 讨论正文 仅抓取新增篇目 内容具备可编辑性,重新抓取可获取最新修订版本,需通过高级选项显式启用
广播 仅抓取上次抓取之后的新增内容 广播发布后具有不可变性,已捕获内容无需重复抓取
多媒体图片(用户上传原图、相册大图与作品封面) 始终跳过 现有附图与封面路线按成功 URL 复用旧字节;不是对任意 URL 永不变的保证,账号头像每个新 bundle 重新下载

中间两类可变内容由管理面板中的「增量抓取,并重新抓取可编辑内容」配置项控制。现有图片路线默认按成功 URL 复用字节,以减少带宽与磁盘开销;这不是对图片地址不可变性的格式保证。

该跳过逻辑源于对实际抓取冗余现象的分析:在早期版本中,一次普通增量抓取重复拉取了 11 张已存在的多媒体图片(其中部分图片甚至在多次增量中被重复捕获了 3 次)。其原因在于广播中的图片属于派生资源(asset.status_photo),每次增量为了发现新动态必须重新拉取前几页广播,导致同一张图被反复派生并加入待抓取队列。通过在派生阶段引入归档去重判断,彻底杜绝了此类冗余网络开销。

对于抓取失败的条目会自动安排重试,这也是跳过名单必须严格维持的核心机制:名单里只放 verdict: ok 的那些。被反爬拦截、网络超时未能获取,或是暂时被标记为已删除(gone —— 条目可能恢复)的页面,在后续增量抓取中均会重新尝试。误将未成功项加入跳过名单会导致该内容被永久遗漏,而少跳过一条仅仅是产生一次轻微的冗余请求 —— 两个方向的容错代价完全不对称。因此判定规则必须采用白名单机制,而非宽泛的黑名单排除法。

归档分类规则位于 src/crawl/known-captures.js,保持纯函数设计:由于 offscreen 文档无法直接在 Node 环境中导入(依赖 chrome.* 和 Worker 环境),若将业务逻辑耦合在其中,单元测试便只能退化为脆弱的源码正则匹配,无法有效验证运行时语义。相同的设计分层考量亦体现在 crawl/backlog.js 中。

把导出过的档案搬回来

外部归档的重新导入与恢复机制

在扩展面板的「档案」页中点击「导入档案…」,选择 doubak-bundle-… 文件夹或其父级目录(系统会自动往下找三层,能够自适应按月归档或多层嵌套解压的文件结构)。

该功能的核心价值: 用户在将数据导出至磁盘后,通常希望能够清理扩展内部的存储占用以释放空间。只有在支持双向无损导入恢复的前提下,「导出后可安全删除」的承诺才真正成立 —— 否则当下一次需要执行增量抓取时,系统将失去基准参照,被迫退回耗时数小时的全量抓取。

三项核心设计保障:

① 身份以 bundle_id 为准,而非目录名。 本地目录名称容易被用户重命名、放入子目录或因重复解压生成包含副本编号的名称。目录名仅作为初步检索线索,归档唯一标识以 manifest.json 与各 index 文件名中记录的 ID 为准,且所有组件的元数据必须完全匹配。若存在不匹配,说明该目录下混杂了不同归档的分段文件,直接导入会导致数据自相矛盾,系统将主动拦截。

这一校验在早期版本中曾仅核对了首个 index 文件,若排序在前的索引碰巧与 manifest 一致,则会误判为合法,导致另一份归档的分段文件混入。后续改进为检索并比对全部 index 文件,确保整套归档文件完全一致。

② 绝不覆盖已有归档 —— 这一原则由底层存储层强制保障。 负责导入的 Worker 仅被授予新建文件的权限,无法篡改任何已存在的物理字节;判断依据是直接向底层存储校验「该文件当前是否存在」,而非信任调用方的传入声明(src/storage/opfs-rpc.js)。这种机制比简单限制「仅允许写入空目录」更加精准且实用:支持对上次未完成的断点导入进行无缝补全(仅新建缺失的文件),避免了强迫用户先手动删除半截归档的繁琐操作。

③ 遵循“先扫描校验、后执行写入”的工作流。 选择目录后,系统先完整扫描并列出清晰的导入变更清单,经用户确认后才执行物理写入。每一种拒绝导入的情况都有明确的原因反馈:

状态反馈 触发场景
已经有了 目标文件与物理字节数完全一致,无需重复导入
补齐 上次导入意外中断,本次仅补齐缺失的物理分段
重复 同一份归档在所选的目录树中被检测到多次
编号撞了 存在相同 bundle_id,但文件清单或字节数不同的归档,系统默认拒绝覆盖以防止误损数据
别的账号 检测到与当前浏览器已绑定账号不同的归档(混合后会导致解析器异常,需用户显式确认)
不能导 分段文件缺失、实际文件大小与 manifest 记录不一致,或目录内混杂多份归档(提示信息会明确指出异常的具体文件)

在校验阶段主动拒绝的代价为零 —— 本地磁盘源文件完好无损;而一旦导入损坏或不一致的归档,则会在系统内产生表面可用但在深层查询或解包时发生偏移量越界崩溃的隐蔽异常。

About

豆备 (Doubak) 的浏览器插件,用于实际从豆瓣 (Douban.com) 下载网页,整理数据,导出等。整个项目的所有功能都可以在这个插件里找到入口。

Resources

Stars

29 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages