上一轮:工作区\交接-知识库修复与EPUB碎块-20261002.md(重排修复、碎块根因、723 块候选方案)。 本轮做的是 ChatGPT 回复里要求的补查与单章对照测试。全部结论均为本机实测,命令与产物路径见文末。
实测(脚本 _tmp\epub_spine_check.py,直接读 content.opf):
META-INF/container.xml → rootfile OEBPS/content.opf
manifest 251 项 = xhtml 29 + image/jpeg 219 + css 2 + ncx 1
spine itemref = 29,顺序 text00000.html … text00028.html
ZIP 内 29 个 html 的字母序 = text00000.html … text00028.html
两者顺序完全一致:True 集合一致:True
结论要限定范围:
| 项 | 结论 |
|---|---|
| 本 EPUB 的阅读顺序 | ✅ 正确。spine 与 ZIP 顺序逐项一致,29 页无缺无多 |
| 实现是否遵循 OPF spine | ❌ 没有。parseEpub(lib/knowledge/index.js:4433-4445)只做 Object.keys(zip.files).sort() + 扩展名过滤 + join("\n\n"),从不读 container.xml / content.opf / spine |
| 风险 | 属潜在缺陷:凡 spine 顺序 ≠ ZIP 字母序的 EPUB(改名、非零填充编号、spine 重排),解析出的正文顺序会错。本文件恰好看不出问题 |
实测(_tmp\epub_img_refs.py):
ZIP 内 .jpg 文件 219 个,编号 Image00000–00219,其中 00218 缺号(共 219 个 = 0..219 去掉 218)
HTML 中 <img> 标签 217 个,对应 217 个不同文件名(无重复引用)
孤儿图片(存在但无 <img> 引用) Image00217.jpg、Image00219.jpg
存档文本图片占位  217 个,使用的文件名最大到 Image00216.jpg
两个孤儿的身份(content.opf manifest 属性 + 引用关系):
| 文件 | manifest 属性 | 说明 |
|---|---|---|
Image00217.jpg | properties="cover-image" | 封面图,由阅读器取用,正文不引用 |
Image00219.jpg | 普通 image 项,无 properties | 正文、toc.ncx、guide 均无引用;相邻的 00218 已被出版社删除 → 判为封底/版权类页图(同批被清理) |
逐项对应:217 个占位 ↔ 217 个 <img>,文件名一一对应,无缺失、无多余、无重复。差 2 = 封面 1 + 封底类 1。
顺带核到的可访问性(此前未测):
| 项 | 结果 |
|---|---|
脚注锚点 <a href="textNNNNN.html#filepos…"> | 1922 个,断链 0 个(目标页与目标 id 全部存在);同页 840 / 跨页 1082 |
其他 <a href>(非 text 开头) | 58 个,未逐一定位 |
<img> 的 alt 文本 | 全部为空(0/217)→ 图内文字确实没有任何文字化入口,只能靠视觉模型另做 |
| 图片路径 | 全部在 OEBPS/ 同一目录,与 HTML 同目录,相对路径可直接解析 |
按插件自己的口径复算(chunkSize=800、chunkOverlap=100、smartChunk=true):
normalizeText 后长度 283093;estimateTokens(ck/1.5 + 其他/4) = 124162
charsPerToken = 2.2800 → safeSize = round(800×2.28) = 1824 字符,safeOverlap = 228
上一轮交接里「折算 1280 字符」需更正为 1824。对结论无实质影响(现状仍是 0 个块超限),但后续拆分阈值要按 1824 判断。
写了两版抽取器,第二版才对齐(差异全在非 <p> 结构上):
| 版本 | 差异原因 | 结果 |
|---|---|---|
v1 只取 <p> | 漏掉 text00007.html(540 个 <blockquote> 目录行)与 text00028.html(嵌套 blockquote)内的 1057 块 | 5478 块 |
v3/v4 取 body 顶层块元素(<p> + <blockquote>) | — | 6535 块,拼接后 283096 字符,与库内 rawText 逐字相同(含 29 处仅来自我复刻脚本的页眉行) |
抽取器同时输出可追溯字段:page(原始 XHTML)、elemIndex(页内第几个 <p>)、id(如 filepos203143)、class、spans、images。这满足「每块能定位到版本、章节、原 XHTML/锚点」的要求。
按上轮 §五.2 的做法执行:第三章(手太阴经络与腧穴,text00011.html),两个临时库、同一套嵌入/重排/检索参数。
文件与库:
| 项 | A(现状策略) | B(结构优先) |
|---|---|---|
| 源文件 | _tmp\ch3-A.md(每段一块,183 段 / 7451 字符) | _tmp\ch3-B.md(结构优先,20 片 / 7604 字符) |
| 临时库 | _tmp-第三章策略A 30cea406-cc9e-4a5b-a538-9b9f7a43536e | _tmp-第三章策略B b0fdc239-1b96-4d1c-8891-1578bad6ae25 |
| 入库块数 | 183 | 184 |
| 块长中位/均值/p90/最长 | 22 / 38.7 / 74 / 430 | 24 / 39.3 / 74 / 430 |
【定位】/【主治】/图片/脚注锚点 | 11 / 11 / 9 / 68 | 11 / 11 / 9 / 68 |
| 孤立标题块 | 9 个(章、节、一、、(一)…各自成块) | 7 个(其中 3 级标题被并成一片) |
关键实测:B 的源文件本来就是 20 片,但入库后仍变成 184 块 —— 因为插件的 splitBlocks 按空行分段,B 片内部为了保留段落结构必然留有换行分隔,于是入库时被重新切开;我为超预算穴位条目加的「章·节」来源前缀也因此被单独切成了 第三章手太阴经络与腧穴 · 第二节 手太阴腧穴 这种小片(库内块 134/142/175 实测)。
也就是说:
splitBlocks 就按空行把聚合拆回去。parseEpub / splitBlocks:让 EPUB 解析按 OPF spine + 元素结构产出块(推荐,一次到位,且能顺带修 §一 的 spine 问题);在同一 6535 块基线上按 §五 规则离线重排(_tmp\seg_v9.py):
| 指标 | A 现状 | B 结构优先 |
|---|---|---|
| 块/片数 | 6535 | 1151 |
| 中位/均值/p90/最长 | 28 / 41.3 / 75 / 951 | 108 / 244.0 / 405 / 43371(附录歌诀那段,嵌套 blockquote 无段落边界,属解析层残留问题) |
| 超 1824 字符 | 0 | 6(全部是论文式长段/附录,需按句再切) |
| 孤立标题 | 253(含目录行 540 另计) | 0(标题与紧随内容同块) |
| 无损性 | — | 去空白逐字相同;253 个标题一个不少 |
| 穴位条目 | 401 个【定位】分散在 401+ 块 | 401 个条目各自成单元,穴位头与【定位】同片 |
(注:401 是全书穴位条目数,与单章 A/B 的 11 个不矛盾。)
dsh-knowledge(改 parseEpub 读 spine + 按元素层级切块)。这是唯一能让结构优先真正生效的位置;插件升级会覆盖改动,需记录还原点。imageCaptionProvider=off,尚未启用。_tmp-策略验证-可删、_tmp-第三章策略A/B)用完即删。工作区\_tmp\,未清理)| 文件 | 用途 |
|---|---|
epub_spine_check.py / epub_img_refs.py | 补查 1、2 的脚本与结论 |
extract_v1.js / extract_v2.js / extract_para.js | EPUB 结构抽取器(v2 = 与库内逐字一致的版本) |
blocks_src.json / block_tiers.json | 6535 块 + 结构层级标注 |
seg_v9.py / B-pieces-v9.json | 结构优先切分引擎与结果(无损性已验) |
ch3-A.md / ch3-B.md / bases.json | 单章 A/B 材料与临时库 id |
compare_ab.py / ck_rechunk.py | 临时库块级对比、重切验证 |
db-raw.txt / doc-full.json | 从插件 API 取回的库内 rawText 与全量块(用于比对) |
凭据提醒(上轮遗留未处理):GET /knowledge/config与GET /knowledge/bases会把嵌入/重排/图注/mineru 的 key 明文返回;本机环回口暂无鉴权。本轮未把任何 key 写入文件或回复。