豆备 DOUBAK

怎么用

四步:装扩展 → 抓下来 → 解析 → 建站或者导出。 每一步都在你自己的机器上跑,中间不经过任何服务器,断网也能跑完后面三步。

这一页是导览,不是文档。 每一节末尾都有一条指向对应仓库 README.md 的链接, 那才是唯一真源——具体命令、参数、以及所有「实测多少条」的数字, 一律以 README 为准。这一页会过时,README 跟着代码一起改。

只想要一份备份的话,第 1 步就够了:装上扩展、抓一次、导出到硬盘, 那份档案是完整的,后面三步什么时候做、做不做,都不影响它。

  1. 扩展:在你的浏览器里抓豆瓣,把页面原样存成原始档案(bundle)
  2. 解析器:原始档案 → 结构化数据(canonical)
  3. 生成器:结构化数据 → 一个能打开的静态网站
  4. 导出适配器:结构化数据 → NeoDB / Letterboxd / Goodreads 的导入文件

第 1 步 · 抓下来

装上浏览器扩展,用你自己的账号、自己的 IP、自己的节奏抓。 登录凭据和会话 cookie 不离开你的设备——没有服务器可以泄露它们,因为根本没有服务器

装它有三条路,从上往下试

  1. Chrome 应用商店 —— 多数人走这条,点一下就装上,之后自动更新。Edge 也能从这里装。
  2. Releases —— 不想走商店的话,下 doubak-<版本>.zip。它跟商店里那份是同一条 CI 打出来的, 逐字节相同。注意那是提交给商店的格式,Chrome 不能直接装 zip: 要先解压,再到 chrome://extensions 打开开发者模式 →「加载已解压的扩展程序」→ 选解压出来的目录。
  3. Actions 里的构建产物 —— 只在想试 main 上还没发版的改动时才用。 下载它需要先登录 GitHub(匿名取会 401),所以这条不适合当默认路径。

第一次跑的是全量抓取:从头到尾走一遍,能抓的都抓下来。 时间取决于账号大小,中途可以停,下次接着来。 要是想先试一小段,建议先抓广播——它是最不可替代的一路: 发出去那一刻就冻住了,改不了,而且删掉之后不留任何痕迹, 没有第二个地方能发现它曾经存在过。

抓完在面板里导出,得到一个目录,那就是你的原始档案(bundle)。

以后再抓用增量抓取:它走到上次那条为止就停, 广播、已经抓过的作品详情页、还有图片都跳过——判据是 「上游那份东西还会不会变」,不是「抓过没有」。 所以增量很快,可以经常跑。日记和影评是可以编辑的, 想连它们一起重抓,用「增量抓取,并重新抓取可以编辑的内容」那个选项。

每次导出都是一份新的原始档案,别覆盖旧的—— 解析器要的是装着历次档案的整个目录,多一份就多一批观测。

备份和恢复

导出到硬盘之后,扩展里那一份是可以删的。 但这句话只有在它回得来的时候才诚实,所以档案页上有「导入档案…」: 选中 doubak-bundle-… 文件夹,或者它的上一级都行 (往下找三层,按月归档、解压多套一层,都认得出来)。

恢复不是为了看,是为了接着抓:没有旧档案当基准, 下一次增量就退回全量——几个小时,还要把几千个作品详情页重抓一遍。

档案就是一个普通文件夹,里面是 WARC 段文件、索引和 manifest.json。所以「怎么备份」没有专门的答案—— 拷到移动硬盘、扔进网盘、同步到 NAS、丢给任何一个文件备份工具, 都跟备份别的文件夹没有区别。换台机器想接着抓, 把文件夹拷过去、在那台机器上「导入档案…」就行。

网盘改了文件夹名字也不要紧。 档案的身份写在 manifest.json 和索引文件名里, 不是目录名——所以 xxx (1)备份/2026-08/ 这种都认得出来。 真正不能动的是文件夹里面:段文件、索引、 manifest.json 三者必须互相对得上, 少一个或者被别的档案的文件混进来,导入时会直接拒绝并告诉你是哪个文件。

档案里是你的豆瓣全部内容——短评、日记、私密豆列、 自己上传的图,原样存着。放上网盘之前想一下那个网盘是谁的。 这也是这个工具没有服务器的原因:它没地方替你保管, 所以保管在哪儿始终是你自己的选择。

导出中途断了就再导一次,选同一个文件夹,它只补缺的那几个。 没有进度文件——目的地目录本身就是进度,每个文件要么完整要么不在。 (中断留下的是「没有这个文件」而不是「半个文件」,所以这条判据站得住。)

导入不会覆盖任何已经在那儿的字节,而且先扫描、先列清单、 确认了才写。每一种「不导」都有自己的名字:已经有了、补齐、重复、 编号撞了、别的账号、不能导——合成一句「跳过」等于什么都没说。

准的是这一份: doubak-extension / README.md —— 安装、抓取范围、增量抓取跳过什么、导出被打断了怎么办,都在那里。

第 2 步 · 解析

把装着一堆原始档案(bundle)的目录整个喂给解析器,它产出 结构化数据(canonical)——可读、可 grep 的 NDJSON。 这一步是纯函数,不联网:同样的原始档案永远得到同样的结果, 随时可以删掉重跑。

node bin/parse.js <装着一堆原始档案的目录> [输出目录] [--ignore-warnings]

整个目录就好,不用自己挑哪几份, 子目录里的也会一起找到——解压出来带一层外壳、 按月份分了文件夹、几次导出堆在一起,都不用先手工摊平。 抓了很多次、换过机器、甚至有两条互不相连的链,都直接扔进去: 合并的结果正好是它们的并集,挑一条反而会丢东西。

只有一件事它会拦下来:目录里混着不止一个人的档案。 两个人的记录合进同一份结构化数据之后再也分不开, 而这件事太容易发生——把两次导出解压到同一个下载目录就够了, 往下找子目录之后更容易。 确实是同一个人的两个账号,就加 --ignore-warnings 放行; 它绕过的是「停下来」,不是「说出来」,那条告警照样会印出来。

准的是这一份: doubak-data-parser / README.md —— 两条不变量、以及为什么不该自己挑原始档案。

第 3 步 · 生成自己的网站

结构化数据加上原始档案里的图片,变成 Markdown + YAML front matter, 再交给现成的静态站生成器渲染。长什么样不用想象—— 样张站就是这么出来的。

npm run md     # 只出 Markdown + 图片(给任何静态站生成器)
npm run site   # 再构建成 HTML
npm run deploy # 铺进一个仓库根,GitHub Pages 直接发

Markdown 才是产物,HTML 只是它的一个消费者。 想换 Astro、Eleventy、Jekyll,用 md 就行,产出里没有任何 Hugo 专属的东西。

要发到网上之前先看一眼。 广播和日记里到处是别人的名字和评论, deploy 会先给你一个「哪些内容会变成公开的」的预览。

准的是这一份: doubak-site-generator / README.md —— Hugo 从哪来、怎么换别的生成器、怎么发到 GitHub Pages。

第 4 步 · 导出到别处

一条命令产出三家的导入文件,不联网—— 上不上传、什么时候传,都不影响你的档案。

node bin/export.js <canonical 目录> [输出目录]

NeoDB 收得最全,而且默认带上一样别处没有的东西: 状态历史。豆瓣只存「你现在是看过」, 但广播是发出去那一刻冻住的,所以 想看 → 在看 → 看过 每一步都还在, 带着当天的日期和当天打的星——豆瓣自己只留最后一次。 不想带就加 --no-shelf-history

整份档案导进去长什么样,也是公开的: neodb.social/users/immewx

Letterboxd 只收电影、Goodreads 只收书,所以能带走多少、带不走什么, 报告里会一条条数给你看。反过来绝不成立:档案永远存无损的超集, 这三个都是向外的有损适配器。

三家的导入都不好撤。 先用 --sample=20 切一小份传上去看看,确认没问题再导全量。

NeoDB 那一路出了问题,可以退回 CSV

默认出的是 NeoDB 自己的 NDJSON 归档格式。 万一它那边收不下——上传页面认不出格式、或者导进去有哪一类记录不对—— 还有一条走了更久的老路可以退:

node bin/export.js <canonical 目录> [输出目录] --target=neodb_csv

产出的是 neodb-import.zip上传的地方是同一个—— 还是「导入 NeoDB 备份」,那个页面会自己看 zip 里有什么来判断格式, 传上去之后「检测到的格式」那一行应该显示 CSV。 (显示「未知格式」就说明拿错文件了,那个 zip 根本提交不上去。)

这一路真导进去过一整份档案,长这样: neodb.social/users/doubak ——跟上面那个 immewx同一份档案的两条路,可以直接对照着看两边差在哪。

退回去要付的代价说清楚:CSV 结构上装不下豆列、 不挂作品的日记,以及那条状态历史时间线。 它有两处反而比 NDJSON 强:一是可见性那三个单选框只在 CSV 时才出现 (NDJSON 会被页面藏起来,一律按公开导入,要别的可见性得用 --visibility=12 写进文件里); 二是书的 ISBN 兜底——NDJSON 只按 URL 匹配, 一本豆瓣页面已经没了的书,CSV 还找得回来。

准的是这一份: doubak-export-adapters / README.md ——三家各自收得下什么、匹配靠什么、以及换格式换来的两处倒退。 第一次导之前请看 docs/manual-testing.md

档案里到底是什么

两种格式,生命周期正好相反。原始档案(bundle)由扩展写出, 是不可逆那一步的产物——没法请谁「再抓一次 2019 年」——所以一旦有人抓过就冻住不改; 结构化数据(canonical)由解析器写出,随便改, 重新跑一遍解析器不花什么代价。

两种都是 WARC 加 NDJSON,没有私有二进制格式。 判据是:2040 年一个不认识这个项目的人,能不能只靠 jq 把它读出来。

准的是这一份: doubak-data-specs / bundle/v1/SPEC.md ——格式定义、以及配套的校验器和一致性用例。

出了问题

每个仓库都收 issue,报的时候把命令打出来的整段报告带上—— 里面每个数字都是实测的,比描述有用得多。 仓库列表在 github.com/Doubak