怎么用
四步:装扩展 → 抓下来 → 解析 → 建站或者导出。 每一步都在你自己的机器上跑,中间不经过任何服务器,断网也能跑完后面三步。
这一页是导览,不是文档。
每一节末尾都有一条指向对应仓库 README.md 的链接,
那才是唯一真源——具体命令、参数、以及所有「实测多少条」的数字,
一律以 README 为准。这一页会过时,README 跟着代码一起改。
只想要一份备份的话,第 1 步就够了:装上扩展、抓一次、导出到硬盘, 那份档案是完整的,后面三步什么时候做、做不做,都不影响它。
- 扩展:在你的浏览器里抓豆瓣,把页面原样存成原始档案(bundle)
- 解析器:原始档案 → 结构化数据(canonical)
- 生成器:结构化数据 → 一个能打开的静态网站
- 导出适配器:结构化数据 → NeoDB / Letterboxd / Goodreads 的导入文件
第 1 步 · 抓下来
装上浏览器扩展,用你自己的账号、自己的 IP、自己的节奏抓。 登录凭据和会话 cookie 不离开你的设备——没有服务器可以泄露它们,因为根本没有服务器。
装它有三条路,从上往下试:
- Chrome 应用商店 —— 多数人走这条,点一下就装上,之后自动更新。Edge 也能从这里装。
-
Releases
—— 不想走商店的话,下
doubak-<版本>.zip。它跟商店里那份是同一条 CI 打出来的, 逐字节相同。注意那是提交给商店的格式,Chrome 不能直接装 zip: 要先解压,再到chrome://extensions打开开发者模式 →「加载已解压的扩展程序」→ 选解压出来的目录。 -
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=1 或 2 写进文件里);
二是书的 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。