Skip to content

Repository files navigation

doubak-extension

中文 · English README

test Coverage Status

这是源码仓库。 想了解这个工具是什么、能做什么,去 https://doubak.com; 想看它生成出来长什么样,去 https://sample.doubak.com

豆备 (Doubak) 的浏览器插件,整个项目的所有功能都可以在这个插件里找到入口。

它在你自己的浏览器、用你自己的 IP 和节奏抓取豆瓣,把页面原样存成标准 WARC 档案。登录凭据和会话 cookie 不离开你的设备。验收标准是:把服务器关掉,插件依然能产出一份完整可用的本地档案。

抓完之后不用再装任何东西:「导出」页当场把档案算成结构化数据NeoDB 导入包、 或者一棵 Markdown 站点(图片一并导出)。用的就是下游那三个仓库的同一份代码 (src/vendor/,逐字节拷贝 + CI 查新鲜度),所以扩展出的东西和命令行出的东西是同一个东西。

v1.0。 全量与增量抓取都对着真实豆瓣跑通了,图片(自己上传的、作品封面)、 长文正文、豆列、导出与导入都在里面。相册与移动应用(app)都明确不做, 但理由不是同一个:app 是上游已废弃,相册是那些照片属于条目而不属于你的档案 ——见下方「现状」。

文档

  • DESIGN.md —— 功能规划、抓取边界模型、三份实测附录
  • docs/toolchain.md —— 工具链选型与理由
  • docs/ui.md —— 界面设计:说什么话、怎么显示进度才不撒谎
  • docs/permissions.md —— 权限审计:声明了什么、刻意不要什么、 以及「哪个执行上下文有哪个 API」那张咬过人的表
  • docs/release.md —— 怎么发一个版本,以及版本号为什么值得小心
  • 档案格式定义在另一个仓库:doubak-data-specs

开发

零依赖、零构建步骤。仓库里的源码就是浏览器里跑的东西。

npm test                       # 跑测试(Node 内置的测试运行器,不需要 npm install)
node tools/package.mjs         # Chrome / Edge 的 zip → dist/doubak-<版本>.zip
node tools/package.mjs --firefox               # AMO 的 zip → 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.mjsTARGETS 表里那三格: 用哪份 manifest、文件名后缀、去掉哪两个在 Firefox 上永远走不到的文件 (host-offscreen.jsoffscreen.html)。manifest.firefox.json算出来的, 手改会被测试打回——它与 Chrome 那份分家的方向是「Firefox 那份少了一个新权限」, 症状是装得上、某个功能悄悄不工作。

装载到浏览器

四条路,装出来是同一个扩展。Firefox 用的是另一个包-firefox 结尾的那个), 两份 manifest 不一样,装错了是直接加载失败,而失败信息不会提到是拿错了包。

从 Chrome 应用商店装(多数人走这条): https://chromewebstore.google.com/detail/hilmaopahndgbiolohgefnbeedobpafe (链接只写扩展 ID、不带名字那一段——带名字的形式在改名之后会失效)。Edge 也能从 这里装。

下一个发布版(不想走商店时):从 Releases 拿 zip,先解压, 然后按浏览器分两条路——那两份都是商店的提交格式,收件人是商店,不是浏览器:

拿哪个 怎么装
Chrome / Edge doubak-<版本>.zip chrome://extensions → 开发者模式 → 加载已解压的扩展程序 → 选解压出的目录
Firefox doubak-<版本>-firefox.zip about:debugging#/runtime/this-firefox → 临时载入附加组件… → 选目录里的 manifest.json

Chrome 那份与商店里的是同一个包:同一条 CI 打出来,时间戳清零,逐字节相同。 Firefox 的「临时载入」是字面意思,关掉浏览器就没了;但 OPFS 里的档案还在,下次 载入还能接着增量抓。AMO 上架之前只有这一条路(#11)。

从仓库直接装:Chrome / Edge 同样是「加载已解压的扩展程序」,选本仓库根目录。 这个项目没有构建步骤,源码就是浏览器里跑的东西,所以这条路一直有效——代价是它带着 test/docs/,不是分发出去的那份。Firefox 走不了这一条:仓库根上那份 manifest.json 是 Chrome 形状的,载入时 Firefox 直接拒绝,原话是

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

node tools/package.mjs --firefox --stage dist/unpacked-firefox,再选那个目录里的 manifest.json

试 main 上还没发版的改动:GitHub 的 Actions → package 里挑一次运行,下载 doubak-<版本>-unpacked(Firefox 是 -firefox-unpacked),解压 即可加载。里面只有运行时真正需要的文件。下载构建产物需要登录 GitHub(匿名取是 401),所以面向用户的地方一律指向 Releases。

发版流程见 docs/release.md

浏览器自检

有些东西 Node 里测不到:OPFS、持久化存储许可、配额。装载扩展后打开

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

点「开始自检」,它会在真实浏览器里跑:

  • FileStore 契约(与 Node 里跑在内存实现上的是同一组断言),两遍——一遍直接对 OPFS, 一遍经由 Worker RPC(抓取真正走的是后者)
  • IndexedDB 的 KvStore 契约(Node 里没有 IndexedDB,这是它唯一的真实覆盖)
  • 完整的 bundle 写入与崩溃恢复
  • 环境探测:持久化存储许可、配额、写入吞吐

跑完点「复制报告」拿到纯文本,[PASS] / [FAIL] 前缀,失败项汇总在开头。

手机浏览器:豆瓣发的是手机版,扩展会把它改回桌面版

装在移动版 Chromium(安卓 Edge、Kiwi 之类)上时,豆瓣按 User-Agent 里的手机标记 把每一条路线都跳到 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 里那个手机标记去掉。三条边界:

  • 只删词,不编 UA。 安卓 Chromium 去掉 Mobile 之后,得到的正是同一个浏览器 在安卓平板上发的那一个——同一系统、同一浏览器、同一份 TLS 指纹。规范里 「生产者不得伪造 UA」给的理由就是「伪造出的 UA 与指纹不一致,反而更容易被风控 识别」,而拼一个 (X11; Linux x86_64) 出来正好撞在那句话上。认不出的形状 (iOS 的 Mobile/15E148 是个带版本号的整体,删掉得不到任何真实存在的 UA) 一律不动,宁可不支持。
  • Firefox 安卓不在覆盖范围内,而这不是「还没做」。 它的两种真实形态 (Mobile; / Tablet;会被跳到手机版,唯一能拿到桌面版的是货真价实的 桌面 UA——在安卓上发它就正好是上面那句禁令针对的东西。这个方案在 Firefox 上 不成立,留给 #11 在真机上定。
  • 只改扩展自己发的请求tabIds: [-1])。你在标签页里逛豆瓣照样是手机版—— 在手机上多半就是想要手机版。这也是浏览器那个「请求桌面版网站」开关帮不上忙的 原因:它按标签页生效,而抓取跑在离屏文档里。
  • 桌面浏览器上这条规则压根不装,发出去的 UA 与之前逐字节相同。

抓出来的档案会在 manifest.notes 里写明这件事;producer.user_agent 里记的 仍然是浏览器真实的那一个。调试页的「环境自检」表上有这两个 UA,可以直接核对。

这一条只解决内容那一侧:页面回到桌面模板,分类器与抽取器就都对得上了。 离屏文档在移动版浏览器上能不能扛住几小时的抓取,是另一个问题,还没有实测。

界面

点工具栏图标直接开面板标签页(已经开着就切过去):

标签 内容
概览 各路线进度、抓不下来的页面(可重试 / 可确认收尾)、开抓前预检
覆盖率 合起来 / 这一份两个视角。默认合起来——增量之后完整性是整条链的属性
档案 档案的一切都在这一页:导入、导出、「验一验」、删除、用量;记录预览;标出新增 / 又抓了一次;跨链的版本历史。选中一份之后有两个互斥的视角——翻看捕获(生的:URL、判定、原始字节)与查看内容(熟的:解析成条目,看得见自己写的短评、广播、豆列评语)
日志 抓过的 URL、重试、停机、错误,存在本机、可复制
调试 演练(零网络请求)、小范围试跑、环境自检

现状

已经能跑的

模块 状态
脚手架、工具链、基础类型
bundle 写入器(段轮转 / index / manifest) ✅ 产出通过规范校验器
崩溃恢复 ✅ 真实 OPFS 上验证过
OPFS 存储后端 + Worker RPC ✅ 与内存实现共用一份契约测试
会话守卫、分类器、路线注册表、frontier ✅ 分类器对着 6341 个真实页面校准
抓取引擎、主循环、编排 ✅ 对真实豆瓣跑通
offscreen 架构 ✅ 抓取跑在 offscreen document 里,service worker 只调度
生命周期自恢复(心跳、崩溃哨兵)
抓取状态持久化(IndexedDB)
增量抓取 ✅ 下界按路线沿链回溯(规范 §5.5);有缺口的路线不提供下界,从头重走。抓过的作品页 / 长文正文 / 图一律跳过(见下)
完整面板 ✅ 唯一的界面;popup 已拆掉(docs/ui.md §1.1)
bundle 读取器与「验一验」
导出(File System Access)+ 回读校验
导入(把导出过的档案搬回来) ✅ 认 bundle_id 而不是目录名;先列出「将要发生什么」再写;残缺 / 撞编号 / 别的账号一律不导
存储管理(列表 / 删除 / 配额) ✅ 并进档案页 —— 原来两页列的是同一批档案,只换了几列
「导出」页(canonical / NeoDB / Markdown) ✅ 三种产出共用一次解析,中间产物不落盘,直接写进用户选的文件夹
通知与角标

路线覆盖

路线 状态
广播
标记列表(书 / 影视 / 音乐 / 游戏 / 舞台剧)
作品详情页 ✅ 由列表页派生,受广播门控
日记 / 影评书评 ✅ 列表页只有截断的摘要,所以正文页单独抓一遍;两种日记 URL 形状都认
用户上传的图片 ✅ 广播附图与日记正文内嵌图,只取本人上传的原图
作品封面 ✅ 随作品详情页抓,单独成段
相册 明确不抓(2026-09-06 定)。个人相册这个账号确实是「创建 0」;而传到作品相册里的照片属于那个条目,不属于你的档案——实测 17 条广播、51 张。宣布这件事的那 17 条广播照抓,照片本身不抓,见下
豆列 ✅ 只抓自己编的(doulists/all)与每份的内容页。值钱的是条目上自己写的评语——实测 134 个条目里 62 条有
移动应用(app) 不支持,上游已废弃:列表页打得开,作品详情页全部 404(2026-08-14 实测)。你的 app 标记本身随广播照抓

还没做的

为什么要紧
移动应用(app) 不在这张表上了 —— 它不是「还没做」,是上游没有了,见上面的路线覆盖表

已经不在这张表上的

相册从「还没做」挪到了「明确不抓」。原来这一行写着「这个账号一个相册都没有, 没有样本可量」——那句话是错的,而且错得很安静:个人相册的确是 0,但实测有 17 条广播写着「MewX 上传了 N 张照片到《寂静之人》的相册」,一共 51 张, 其中 46 张的缩略图早就躺在已抓下来的时间线页里。样本一直在,只是没人去数。

不抓是范围决定,不是抓不到:传进作品相册的照片是对那个条目的贡献, 跟着条目走而不是跟着账号走——所有人都看得见,保留与删除由条目那边决定。这正是 「用户创作内容与目录数据永远不放在一张表里」那条规矩,所以它归目录那一侧。 上传这个动作是你的,上传的东西属于条目:那 17 条广播(你写的那句话、日期、 指向哪个相册)照进档案,51 张照片不进。真要留是 enricher 之后的事,甚至不是 enricher。

写清楚而不是删掉,是因为「查过了,决定不做」与「还没做」必须分开——下一个人 照样会数出那 51 张,然后重推一遍这件事,而这里每一次「重查」都要对着真实豆瓣点。

长广播的全文这一条查下来根本不缺:实测 804 条带正文的广播里有 2 条被截断, 而两条的「(全文)」都指向日记,两篇日记本来就完整地在档案里。所以既不用重爬也不用 加路线,只是当时没人顺着那个链接看一眼。截断本身现在记在 canonical 里 (text_truncated + full_text_url)。在给「缺的东西」造一条抓取路径之前, 先确认它是不是已经在档案里的别处。

图片抓取(F-04e)与长文正文都做完了 —— 前者曾是「备份必须联网才能看」的病根。 一次完整的全量抓取也跑过了:4 小时 16 分、5880 条捕获、零失败。 中断续导(F-08i)也做了:导出被打断之后,再导一次只补缺的那些。 导入也做完了:换机器、清过站点数据之后,以前导出的档案搬得回来, 之后照样能增量抓取——否则「导出之后可以安全删除」这句话只成立一半。

测试:npm test(零安装即可跑,85 个测试文件、1888 个测试)。装了可选开发依赖后会额外用 webrecorder 的 warcio 独立验证 WARC 输出;同级目录有 doubak-data-specs 时会额外跑跨仓库一致性检查(规范常量的新鲜度、产出与校验器的一致性)。

发布到应用商店

完整流程(改版本号 → 打标签 → CI 建 release → 核对哈希)在 docs/release.md。打了 v* 标签之后 release 上挂的两份 zip 就是 提交用的,不需要另外打。想在本地各打一份:

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

几件要知道的:

  • 打包用白名单,不是黑名单。 名单在 tools/package.mjsINCLUDE 里。黑名单漏一条会多打进去一个不该有的东西且没人发现;白名单漏一条则是扩展装上就报错 —— 后者一眼就能看见。有测试守着 test/ tools/ docs/ 不许进包。
  • selftest/ 必须打包进去。 它没有被 manifest.json 引用,只被调试页那个按钮 getURL 打开 —— 漏了它,那个按钮就是个死链。
  • _locales/ 同理,而且更狠。 manifest.jsondefault_locale 指着它,漏了它 Chrome 整个拒绝加载这个扩展;而 collect() 只在名单里的路径不存在时才抛,名单里压根没有的目录它不看 —— 本地全绿,问题要到上传审核时才出现。test/locales.test.js 守着这一条。
  • zip 是自己写的,不调系统 zip 与整个工具链一致(不依赖外部程序),而且时间戳一律写 0,所以同样的源码打出逐字节相同的包 —— 才能核对「上传的到底是不是我构建的那个」。
  • 版本号只能升不能降。manifest.jsonpackage.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、不用 clone 任何仓库:

产出 是什么 拿它干什么
结构化数据(canonical) 五个 NDJSON,每行一条 JSON jq 直接查;也是下面两个的输入
NeoDB 导入包 一个 zip 传到 NeoDB「设置 → 数据 → 导入 NeoDB 备份」
Markdown 站点 带 YAML 头的 Markdown 树 + 图片 + 搜索索引 交给 Hugo / Astro / Eleventy / Jekyll

三种都是现场从档案算出来的:解析结果不落盘(派生数据落盘就是第二个真相来源), 直接写进你选的文件夹,一次只跑一个。样张:sample.doubak.com

命令行工具做的是同一件事,适合要改参数、接进脚本、或者只想拿一部分数据的人:

doubak-data-parser bundle → canonical(结构化、带修订历史、纯离线)
doubak-site-generator canonical → Markdown + 图片 → 静态网站
doubak-export-adapters canonical → NeoDB / Letterboxd / Goodreads 的导入文件

为什么是抄一份,不是 submodule

src/vendor/ 是那三个仓库里 23 个纯函数文件的逐字节拷贝tools/sync-vendor.mjs --check 由测试与 CI 守着新鲜度。

一次跨仓库的改动要先合上游。 CI 按各自的默认分支检出兄弟仓库,所以 vendor 名单里 新加的文件在上游合并之前根本不存在,这边的 CI 一定是红的。这不是坏事——它就是 「规范先行」那条规矩在这里的样子(见根目录 CLAUDE.md 的最后一条约定)。

不用 submodule 有个具体的理由:git clone 不加 --recursive 会留下一个存在但是空的 目录,而 tools/package.mjscollect() 只在路径不存在时才抛——空目录它照收不误。 于是打包成功,装出来的扩展里没有解析器,而且一声不响。白名单当初就是为了避开这种 「静静少了东西」的失败。

界线是「字节从哪儿来各写各的,字节怎么解释只有一份」:读文件系统的那几个 (bundle-source.jscanonical.jsgenerate.jsimages.js)不搬, 扩展这边在 src/pipeline/ 里有对应的 OPFS 版本。

抓取被打断了怎么办

什么都不用做,尤其不要删掉那份档案。

停机原因有十几种(验证码、软封锁、掉登录、断网、切账号、配额、手动暂停、关掉浏览器……), 没有一种会让你损失已经抓到的东西。每抓到一页就落一次盘,所以无论停在哪儿, 在那之前的每一页都已经在档案里了。连写盘出错也不例外:修复的方向永远是向后截断, 被截掉的那一条会在续抓时重新抓一遍(src/bundle/recovery.js),重复是免费的。

没收尾的档案没有 manifest.json —— 它只在收尾时写一次。于是账号、体积、 覆盖率证据都还看不到,但那不是损坏:

没收尾的档案
解析 ✅ 照常 —— 读的是索引与页面(src/pipeline/opfs-bundle-source.jsstatus
导出到磁盘 ✅ 照常
再导入回来 ✅ 照常,只是校验只能核对字节数(src/bundle/importer.jsno_manifest 告警)
当下次增量的基准 ❌ 进不了链 —— 下次会退回全量
证明某条路线抓全了 ❌ 没有覆盖率证据

中止status: aborted)与「没收尾」不是一回事:中止是会写 manifest 的, 缺口如实记进去、受影响路线的水位线不推进,所以它仍然可以当增量基准, 下次只会重走那一段。

唯一一种真的「失败」是个别页面抓不下来:那是逐页的,概览页会点名列出(哪几页、 什么原因),可以重试,也可以确认「就这样收尾」。

真正会让你永久丢东西的只有一件事 —— 没抓。广播删掉不留任何痕迹。

导出被打断了怎么办

再导一次,选同一个文件夹。它会先看清楚上次导到哪儿,然后只补缺的那些

上次导出到一半:12 个文件已经完整(180.4 MB),还差 8 个。
继续只会补齐缺的那些,已经完整的不动。

没有进度文件。 目的地目录本身就是进度 —— 每个文件要么完整、要么不在。记一个 export-progress.json 反而更糟:它会与真实情况不同步(用户手动删了一个文件、盘满了写了一半),而崩溃恰恰是最没机会写下状态的时刻,偏偏崩溃正是续导要解决的场景。

这条判据站得住,是因为 createWritable() 写的是临时文件、只在 close() 那一刻整体换上去:中断留下的是「没有这个文件」,不是「半个文件」。

但仍然要验,不能只看在不在。盘满、U 盘拔掉、上次导的是别的档案,都会留下一个同名却对不上的文件 —— 那种情况照样重抄。

增量抓取跳过什么

增量的前提是**「已经抓到的东西都没有变过」,所以它跳过的不只是列表页上的旧条目。分界线是上游那份东西还会不会变**:

这一类 增量 为什么
列表页 抓到上次那条为止 按时间排,往回走到上次的位置就够
作品详情页 只抓新出现的 评分与短评会变,但它占档案九成体积
日记 / 影评正文 只抓新出现的 可编辑,所以重抓有意义 —— 但那是用户明说要做的事
广播 只抓上次之后的 发布后不可编辑,抓过的永远不会变
图(自己上传的、封面) 永远跳过 图片地址是内容地址,重抓拿回来必然是同一批字节

中间两档是「增量抓取,并重新抓取可以编辑的内容」那个选项管的。图那一档没有对应的选项,因为重抓一张已有的图拿不到任何新东西 —— 那不是一个「要不要」的选择。

这条规则是被一个真实的浪费逼出来的。 一次普通增量重抓了 11 张已经在档案里的图(0.77 MB),其中 3 张已经被抓过三遍(ba57a30fb09c157e63,每一份的 parent_capture_id 都指向自己那一趟刚抓的广播页)。

成因是派生asset.status_photo 是从广播页上抽出来的,而增量必须重读最新那几页广播(不然发现不了新条目)。于是那几页上的图每趟都被重新派生一遍 —— 而派生这条路上从来没有过「我是不是已经有了」这道判断。作品详情页有跳过名单,长文和图都没有。

失败过的会自动重试,而这一点是整套跳过名单唯一必须保住的性质:名单里只放 verdict: ok 的那些。被拦下、没抓着、当时已被删掉(gone —— 条目可能又回来了)的,下一趟都会再试。多收一条的代价是那份东西再也不会被抓,而少收一条只是白抓一遍 —— 两个方向差着一个量级,所以判据是白名单而不是「除了列表页以外都算」(把列表页收进去,后果是整趟抓取一页都不抓)。

分档规则在 src/crawl/known-captures.js,是纯函数:offscreen 在 node 里 import 不进来(chrome.*、Worker),写在那里的逻辑就只能拿正则去比对源码,而那种判据挡不住语义错误。同一个分层理由见 crawl/backlog.js

把导出过的档案搬回来

档案页 →「导入档案…」,选中 doubak-bundle-… 文件夹,或者它的上一级(往下找三层,所以按月归档、解压出来多套一层都认得出)。

它存在的理由: 我们一直在劝用户「导出之后可以安全地删掉扩展里那一份」。那句话只有在它回得来的时候才诚实 —— 否则下一次增量抓取找不到基准,退回全量:几小时,还要把几千个作品详情页重抓一遍。

三件事值得单独说:

① 身份是 bundle_id,不是目录名。 用户会改名、会放进 备份/2026-08/、解压两遍会得到 xxx (1)。目录名只当线索,真相在 manifest.json 与 index 文件名里 —— 而这几处必须互相对得上。对不上意味着两份档案的文件被放进了同一个目录,那时任何一种猜法都可能写出一份自相矛盾的档案(index 说自己属于 A,段文件名里写着 B)。

这一条的检查第一版写错过,而且错得很值得记:实现只取了第一个 index 文件,而排序后先出现的那个恰好与 manifest 对得上 —— 于是「三处必须一致」全票通过,另一份档案的段文件跟着一起导了进来。收全部 index 文件再比才对。

② 绝不覆盖已有档案 —— 这一条不靠界面拦着。 导入用的 Worker 只能新建文件,碰不到任何已经在那儿的字节;判据是去问存储「这个文件此刻在不在」,不是听调用方声称(src/storage/opfs-rpc.js)。它比「只许往空目录里写」更准,也更有用:补齐上次没导完的天然合法(缺的文件是新建),而后者会把这种情况一起挡掉、逼用户先删掉半份档案。

③ 先看清楚,再动一个字节。 点完文件夹先扫描、先判断、先把整份清单列出来,确认了才写。每一种「不导」都有自己的名字和理由 —— 合成一句「跳过」等于什么都没说:

说什么 什么情况
已经有了 文件与字节数完全一致,不必再导
补齐 上次导到一半,只补缺的那几个
重复 同一份档案在选的目录树里出现了不止一次
编号撞了 同编号但内容不同。不覆盖 —— 说不清哪份是对的
别的账号 混在一起之后解析器会拒绝整个目录,而合过之后拆不开。会问一次
不能导 段文件缺了、字节数与 manifest 对不上、一个目录里两份档案。提示里写出具体是哪个文件

拒绝的代价是零 —— 源文件还在盘上,什么都没丢;而导进来一份残缺档案的代价是一份看起来正常的坏档案:能选中、能导出、能被解析器读,只是索引里有偏移量指向不存在的字节。

About

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

Resources

Stars

5 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages