news 2026/10/8 6:31:29

浏览器插件+Ponytail:网页正文提取与AI摘要的实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
浏览器插件+Ponytail:网页正文提取与AI摘要的实战指南

1. 什么是 ponytail 插件,我为什么要折腾它

如果你经常需要“把一篇文章快速提炼成要点”,或者“把网页正文干净地抓下来丢给 AI 整理”,你多半经历过同样的一连串麻烦:先手动复制,再去掉广告、推荐位、评论区,最后还要应付各种乱七八糟的格式。时间长了你就会发现,真正有信息量的内容可能只有三分之一,剩下全是噪音。

我做的这个小工具叫 ponytail,定位就是一个“网页内容整理插件 + 技能包”。它的名字很直白——马尾辫。散落的头发收拢成一条干净利落的马尾,就像把网页里散乱的各种噪音收拢成一段清爽的正文。不管你是普通用户、内容创作者,还是经常折腾个人 AI 工作流的人,只要日常需要大量阅读网页并提炼信息,这个工具都能帮你省下不少重复劳动。

我把它拆成了两层:一层是可安装的浏览器插件,负责在页面里抓正文、清理杂质;另一层是 skill,也就是技能包,负责把清理完的内容进一步“上脑”——比如调用大模型做摘要、生成日报、翻译成本地语言。两个东西搭配使用,才能发挥最大价值。如果你只需要其中某一个部分,也可以单独拿着用,互不干扰。

这篇文章我会从设计思路、安装配置、核心逻辑、踩坑记录一直讲到后续扩展方向,全部基于我自己的实际使用经验。里面涉及代码的部分我会尽量贴出可运行的片段,不是那种只给个伪代码就把你打发的教程。你甚至可以照着抄一段就立刻跑起来。

2. 整体设计与核心思路拆解

2.1 为什么要把“抓取”和“理解”拆成两层

动手之前我参考过不少“一键摘要”类工具,发现它们的通病是:把页面抓取、文本提取、模型调用全部揉在一个脚本里。看着省事,可一旦哪一层出了问题,排查起来特别痛苦。比如页面改版了,你可能要跑到很深的代码里去改 CSS 选择器;想换一个模型,又要动编辑器逻辑。两个不相干的事被绑在一起,非常不利于维护。

所以我干脆把流程拆成两个独立模块:

  • 插件层(collect):只负责“把乱七八糟的网页变成干净的正文”。它不关心你后面用谁来总结、总结成什么样。
  • 技能层(skill):只负责“把干净的正文交给模型处理”。它不关心正文是从哪个网站、哪种页面结构里来的。

这样做的好处很直接:页面抓取变差了,我就只修插件层;模型换成新的版本或者想换摘要风格,我就只调 skill 层。两边互不牵连,出问题的范围一下就缩小了。而且抓下来的 Markdown 纯文本可以直接粘贴到任何笔记软件、聊天窗口或者本地 AI 工具里,哪怕后面不想用模型,也完全不浪费。

2.2 为什么选浏览器插件而不是后端爬虫

最早的原型我其实是用 Python 写的爬虫,直接 HTTP 请求页面再解析 HTML。听起来技术感很强,但实际用起来有致命伤:很多页面是动态渲染的,正文内容靠 JavaScript 加载,你直接抓到的只是一堆空壳和初始化代码。登录态、个性化推荐也需要处理,光 cookie 和会话维护这一块就足以让人头大。

浏览器插件没有任何这个问题。它跑在真实浏览器环境里,DOM 已经渲染完毕,登录态天然就有,浏览器自己也处理了编码和跳转。插件的 content script 可以拿到你当前看到的那个页面内容,所见即所得,这是任何后端爬虫都不可能替代的优势。

代价是插件不能用 Node.js,也没法直接跑 Python 的 BeautifulSoup。但好在浏览器的 DOM API 已经足够强大,配合一些简单的文档密度算法,实现一个轻量版正文提取器并不难。

2.3 核心组件的技术选型逻辑

整个项目实际上只有三块核心,我做的技术选型也尽量遵循“能不引入依赖就不引入”的原则:

第一个是正文提取算法。我没有用商业方案,也没有引那种动辄几百 KB 的第三方库,而是自己实现了一个简化版 Readability:先遍历页面主要区域的段落容器,计算候选节点的文本密度,再把分低的和明显是导航、广告的节点剔除。大概两百行 JavaScript,效果已经能覆盖绝大多数资讯类、博客类页面。

第二个是文本转换。从 DOM 里取出来的是 HTML,要变成对 AI 友好的纯文本或 Markdown。这一块不需要复杂库,核心就是递归遍历节点,遇到标题、段落、列表、引用就按规则加对应的 Markdown 符号。注意不要直接拿 innerHTML 去正则替换,因为嵌套标签和转义会让你的输出变成灾难。

第三个是模型调用。技能层我用的是对命令行和脚本都比较友好的方式,直接通过 HTTP 接口把清理好的文本送给模型服务,返回结果再落回本地文件。接口地址、模型名、token 上限全部做成配置项。这样今天你用一个模型,明天换另一个模型,改配置就行,代码不用碰。

3. 插件安装与核心实战

3.1 准备工作:先把环境弄干净

安装 ponytail 之前,建议先把浏览器里的其他“阅读模式”“广告拦截”类扩展暂时停用。我踩过一次大坑:开了几个广告拦截插件之后,正文区域有大量元素被动态替换,导致我的正文提取器总是抓到半截文章。排查了一个小时才发现是插件冲突。

其次,如果你要在 skill 层调大模型接口,注意确认网络环境和接口配置。这一步属于常规准备工作。日常使用不需要额外配置账号等复杂内容,插件本身的安装和加载流程非常简单。

3.2 浏览器插件安装的三种方式

Chrome 和 Edge 都支持“开发者模式加载已解压的扩展程序”。具体路径是:地址栏输入chrome://extensions或edge://extensions,打开右上角“开发者模式”开关,然后选择“加载已解压的扩展程序”,指向项目里plugin/目录即可。

如果你偏好从命令行安装,可以用:

# 先把插件目录压缩成 zip zip -r ponytail-plugin.zip plugin/ # 在支持的命令行浏览器环境下,通过策略安装或手动导入 # 这一步不同系统差别较大,实际使用还是建议直接拖拽加载

还有一种更简单的用法:在浏览器地址栏直接打开chrome://extensions后,把plugin/文件夹拖进窗口,也会自动触发安装。整个过程五秒钟。

安装完成后,地址栏旁边会出现一个小图标。点开之后你会看到两个主要功能区:一个负责“立即整理当前页面”,另一个负责“整理后交给 skill 继续处理”。如果当前页面的正文提取成功,它会直接预览在弹窗里,方便你随时检查抓取质量。

3.3 核心配置项说明

我的理念是“默认配置能跑,进阶配置保手”。首次安装不需要任何配置就能用,因为插件内置的默认值适用于大多数资讯页面。下面是几个比较关键的配置项,它们都在插件的设置面板里可以改。

配置项默认值作用说明
max_length3000整理后的正文最长字符数,超过则从段落末尾截断,防止给模型塞太多无用内容
keep_linksfalse是否保留 Markdown 链接。总结场景建议关闭,原文存档场景建议打开
selector留空手动指定正文区域选择器,比如article.main。留空时自动检测
output_formatmarkdown输出格式,支持markdown、plaintext
model_url留空skill 层调用的模型接口地址
model_name留空模型名称,用于请求体携带参数
temperature0.3模型生成温度,摘要任务越低越好,防止自由发挥

手动指定selector是一个容易被忽略但是非常实用的功能。某些网站结构特殊,自动检测算法找不到正文,你只要打开开发者工具,找到正文对应的 CSS 路径,然后在配置里填上这个选择器,ponytail 就会优先使用你指定的区域,不再盲猜。

3.4 skill 层怎么接入你的个人流程

skill 层本质上是一个可以被命令行或自动化流程调用的模块。最简单的用法是把它注册成一个本地的可执行命令:

export PONYTAIL_URL="https://example.com/article" export PONYTAIL_MODEL="qwen-plus" python pony_tail_skill.py --input "$PONYTAIL_URL" --output summary.md

运行之后,它会先调用浏览器插件把网页整理成 Markdown 文件,然后拿着这个文件请求你配置的模型接口,最后把摘要写入summary.md。

如果你用的是带函数调用的模型,也可以把 skill 封装成一个函数。调用时模型会自己决定“这个请求需要用到 ponytail”,然后传入网页地址,拿到整理结果后再基于它回答用户问题。这一套非常适合接进个人知识库、文档助手或者简单的阅读代理里。

4. 实操过程与关键代码逻辑

4.1 页面主内容的提取逻辑

前面说了,我采用了一个简化版 Readability 算法。核心思路是先找到页面里“文字密度”最高的容器,再围绕这个容器向上向下合并区块。下面是一段可以直接放进 content script 用的简化实现:

function getMainContent(root = document) { const candidates = root.querySelectorAll( 'article, [role="main"], .post-content, .entry-content, .article-content' ); let best = null; let bestScore = -Infinity; candidates.forEach((node) => { const text = node.innerText || ''; const length = text.trim().length; // 基本密度分:文本越长分数越高,同时惩罚带有大量链接的节点 const linkDensity = node.querySelectorAll('a').length / Math.max(node.querySelectorAll('p, div').length, 1); const score = length - linkDensity * length * 0.2; if (score > bestScore) { bestScore = score; best = node; } }); return best || root.body; }

这段代码的思路很简单:优先从语义化标签里找候选区,然后用“纯文字长度减掉链接噪音”作为打分标准。实际用起来你会发现,博客、新闻、帮助文档这类页面基本都能被准确命中,速度也很快。

拿到最佳候选节点之后,还要做一层清理:把藏在内部的script、style、noscript、svg和广告相关节点直接移除。注意移除前先存一份原始 HTML,万一操作失误还能还原。

4.2 HTML 转 Markdown 的细节处理

HTML 转 Markdown 是很容易被低估的一步。很多人用正则一替换就完事,结果列表全变成一团乱麻。稳定起见,我建议用递归遍历的方式,逐个节点判断类型再拼接文本,下面是核心片段:

function nodeToMarkdown(node) { if (node.nodeType === Node.TEXT_NODE) return node.textContent; const tag = node.tagName?.toLowerCase(); let text = ''; node.childNodes.forEach((child) => { text += nodeToMarkdown(child); }); if (tag === 'h1') return `\n# ${text.trim()}\n`; if (tag === 'h2') return `\n## ${text.trim()}\n`; if (tag === 'h3') return `\n### ${text.trim()}\n`; if (tag === 'p') return `${text.trim()}\n\n`; if (tag === 'li') return `- ${text.trim()}\n`; if (tag === 'blockquote') return `> ${text.trim()}\n`; if (tag === 'a') { const href = node.getAttribute('href') || ''; return keepLinksEnabled ? `[${text.trim()}](${href})` : text.trim(); } return text; }

这里你必须留意两点。第一,标题下面我记得加空行,不然渲染的时候会粘连成一行;第二,列表嵌套的情况要比我想象中多,如果遇到二级无序列表,最好根据父级ul的层级自动缩进两格。这个可以在递归参数里加一个 depth,然后生成前缀空格。

清理完的文本我会统一做一次空行合并,把连续三个以上的换行压缩成两个。如果输出是纯文本,还要顺带把 Markdown 符号全部剥掉,只留内容本身。

4.3 调用模型生成摘要的参数选择

技能层调用模型时,很多人上来就问“温度调多少?max_tokens 调多少?”其实这取决于你的输入长度和需要的摘要精度。

我一般先统计整理后正文的字符数,再按这个数据推算输出长度:

content_len = len(markdown_content) # 目标摘要长度 = 输入长度的 30% 左右,取整百 target_tokens = max(200, int(content_len * 0.3 / 100) * 100) # 再留点余量给 prompt 和返回格式 max_tokens = min(8000, target_tokens + 200)

温度建议直接按 0.3 固定。摘要任务本质是压缩信息,不是创作。如果调高温度,模型会忍不住“润色”内容,一不小心就把原文里没有的意思加进去,这在事实核查场景里是致命的。如果你要做的是内容改写而不是摘要,才考虑把温度调到 0.7 左右。

请求体注意携带stream: false,实现更简单。响应返回 JSON 之后,直接解析 content 字段落盘。如果你用的是新模型,记得确认一下它的上下文窗口能不能装下你整篇正文,装不下就先在插件层把max_length调小。

4.4 输出与导出:怎么接笔记软件和日报流程

我不想让 ponytail 只停留在控制台输出,所以多写了一个导出模块。它支持三种输出目标:

  • 本地 Markdown 文件:最通用,适合丢进 Obsidian、Notion 或者其他笔记工具。
  • 剪贴板:适合快速粘贴到聊天窗口或文档里,省去文件管理。
  • 追加写入指定日志:适合做“每日阅读摘要”流程。每天固定把当天整理过的内容追加到同一个文件里,晚上直接拿出来归档。

导出模块的配置同样放在同一个配置文件里,切换目标只需要改一个参数。我个人最常用的组合是“清理后导出到剪贴板”,因为很多时候我并不会立刻让模型参与,而是先把原文存下来,等有空再统一处理。

5. 常见问题与排查技巧实录

5.1 为什么抓不到正文,或者抓到了侧边栏

这个问题我修过的次数最多,原因基本逃不开下面几个。

第一,页面是动态加载的,正文区域在滚动之后才渲染出来。我的解决办法是在开始提取前,先自动滚动页面到底部再回顶部,强制触发懒加载。这个动作在提取逻辑里加一个await new Promise(r => setTimeout(r, 800))就行。

第二,网站是单页应用(SPA),路由切换过程中 DOM 会大换血。插件需要监听history.pushState事件,在路由变化后重新触发提取,否则你只会拿到上个页面的残留内容。

第三,正文标题和正文内容分离两个容器。这点容易漏,很多页面把标题放在header里,正文在section里,如果你只取一个候选节点,标题就丢了。我后来在提取结束之后会额外补一次:在当前页面里查找h1,如果标题文本没有包含在正文里,就手动拼到正文最前面。

5.2 中文内容乱码或输出字数对不上

现代浏览器基本上不会真的乱码,除非页面本身用的是<meta charset="gb2312">这种老编码。不过插件拿到的是渲染后的 DOM,编码已经被浏览器解码过了,所以插件层通常没有乱码问题。乱码更容易出现在你“手动复制到某些老终端工具”这一步。

字数对不上主要有两个原因:一个是文本节点里包含大量空白字符和换行,统计时没过滤干净;另一个是遇到 emoji 和特殊符号,JavaScript 的length按 UTF-16 码元计算,一个 emoji 可能是两个码元。统一处理办法是统计前做text.replace(/\s+/g, ' ').replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, '_'),先把不可见字符清理掉再算长度。

5.3 模型接口报错或返回超时

技能层调模型接口时最常见的报错是超时和 JSON 解析失败。超时多半不是网络本身的问题,而是你输入文本太长,模型处理时间超过了接口默认的 60 秒阈值。解决办法有两个:把max_length调小,或者在插件配置里把超时时间从 60 秒提到 180 秒。

JSON 解析失败通常是因为模型返回的内容里夹带了 Markdown 代码块标记。有的模型会习惯性在摘要外面包一层 ```json,导致你直接json.loads失败。解析前先做一次清理:

import re text = re.sub(r"^```json\s*|\s*```$", "", response_text.strip()) data = json.loads(text)

这个坑非常隐蔽,因为大多数时候不会报错,只有在你换了某个特定模型之后才频繁出现。如果你不想依赖这个清理逻辑,也可以在 prompt 里明确强调“不要输出 Markdown 代码块标记,直接输出纯 JSON”,能显著降低概率。

5.4 浏览器更新或版本差异导致插件失效

Chrome 更新之后插件偶尔会失效,尤其是每次大版本升级,Manifest V3 的一些 API 行为会有细微调整。最典型的例子是scripting.executeScript的权限要求比 Manifest V2 严格得多,如果你把 content script 改成动态注入的方式,必须确认host_permissions里包含了目标站点。

我建议每次浏览器提示升级时,先不要急着更新 ponytail。等插件失效了你再升级,或者升级后第一时间遍历一遍核心页面测试“整理当前页面”功能。浏览器升级带来的兼容性问题,90% 和扩展权限声明有关,重新检查manifest.json基本就能定位到问题。

另外,同一个插件在 Chrome 和 Edge 上虽然可以共用,但如果你开了严格隐私模式,第三方 cookie 和存储权限会不一样,可能导致你的配置项在其中一个浏览器上“消失了”。解决方法是把配置文件放在插件自身的chrome.storage.local里,而不是依赖页面级的 localStorage。

5.5 把 ponytail 接进 AI 工作流时的注意事项

很多人拿到工具第一反应是“我要拿它做一个全自动阅读机器人”,我劝你不要一开始就全自动。建议先手动跑通三个场景:单个网址整理、单页摘要、批量文件导出。

在这三个场景都稳定之后,再接进自动化流程。因为自动化的难点不在于调用,而在于异常处理:网页结构变了怎么办、模型接口限流怎么办、输出文件冲突了怎么办。每个问题都要有一套预案,否则自动化跑起来十分钟就断,你还要半夜爬起来看日志。

另一个容易被忽视的点是 token 成本。跑批量摘要时不控制输入长度,费用会非常夸张。我的经验是批量任务里把正文裁剪到 1000 到 1500 字,摘要长度控制在 200 字以内,信息保全率已经能到八成。为了多保那两成信息去付出三四倍的 token 开销,不划算。

6. 一些让我印象深刻的小技巧和后续扩展想法

6.1 先说几个常规文档里不会写的细节

第一个技巧是“摘要质量不好,先别急着换模型,先看正文清理干不干净”。我遇到过很多次模型发挥不稳定,排查到最后发现是正文里混了一堆推荐位文字,模型被带偏了。把清理阈值调严之后,同样的模型、同样的 prompt,效果立刻变好。数据干净比模型强重要得多。

第二个技巧是“正文提取时优先选短路径选择器”。如果自动提取总是有问题,你手动指定的选择器也不要写太长。选择器越长,页面改版的概率就越大。比如#main .article-content就比body > div#wrapper > main > section > div.article-content稳定得多。能用类名解决就不要用层级路径。

第三个技巧是“让 skill 层默认附带原文标题和来源链接”。不要只把正文丢给模型,而是在开头拼上来源:{url}和标题:{title}。这样模型在总结时会自动带上来源意识,不会凭空默认所有信息都来自同一个人。对于做信息聚合的场景,这个细节能帮你省很多后面核对的功夫。

6.2 这个工具还能往哪些方向继续折腾

目前我计划里排在第一位的扩展是给它加一个“整站配置记忆”能力。大概思路是这样:每当你手动指定过某个网站的正文选择器,插件就把这个配置记下来,下次访问同一个域名时自动应用。跑一段时间之后,你常用网站的正确选择器会被逐渐积累出来,以后基本就不需要手动干预了,这个思路在信息流整理场景里特别实用。

第二个方向是接入本地模型。跟远程接口相比,本地模型的好处是隐私和零成本,缺点是速度和效果参差不齐。你完全可以保留现在这套远程接口,只是把model_url从云端地址改成本地地址,剩下的逻辑几乎不用动。接口协议只要保持 OpenAI 风格,兼容性就能拉到最高。

第三个方向是多页面合并整理。现在的逻辑是“每页一条摘要”,但实际场景里你经常需要搜集某个话题的多篇文章,然后合并成一篇综述。我想要的是:先批量整理二十个相关页面,然后把二十份正文拼成一个临时文档,再一次性让模型输出一篇综述。这个功能非常适合做行业调研和竞品分析,省下来的时间非常可观。

坦白说,当初做 ponytail 只是被“手动复制粘贴网页内容”这件事烦得够呛。做到现在反而发现,真正让效率起飞的不是哪一段特别神奇的代码,而是你愿意为“整理与理解”这条链路多花一点心思。如果你也在被类似的问题困扰,完全可以依照我上面写的那几个核心逻辑,从最简陋的版本开始搭起。工具这东西,自己动手改过一遍,用起来才真正顺手。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/8 6:31:26

进阶篇11:重构OpenCode请求管线与中间件链,把endpoint改到TaoToken

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/8 6:31:25

PLC智能跑步机控制系统设计:多变量耦合与工程落地实践

1. 这不是“套模板”的毕业设计&#xff0c;而是一次真实的工业控制闭环实践“基于PLC的智能跑步机控制系统设计”——光看标题&#xff0c;很多人第一反应是&#xff1a;又一个用西门子S7-1200博途TIA Portal搭个启停按钮、加个变频器调速、再接个HMI显示速度的“标准答案式”…

作者头像 李华
网站建设 2026/10/8 6:31:24

SVN(2)-可视化操作工具:用TaoToken统一Key打通提交与回滚流程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/8 6:31:12

用云开发构建微信小程序点餐系统:从环境初始化到订单闭环

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/8 6:30:39

ROS2+SLAM+Nav2全链路实战:从Gazebo仿真到实机部署的避坑指南

1. 从一台扫地机说起&#xff1a;为什么我要跑通这条全链路去年年底我接手了一个小项目&#xff0c;需求说起来很简单&#xff1a;让一台差速轮式机器人&#xff08;底盘结构跟主流扫地机几乎一样&#xff09;在未知的室内环境里自己跑起来&#xff0c;先建图&#xff0c;再基于…

作者头像 李华
网站建设 2026/10/8 6:30:35

U-Boot移植实战笔记:从最小系统点亮到内核引导

搞嵌入式的&#xff0c;谁没被U-Boot劝退过一次呢&#xff1f;我说的不是那满屏的寄存器配置&#xff0c;也不是看起来永远对不上的内存地址&#xff0c;而是明明照着参考板抄了一遍&#xff0c;上电后串口却死活不吐字的那种挫败感。U-Boot移植不是照着手册敲几条命令就能完事…

作者头像 李华