资讯动态

Defuddle 脚注提取实战:嵌套 prose 容器中的 Loose Footnotes 识别与标准化

发布时间:2026/10/16 12:55:33 来源:尧图企业网站定制
后端【免费下载链接】defuddleGet the main content of any page as Markdown.项目地址https://gitcode.com/gh_mirrors/de/defuddle点击查看免费下载导读本文以 Defuddle 仓库中的 footnotes--nested-prose 测试夹具 为切入点深入讲解 Defuddle将任意网页正文提取为 Markdown 的库如何应对一种棘手的脚注形态松散脚注loose footnotes——正文中没有a href#fn1锚点链接只有sup1/sup之类的数字上标脚注定义则是正文末尾一串带supN/sup前缀的编号段落且整个内容被包裹在多层嵌套的布局 div如div classlayoutdiv classprose之下。读完本文你将掌握 Defuddle 的脚注检测管线、三种启发式扫描策略、交叉验证机制以及从 HTML 到[^1]风格 Markdown 脚注的完整转换链路。一、问题背景什么是 Loose Footnotes在绝大多数渲染引擎中脚注由三部分构成正文中的引用标记通常是一个指向定义锚点的a href#fnN、定义列表ol/section中的定义项、以及两者的ID 配对。Defuddle 通过 FOOTNOTE_LIST_SELECTORS覆盖div.footnotes ol、ol.footnotes、div[roledoc-endnotes]、Wikipedia 的ol.references等和 FOOTNOTE_INLINE_REFERENCES覆盖sup.reference、a[href^#fn]、sup.footnoteref等可以按部就班地处理这些结构化脚注。但存在一类没有锚点、没有 ID、甚至没有脚注容器的松散形态pThe first claim is well-established.sup1/sup/p hr/ psup1/sup Supporting evidence for the first claim./p正文用sup1/sup引用定义用psup1/sup …编号两者之间仅靠一个hr/划分没有任何 href 关联。这类页面往往由轻量静态站点生成器或手写 HTML 产生。Defuddle 中负责兜底处理这类场景的正是FootnoteHandler.tryLooseFootnotes及其底层扫描器findLooseFootnoteParagraphs见 src/elements/footnotes.ts 与 src/elements/footnotes.ts。二、测试夹具剖析嵌套布局下的真实 HTML 结构footnotes--nested-prose.html 完整复现了该模式其结构有两个关键特征article div classlayout div classprose h1Loose footnotes: nested prose div/h1 pThe first claim is well-established.sup1/sup/p pThe second claim is more contested.sup2/sup/p pThe conclusion follows from both claims./p hr/ psup1/sup Supporting evidence for the first claim./p psup2/sup Counterarguments and rebuttal for the second claim./p /div /div /article特征一内容元素不是段落而是结构性包裹 div。article的直接子元素是div classlayout实际内容含脚注位于深层嵌套的div classprose中。夹具注释明确说明Defuddle 选中的内容元素是article因此collectFootnotes必须先通过lastP.parentElement定位到最内层容器再在其上扫描。特征二脚注是松散的。定义项是hr/之后的两个psupN/sup…/p编号段落正文引用是supN/sup纯数字上标——二者之间没有任何 ID 或 href 关联只有crossValidate的数字交叉匹配可以作为判据。三、检测原理findLooseFootnoteParagraphs 的三重扫描策略findLooseFootnoteParagraphs的入口有一个针对嵌套布局的关键设计src/elements/footnotes.ts// Use parent of last p as scan container for nested layouts (e.g. Next.js) const allPs Array.from(element.querySelectorAll(p)) as any[]; const container allPs.length 0 ? (allPs[allPs.length - 1].parentElement ?? element) : element;它不直接以内容元素为扫描容器而是取最后一个p的父元素作为容器。在article div.layout div.prose这种嵌套结构下最后一个段落的父元素正是div classprose从而把扫描范围收敛到真正的内容容器避开外层布局 div 的干扰。这一技巧在注释中被点明是专门为 Next.js 等框架的嵌套布局准备的。随后在该容器内依次尝试三种策略策略 1最后一个hr/之后的顺序扫描从容器末尾向前找到最后一个hr/src/elements/footnotes.ts对其后每个兄弟节点调用parseFootnoteNum解析编号收集全部编号段落for (let j i 1; j children.length; j) { const num this.parseFootnoteNum(children[j]); if (num ! null) paragraphs.push({ num, el: children[j] }); } if (paragraphs.length 2 this.crossValidate(element, paragraphs)) { return { paragraphs, toRemove: children.slice(i) }; } break; // only check the last hr注意break——只检查最后一个hr/因为脚注区通常紧贴文章末尾的唯一分隔线。策略 2末尾编号段落的逆向扫描若没有hr/则从容器末尾逆向累积编号段落src/elements/footnotes.tsif (tag p) { const num this.parseFootnoteNum(child); if (num ! null) { trailingNumbered.unshift({ num, el: child }); firstFootnoteIdx i; continue; } break; // non-numbered paragraph — stop } if (tag ul || tag ol || tag blockquote) continue; break; // any other element (heading, div, ...) — stop一旦遇到非编号段落或非ul/ol/blockquote的元素立即停止。若凑齐 ≥2 个编号段落并通过交叉验证还会检查其前一个兄弟节点是否为FOOTNOTE_SECTION_RE/^(foot\s*notes?|end\s*notes?|notes?|references?)$/i定义于 src/elements/footnotes.ts匹配的标题如 Footnotes一并纳入待移除集合。策略 3后半段中的散落编号段落策略 3 用于兜底两种场景脚注之后还有非脚注的尾随内容或脚注与最后一个p不在同一容器src/elements/footnotes.ts。它取全部段落的中点Math.floor(allPs.length / 2)在后半段中收集编号段落同样要求 ≥2 个且通过交叉验证。四、判据的严格性parseFootnoteNum 与 crossValidate松散脚注检测必须避免把普通编号段落误判为脚注因此两个判据都相当严格。编号解析parseFootnoteNumparseFootnoteNum 要求编号标记必须是段落第一个子元素前面不能有文本节点且只能是sup或strong兼容bsup包裹见isBoldWrappedSup其文本必须能严格解析为 ≥1 的整数String(num) numText拒绝 1. 这类带后缀的文本。交叉验证crossValidatecrossValidate 在整个内容元素上扫描所有sup统计其数字文本与已收集编号集合的交集要求至少匹配 2 个才判定为脚注if (!isNaN(n) n 1 String(n) text numberedNums.has(n)) { matchedNums.add(n); } return matchedNums.size 2;至少 2 个的门槛有效防止了单个巧合编号如正文里恰好有个孤立的sup1/sup触发误判。这正是本夹具中正文sup1/sup、sup2/sup与定义段落编号 1、2 相互印证、从而被可靠识别的原因。五、收集与清理stripMarkerAndWrap 与待移除集合通过验证后tryLooseFootnotes 逐个处理定义段落用stripMarkerAndWrapsrc/elements/footnotes.ts克隆段落、剥掉开头的supN/sup标记、修剪前导空白再包进一个div将当前定义与其后、直到下一个编号定义为止的所有兄弟节点并入同一脚注内容保持多段落脚注的完整性通过addFootnote以数字字符串为 ID 注册state.count从 1 起按序分配见 addFootnote把hr之后的所有节点children.slice(i)加入pendingRemovals等待后续统一移除。最终在collectFootnotes中tryLooseFootnotes作为第 6 个 fallback 参与兜底链src/elements/footnotes.ts。注意兜底链的执行前提是state.count 1即前面的结构化选择器路径FOOTNOTE_LIST_SELECTORS没有收集到任何脚注——松散形态的兜底绝不会与结构化脚注重复处理。六、标准化输出重建 #footnotes 列表收集完成后standardizeFootnotessrc/elements/footnotes.ts负责统一重建输出结构替换正文引用按 FOOTNOTE_INLINE_REFERENCES 匹配正文中的引用元素纯数字supN/sup虽不在该选择器列表内但会落入 src/elements/footnotes.ts 的兜底 Pass 2按FOOTNOTE_MARKER_RE匹配sup的数字文本替换为规范化的sup idfnref:Na href#fn:NN/a/supcreateFootnoteReferencesrc/elements/footnotes.ts生成定义列表新建div idfootnotesol每个脚注通过createFootnoteItemsrc/elements/footnotes.ts变成li classfootnote idfn:N内容按有无段落/块级子元素选择包p或直接挂块级节点最后在末尾追加↩返回链接footnote-backref清理源 DOM移除所有FOOTNOTE_LIST_SELECTORS命中的列表和pendingRemovals中的节点随后调用removeOrphanedDividerssrc/standardize.ts清除孤立的hr/——这正是本夹具中hr/不会残留在 Markdown 里的原因。整个standardizeFootnotes在 Defuddle 主流程中于 src/defuddle.ts 被调用options.standardize为 true 时位于外部脚注采纳adoptExternalFootnotes之后、隐藏元素移除之前。七、Markdown 转换sup 引用与定义列表的 turndown 规则标准化后的 DOM 由 src/markdown.ts 中的 turndown 规则转换为 Markdowncitations规则src/markdown.ts将sup idfnref:N转换为[^N]并解析#fn:Nhref 获取真实编号footnotesList规则src/markdown.ts将#footnotes ol的每个li转换为[^N]: 内容定义行剥离开头与编号相同的sup标记并清理尾部↩︎返回链接removals规则src/markdown.ts删除内容中残留的#fnref返回链接和footnote-backref。八、预期输出解读从 HTML 夹具到最终 Markdownfootnotes--nested-prose.md 展示了经过完整管线提取 → 脚注标准化 → Markdown 转换后的最终结果。文件头部是 JSON 元数据前导块title/author/site/published正文为The first claim is well-established.[^1] The second claim is more contested.[^2] The conclusion follows from both claims. [^1]: Supporting evidence for the first claim. [^2]: Counterarguments and rebuttal for the second claim.对比夹具 HTML 可以确认正文中的sup1/sup、sup2/sup分别变成了[^1]、[^2]脚注定义段落的supN/sup前缀与hr分隔线被剥离内容进入定义行定义行按编号升序排列与正文引用一一对应h1被提升为 frontmatter 的titleLoose footnotes: nested prose div不再出现在正文中。九、测试机制夹具自动发现与基线比对该夹具的预期结果由 tests/fixtures.test.ts 自动驱动测试套件扫描 tests/fixtures 目录下所有.html文件用Defuddle(doc, url, { separateMarkdown: true, fetch: offlineFetch })处理offlineFetch 强制禁用网络保证确定性将结果与 tests/expected 中对应的.md文件比对。若期望文件不存在则生成基线若存在则以规范化后的 Markdown 严格相等校验。这意味着footnotes--nested-prose这套嵌套 prose div 松散脚注的场景被固化为回归测试任何后续改动若破坏该检测路径都会立即暴露。十、小结松散脚注处理的关键设计要点回顾整个流程Defuddle 对footnotes--nested-prose这类页面的处理可以归纳为四个可复用的设计原则容器下钻用最后一个段落父元素而非内容元素本身作为扫描容器化解嵌套布局问题多策略兜底hr后扫描、末尾逆向扫描、后半段散落扫描三管齐下覆盖不同排版双重校验parseFootnoteNum的严格格式约束 crossValidate的 ≥2 数字交叉匹配杜绝误判统一重建无论来源形态如何最终都收敛到#footnotes ol的标准结构再由 turndown 规则无损转换为[^N]风格 Markdown。对于希望在 Defuddle 基础上理解脚注提取机制、或为类似场景编写提取逻辑的开发者建议顺着 src/elements/footnotes.ts → src/constants.ts → src/markdown.ts 这条链路阅读并结合 tests/fixtures 与 tests/expected 中数十个脚注场景夹具如footnotes--named-anchor、footnotes--hr-sup-numbered、footnotes--wikipedia-references逐一验证各分支行为。赞分享后端【免费下载链接】defuddleGet the main content of any page as Markdown.项目地址https://gitcode.com/gh_mirrors/de/defuddle点击查看免费下载相关推荐defuddle 脚注提取span fnref:id 模式Blogger/righto.com的识别、标准化与 Markdown 输出全解析defuddle 脚注提取span fnref:id 模式Blogger/righto.com的识别、标准化与 Markdown 输出全解析 本文以 de后端Defuddle 脚注标准化实战named-anchor 风格脚注如何变成标准 Markdown 脚注Defuddle 脚注标准化实战named anchor 风格脚注如何变成标准 Markdown 脚注 本文以 Defuddle 仓库中的回归测试用例 foo后端defuddle 脚注标准化实战从 maggieappleton.com 的 Tufte 侧注 HTML 到干净 Markdowndefuddle 脚注标准化实战从 maggieappleton.com 的 Tufte 侧注 HTML 到干净 Markdown 导读 本文以 defudd后端创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑