Nanobrowser 源代码解析:Markdown 提取与 Readability——采集页面的两种管线

从源码看 Nanobrowser 内部用 turndown 和 Mozilla Readability 两条管线处理页面内容。代码如何注入、如何执行、两种管线分别适合什么爬虫场景。

亿牛云技术团队2026年4月9日5 分钟阅读

引言:一个浏览器扩展怎么做数据采集

Nanobrowser 是一个 Chrome 扩展——它跑在用户的浏览器里,通过 Puppeteer 的 CDP 控制当前标签页。当智能体需要读取页面上的信息时,它不是直接用 LLM 来解析原始 HTML,而是先通过内置的提取管线把页面转成 AI 更方便处理的格式。

这两种管线都实现在 browser/dom/service.ts 里,加起来不到 40 行核心代码。它们分别是 getMarkdownContentgetReadabilityContent

管线一:turn2Markdown(全量转换)

源码位置

browser/dom/service.ts:44-61

export async function getMarkdownContent(
  tabId: number, selector?: string
): Promise<string> {
  const results = await chrome.scripting.executeScript({
    target: { tabId: tabId },
    func: sel => {
      return window.turn2Markdown(sel);
    },
    args: [selector || ''],
  });
 
  const result = results[0]?.result;
  if (!result) {
    throw new Error('Failed to get markdown content');
  }
  return result as string;
}

函数通过 chrome.scripting.executeScript 注入到目标页面的 JavaScript 上下文中,调用 window.turn2Markdown(selector)

turn2Markdown 是 Nanobrowser 在扩展初始化时注入到页面全局作用域的函数。它基于 turndown 库——一个将 HTML 转为 Markdown 的成熟开源项目。

关键设计要点:

  1. 可选的 CSS 选择器——selector 参数允许只转换页面的某个部分(例如只提取 #product-description 区域的内容),而不是整个页面。这对聚焦采集特定区域的爬虫任务非常有用。

  2. 通过 chrome.scripting 调用——这不是普通的函数调用。chrome.scripting.executeScript 在目标页面的隔离世界中执行代码,返回序列化的结果。这意味着 turn2Markdown 函数必须在目标页面的全局作用域中可用。

  3. 错误处理简单粗暴——失败就抛异常,没有重试逻辑。

示意图

Nanobrowser 后台

  ├── chrome.scripting.executeScript({
  │     target: { tabId },
  │     func: (sel) => window.turn2Markdown(sel),
  │     args: ['#product-info']
  │   })


目标页面上下文

  ├── window.turn2Markdown('#product-info')
  │     │
  │     ├── 定位到 #product-info 元素
  │     ├── 获取 innerHTML
  │     ├── turndown 将 HTML → Markdown
  │     └── 返回 Markdown 字符串


返回给 Nanobrowser → LLM 消费

适用场景

场景适合不适合
全页面内容提取页面结构简单的文档页面包含大量无关内容
部分区域提取产品详情、文章正文有明确容器内容分散在多个区域
表格数据简单的表格复杂嵌套表格
混合内容图文混合的页面动态加载的内容

管线二:parserReadability(正文提取)

源码位置

browser/dom/service.ts:63-81

export interface ReadabilityResult {
  title: string;
  content: string;
  textContent: string;
  length: number;
  excerpt: string;
  byline: string;
  dir: string;
  siteName: string;
  lang: string;
  publishedTime: string;
}
 
export async function getReadabilityContent(
  tabId: number
): Promise<ReadabilityResult> {
  const results = await chrome.scripting.executeScript({
    target: { tabId },
    func: () => {
      return window.parserReadability();
    },
  });
  const result = results[0]?.result;
  if (!result) {
    throw new Error('Failed to get readability content');
  }
  return result as ReadabilityResult;
}

parserReadability 封装了 Mozilla 的 Readability 库——也就是 Firefox 的阅读模式背后使用的算法。它分析页面的 DOM 结构,找到"最可能是文章正文"的区域,剥离导航、侧边栏、广告等干扰元素。

ReadabilityResult 接口详解

字段类型说明爬虫用途
titlestring文章标题采集的文章标题
contentstring正文 HTML需要进一步处理的原始内容
textContentstring纯文本内容直接给 LLM 使用
lengthnumber内容长度估算 Token 数
excerptstring摘要快速内容预览
bylinestring作者文章元信息
siteNamestring站点名称来源标记
publishedTimestring发布时间时间维度
langstring语言选择处理模型
dirstring文字方向布局处理

Readability 的核心算法逻辑(简化)

Mozilla Readability 的判断逻辑大致如下:

1. 扫描页面中的所有 <p>, <pre>, <td> 等文本密集标签
2. 对每个候选容器,计算"内容得分":
   - 类名包含 "article", "post", "content" → 加分
   - 类名包含 "sidebar", "comment", "ad" → 减分
   - 元素含有 <p> 子元素 → 加分
   - 元素内链接密度过高 → 减分
3. 选择得分最高的候选容器
4. 清理容器内的无用元素(脚本、样式、广告)
5. 提取标题、作者、发布时间等元信息
6. 返回结构化的 ReadabilityResult

两种管线的对比

维度getMarkdownContentgetReadabilityContent
底层库turndownMozilla Readability
输出格式Markdown 字符串结构化对象(含 HTML + 纯文本 + 元信息)
内容筛选按 CSS 选择器区域算法自动识别正文主体
适合页面任何页面文章类页面(博客、新闻、文档)
不适合动态加载内容无正文结构的页面(首页、列表页)
元信息标题、作者、发布时间、站点名
提取范围选择器指定或全部全页自动扫描
Token 效率固定比例压缩高(只保留正文可减少 60-80% Token)

注入机制:代码是怎么跑到页面里去的

两种管线都依赖在页面全局作用域中运行的函数。这个函数的注入不在 service.ts 中——它们是在扩展初始化时通过 content scripts 或 else 机制注入的。

关键在 chrome.scripting.executeScript 的调用方式:

// 方式一:从字符串执行(本例中的用法)
chrome.scripting.executeScript({
  target: { tabId },
  func: () => window.parserReadability(),
  // args 可以传递参数
})
 
// 方式二:从文件执行(同文件中的 buildDomTree 用法)
chrome.scripting.executeScript({
  target: { tabId },
  files: ['buildDomTree.js'],
})

func 参数被序列化为字符串然后反序列化在目标页面中执行。这意味着闭包变量和外部导入在注入的函数中不可用——所有依赖必须在页面全局作用域中可用。这就是为什么 turn2MarkdownparserReadability 必须作为 window 上的全局函数存在。

对爬虫开发者的启示

什么时候用 Markdown 管线

# 爬虫伪代码:全页面采集用 Markdown
page = await browser.new_page()
await page.goto("https://example.16yun.cn/products")
# 提取整个页面为 Markdown
markdown = await page.evaluate("window.turn2Markdown()")
# Markdown 可以直接给 LLM,Token 消耗比 HTML 少约 40%

什么时候用 Readability 管线

# 爬虫伪代码:文章采集用 Readability
page = await browser.new_page()
await page.goto("https://example.16yun.cn/blog/article")
# 提取文章正文 + 元信息
result = await page.evaluate("window.parserReadability()")
# textContent 是纯文本,直接给 LLM
# content 是 HTML,保留排版结构
# title, byline, publishedTime 是元信息

两种管线的选择决策树

加载图表中...

总结

getMarkdownContentgetReadabilityContent 是 Nanobrowser 为智能体提供页面内容的两条管线。它们的实现只有 40 行代码,但背后是 turndown 和 Mozilla Readability 两个成熟的开源项目。

对于爬虫开发者来说,理解这两条管线的区别可以帮助你做出更好的选择:全量提取用 Markdown、正文提取用 Readability。如果两个都不满足需求(例如评论区、动态页面),说明你需要自己实现第三种管线。

下一篇文章将分析 Nanobrowser 的可点击元素检测系统——智能体怎么知道页面上哪些元素可以点、怎么区分"可点击"和"不可点击"、怎么通过哈希去重来避免重复操作。

需要企业代理方案?

我们可根据目标站点、并发规模与稳定性目标提供定制方案。