defuddle 如何提取 ChatGPT 分享页:post-Thought 内容合并的夹具设计与实现解析

发布时间:2026/10/15 23:16:35
defuddle 如何提取 ChatGPT 分享页:post-Thought 内容合并的夹具设计与实现解析 后端【免费下载链接】defuddleGet the main content of any page as Markdown.项目地址https://gitcode.com/gh_mirrors/de/defuddle点击查看免费下载defuddle 是一个把任意网页正文提取为 Markdown 的解析库。在对话类网站中ChatGPT 分享页chatgpt.com/share/...是最特殊的场景之一一个 assistant 回合可能被 Thought思考过程区块拆分成前后两段正文且思考过程本身不应出现在提取结果里。本篇以仓库中的测试夹具tests/expected/extractor--chatgpt-post-thought-content.md及其输入tests/fixtures/extractor--chatgpt-post-thought-content.html为线索逐层剖析ChatGPTExtractor的识别逻辑、消息合并策略、Thought 区块清除机制以及 fixtures/expected 测试体系的工作方式。读完本文你将掌握 defuddle 对话类提取器的设计思路并能在自己的夹具上验证与复现同样的行为。一、场景还原post-Thought 内容到底是什么先看输入夹具tests/fixtures/extractor--chatgpt-post-thought-content.html。它模拟了一个最典型的 ChatGPT 分享页片段用户提问后assistant 先展示一段思考过程页面上表现为一个可折叠的按钮内部文本形如 Thought for 12s随后再输出正式回答。关键 DOM 结构如下div>{ title: ChatGPT post-Thought content fixture, author: , site: ChatGPT, published: }**You said** Please help me plan a simple weekend picnic. --- **ChatGPT said** Start with a simple checklist before choosing the location. Pick a nearby park, check the weather, and choose food that travels well. Bring water, napkins, a blanket, and a small bag for cleanup.逐项解读这份期望片段来源说明JSON 前导测试框架生成元数据便于人工比对与审查作者字段留空分享页通常不提供作者**You said**strong加粗的作者标题来自 sr-only 标题去掉尾部冒号用户消息.whitespace-pre-wrap内容原文照搬---消息间分隔符hr由ConversationExtractor在消息之间插入**ChatGPT said**第二个回合的 sr-only 标题同一 author 文本三段正文思考前后两个.markdown块关键Start with...思考前与Pick a nearby park...、Bring water...思考后被合并为一个 assistant 消息顺序不变无 Thought 文本span[data-stateclosed]被完整移除不进入任何输出可见该夹具的验收重点不是能不能提取而是内容完整且次序正确、思考过程被干净剔除。三、源码实现ChatGPTExtractor 的识别与合并逻辑3.1 识别canExtract()与注册规则ChatGPTExtractor继承自ConversationExtractor见src/extractors/_base.ts体系。其识别逻辑极简——页面中存在conversation-turn-前缀的测试锚点即可// src/extractors/chatgpt.ts constructor(document, url, schemaOrgData?, options?) { super(document, url, schemaOrgData, options); this.turns document.querySelectorAll([data-testid^conversation-turn-]); ... } canExtract(): boolean { return !!this.turns this.turns.length 0; }在src/extractor-registry.ts第 111–116 行中该提取器按 URL 模式注册this.register({ patterns: [ /^https?:\/\/chatgpt\.com\/(c|share)\/.*/ ], extractor: ChatGPTExtractor });也就是说只有当页面 URL 命中chatgpt.com/c/...或chatgpt.com/share/...且 DOM 出现conversation-turn-*元素时才会走这条专用路径。3.2 回合内消息收集合并多片段extractMessages()是核心方法其处理链可拆解为以下几步源码位于src/extractors/chatgpt.ts第一步提取作者文本。在回合内查找h4.sr-only, h5.sr-only, h6.sr-only取其文本并去掉尾部冒号与空白const authorText authorElement?.textContent ?.trim() ?.replace(/:\s*$/, ) // Remove colon and any trailing whitespace || ;因此 ChatGPT said: 变成ChatGPT said最终渲染为**ChatGPT said**。第二步收集回合内所有消息元素。关键过滤条件是属于当前回合、而非嵌套在其他回合内const messageEls Array.from(turn.querySelectorAll([data-message-author-role])) .filter(el el.closest([data-testid^conversation-turn-]) turn);第三步取内容元素并序列化。助手方法getMessageContentElements()按.markdown, .whitespace-pre-wrap选择器查找内容容器并过滤掉嵌套重复项——只保留最顶层的内容容器避免同一内容被序列化两次private getMessageContentElements(messageEl: Element): Element[] { const contentSelector .markdown, .whitespace-pre-wrap; const candidates [ ...(messageEl.matches(contentSelector) ? [messageEl] : []), ...Array.from(messageEl.querySelectorAll(contentSelector)) ]; return candidates.filter(candidate !candidates.some(other other ! candidate other.contains(candidate)) ); }随后把每个内容容器用serializeHTML()见src/utils/dom.ts即读取innerHTML序列化并拼接const contentHtmlParts messageEls.flatMap(el { const contentElements this.getMessageContentElements(el); return contentElements.length 0 ? contentElements.map(contentEl serializeHTML(contentEl)) : [serializeHTML(el)]; }); let messageContent (contentHtmlParts.length 0 ? contentHtmlParts : [serializeHTML(turn)]).join(\n);正是这一步把思考前后的两个.markdown块按 DOM 顺序拼进了同一个消息字符串。第四步清理零宽字符与 sr-only 标题。先移除 ChatGPT 文本中的零宽空格\u200B再把内容里残留的h4/h5/h6.sr-only元素删掉避免作者标题重复出现在正文。3.3 清除 Thought 区块data-stateclosed在消息合并之后有一段专门针对 Thought 按钮的清理const cleanupDiv this.document.createElement(div); cleanupDiv.appendChild(parseHTML(this.document, messageContent)); cleanupDiv.querySelectorAll(span[data-stateclosed]).forEach(el el.remove()); messageContent serializeHTML(cleanupDiv);从实现看凡带data-stateclosed的 span即 Thought for 12s 按钮内部结构都会被整棵移除且发生在正文合并之后——所以思考前的内容Start with a simple checklist...不会因为按钮被删而丢失思考后的内容也能原样保留。随后还会做一次空段落清理/p[^]*\s*\/p/g替换为空避免合并过程产生孤立空标签。3.4 引用脚注顺带一提同一提取器还会把 ChatGPT 的网页引用形如target_blank relnoopener的 citation pill替换为脚注引用sup idfnref:Na href#fn:NN/a/sup并把 URL 汇总进footnotes列表。这与本夹具主题同源可对照阅读tests/expected/extractor--chatgpt-citations.md及其夹具tests/fixtures/extractor--chatgpt-citations.html观察效果。四、基类组装ConversationExtractor 如何产出 MarkdownChatGPTExtractor只负责把 DOM 整理成ConversationMessage[]结构定义见src/types/extractors.tsauthor、content、timestamp?、metadata.role真正的组装在基类src/extractors/_conversation.ts中完成。4.1createContentHtml()消息模板每条消息被包成带语义 class 的 div作者加粗、角色写入data-属性消息之间插入hrdiv classmessage message-${author.toLowerCase()}>赞分享后端【免费下载链接】defuddleGet the main content of any page as Markdown.项目地址https://gitcode.com/gh_mirrors/de/defuddle点击查看免费下载相关推荐defuddle 内容提取实战如何从带作者与分享控件的博客页生成干净 Markdowndefuddle 内容提取实战如何从带作者与分享控件的博客页生成干净 Markdown 本文以 defuddle 仓库中名为 author share wid后端defuddle 内容提取实战如何识别并保护标题引入列表不被误删defuddle 内容提取实战如何识别并保护标题引入列表不被误删 导读 本文以 defuddle 仓库中的 fixture 测试样例 content pa后端Defuddle 实战实时博客Live Blog页面的元数据提取与内容清理Defuddle 实战实时博客Live Blog页面的元数据提取与内容清理 实时博客Live Blog / 滚动新闻页是新闻网站典型的高难度提取场景后端创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询