web-article-extractor

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Web Article Extractor

Web Article Extractor

使用 Chrome DevTools MCP 服务器从网页中提取干净的文章内容,支持绕过常见的安全限制。
使用 Chrome DevTools MCP 服务器从网页中提取干净的文章内容,支持绕过常见的安全限制。

前置条件

前置条件

MCP 服务器配置

MCP 服务器配置

确保已配置
chrome-devtools
MCP 服务器:
bash
undefined
确保已配置
chrome-devtools
MCP 服务器:
bash
undefined

添加 chrome-devtools 服务器

添加 chrome-devtools 服务器

claude mcp add chrome-devtools npx -y chrome-devtools-mcp@latest
undefined
claude mcp add chrome-devtools npx -y chrome-devtools-mcp@latest
undefined

浏览器启动参数(绕过安全限制)

浏览器启动参数(绕过安全限制)

为了访问有安全限制的网站(如微信公众号),需要配置 Chrome 启动参数。
方法 1:修改 MCP 配置
编辑 MCP 配置文件,添加 Chrome 参数:
bash
claude mcp remove chrome-devtools
claude mcp add chrome-devtools npx -y chrome-devtools-mcp@latest -- \
  --disable-blink-features=AutomationControlled \
  --disable-web-security \
  --disable-features=IsolateOrigins,site-per-process
方法 2:使用环境变量
bash
export CHROME_PATH="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
export CHROME_ARGS="--disable-web-security --disable-blink-features=AutomationControlled"
重要参数说明:
参数作用
--disable-web-security
禁用同源策略,允许跨域请求
--disable-blink-features=AutomationControlled
隐藏自动化特征
--user-agent
自定义 User-Agent(模拟微信)
--disable-features=IsolateOrigins,site-per-process
禁用站点隔离

为了访问有安全限制的网站(如微信公众号),需要配置 Chrome 启动参数。
方法 1:修改 MCP 配置
编辑 MCP 配置文件,添加 Chrome 参数:
bash
claude mcp remove chrome-devtools
claude mcp add chrome-devtools npx -y chrome-devtools-mcp@latest -- \
  --disable-blink-features=AutomationControlled \
  --disable-web-security \
  --disable-features=IsolateOrigins,site-per-process
方法 2:使用环境变量
bash
export CHROME_PATH="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
export CHROME_ARGS="--disable-web-security --disable-blink-features=AutomationControlled"
重要参数说明:
参数作用
--disable-web-security
禁用同源策略,允许跨域请求
--disable-blink-features=AutomationControlled
隐藏自动化特征
--user-agent
自定义 User-Agent(模拟微信)
--disable-features=IsolateOrigins,site-per-process
禁用站点隔离

微信公众号专用提取流程

微信公众号专用提取流程

微信公众号有多层安全防护,需要特殊处理:
微信公众号有多层安全防护,需要特殊处理:

完整提取脚本

完整提取脚本

typescript
// 微信公众号文章提取
async function extractWeChatArticle(articleUrl) {
  // 1. 连接到浏览器或创建新标签页
  const tabs = await tabs_context_mcp({ createIfEmpty: true })
  const tabId = tabs.availableTabs[0].tabId

  // 2. 设置微信 User-Agent(关键步骤)
  await javascript_tool({
    tabId,
    action: "javascript_exec",
    text: `
      // 修改 navigator.userAgent
      Object.defineProperty(navigator, 'userAgent', {
        get: () => 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.38(0x18002633) NetType/WIFI Language/zh_CN'
      });
      'User-Agent set to WeChat';
    `
  })

  // 3. 导航到文章页面
  try {
    await navigate({ tabId, url: articleUrl })
  } catch (error) {
    if (error.message.includes('not allowed')) {
      // 尝试使用代理或备用方案
      throw new Error('无法访问微信文章,可能需要使用已登录的浏览器')
    }
    throw error
  }

  // 4. 等待页面加载完成
  await javascript_tool({
    tabId,
    action: "javascript_exec",
    text: `
      // 等待微信文章内容加载
      await new Promise((resolve) => {
        const check = () => {
          const content = document.querySelector('#js_content, .rich_media_content')
          if (content && content.innerText.length > 100) {
            resolve()
          } else if (document.readyState === 'complete') {
            setTimeout(resolve, 2000) // 额外等待2秒
          } else {
            setTimeout(check, 100)
          }
        }
        check()
      })
      'Content loaded';
    `
  })

  // 5. 提取文章内容
  const article = await javascript_tool({
    tabId,
    action: "javascript_exec",
    text: `
      (() => {
        // 提取标题
        const titleEl = document.querySelector('#activity-name, .rich_media_title')
        const title = titleEl ? titleEl.innerText.trim() : document.title

        // 提取作者/公众号名称
        const authorEl = document.querySelector('#js_name, .rich_media_meta_text, .wx_follow_nickname')
        const author = authorEl ? authorEl.innerText.trim() : ''

        // 提取发布时间
        const dateEl = document.querySelector('#publish_time, .publish_time, [data-time]')
        const publishTime = dateEl ? dateEl.innerText.trim() : ''

        // 提取正文内容
        const contentEl = document.querySelector('#js_content, .rich_media_content')
        let content = ''
        if (contentEl) {
          // 清理内容:移除脚本、样式等
          const clone = contentEl.cloneNode(true)
          clone.querySelectorAll('script, style, noscript').forEach(el => el.remove())
          content = clone.innerText.trim()
        }

        // 提取图片
        const images = Array.from(document.querySelectorAll('#js_content img, .rich_media_content img'))
          .map(img => img.getAttribute('data-src') || img.src)
          .filter(src => src && !src.includes('placeholder'))

        // 提取摘要
        const descEl = document.querySelector('meta[name="description"]')
        const description = descEl ? descEl.getAttribute('content') : ''

        return JSON.stringify({
          title,
          author,
          publishTime,
          content,
          description,
          images,
          url: window.location.href,
          wordCount: content.length
        }, null, 2)
      })()
    `
  })

  return JSON.parse(article)
}
typescript
// 微信公众号文章提取
async function extractWeChatArticle(articleUrl) {
  // 1. 连接到浏览器或创建新标签页
  const tabs = await tabs_context_mcp({ createIfEmpty: true })
  const tabId = tabs.availableTabs[0].tabId

  // 2. 设置微信 User-Agent(关键步骤)
  await javascript_tool({
    tabId,
    action: "javascript_exec",
    text: `
      // 修改 navigator.userAgent
      Object.defineProperty(navigator, 'userAgent', {
        get: () => 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.38(0x18002633) NetType/WIFI Language/zh_CN'
      });
      'User-Agent set to WeChat';
    `
  })

  // 3. 导航到文章页面
  try {
    await navigate({ tabId, url: articleUrl })
  } catch (error) {
    if (error.message.includes('not allowed')) {
      // 尝试使用代理或备用方案
      throw new Error('无法访问微信文章,可能需要使用已登录的浏览器')
    }
    throw error
  }

  // 4. 等待页面加载完成
  await javascript_tool({
    tabId,
    action: "javascript_exec",
    text: `
      // 等待微信文章内容加载
      await new Promise((resolve) => {
        const check = () => {
          const content = document.querySelector('#js_content, .rich_media_content')
          if (content && content.innerText.length > 100) {
            resolve()
          } else if (document.readyState === 'complete') {
            setTimeout(resolve, 2000) // 额外等待2秒
          } else {
            setTimeout(check, 100)
          }
        }
        check()
      })
      'Content loaded';
    `
  })

  // 5. 提取文章内容
  const article = await javascript_tool({
    tabId,
    action: "javascript_exec",
    text: `
      (() => {
        // 提取标题
        const titleEl = document.querySelector('#activity-name, .rich_media_title')
        const title = titleEl ? titleEl.innerText.trim() : document.title

        // 提取作者/公众号名称
        const authorEl = document.querySelector('#js_name, .rich_media_meta_text, .wx_follow_nickname')
        const author = authorEl ? authorEl.innerText.trim() : ''

        // 提取发布时间
        const dateEl = document.querySelector('#publish_time, .publish_time, [data-time]')
        const publishTime = dateEl ? dateEl.innerText.trim() : ''

        // 提取正文内容
        const contentEl = document.querySelector('#js_content, .rich_media_content')
        let content = ''
        if (contentEl) {
          // 清理内容:移除脚本、样式等
          const clone = contentEl.cloneNode(true)
          clone.querySelectorAll('script, style, noscript').forEach(el => el.remove())
          content = clone.innerText.trim()
        }

        // 提取图片
        const images = Array.from(document.querySelectorAll('#js_content img, .rich_media_content img'))
          .map(img => img.getAttribute('data-src') || img.src)
          .filter(src => src && !src.includes('placeholder'))

        // 提取摘要
        const descEl = document.querySelector('meta[name="description"]')
        const description = descEl ? descEl.getAttribute('content') : ''

        return JSON.stringify({
          title,
          author,
          publishTime,
          content,
          description,
          images,
          url: window.location.href,
          wordCount: content.length
        }, null, 2)
      })()
    `
  })

  return JSON.parse(article)
}

微信公众号 CSS 选择器参考

微信公众号 CSS 选择器参考

元素选择器
标题
#activity-name
,
.rich_media_title
正文
#js_content
,
.rich_media_content
作者/公众号
#js_name
,
.rich_media_meta_text
发布时间
#publish_time
,
.publish_time
图片
#js_content img
,
.rich_media_content img
摘要
meta[name="description"]

元素选择器
标题
#activity-name
,
.rich_media_title
正文
#js_content
,
.rich_media_content
作者/公众号
#js_name
,
.rich_media_meta_text
发布时间
#publish_time
,
.publish_time
图片
#js_content img
,
.rich_media_content img
摘要
meta[name="description"]

通用文章提取流程

通用文章提取流程

方法选择

方法选择

本技能提供两种文章提取方法:
  1. Readability.js(推荐) - Mozilla 的成熟提取算法,处理复杂布局更准确
  2. 简化算法 - 自定义轻量级算法,速度更快但准确度稍低
本技能提供两种文章提取方法:
  1. Readability.js(推荐) - Mozilla 的成熟提取算法,处理复杂布局更准确
  2. 简化算法 - 自定义轻量级算法,速度更快但准确度稍低

步骤 1:获取标签页上下文

步骤 1:获取标签页上下文

typescript
const context = await tabs_context_mcp({ createIfEmpty: true })
const tabId = context.availableTabs[0].tabId
typescript
const context = await tabs_context_mcp({ createIfEmpty: true })
const tabId = context.availableTabs[0].tabId

步骤 2:导航到页面

步骤 2:导航到页面

typescript
await navigate({ tabId, url: targetUrl })
typescript
await navigate({ tabId, url: targetUrl })

步骤 3:等待页面加载

步骤 3:等待页面加载

typescript
// 等待 DOM 加载完成
await javascript_tool({
  tabId,
  action: "javascript_exec",
  text: `new Promise(r => window.addEventListener('load', r))`
})
typescript
// 等待 DOM 加载完成
await javascript_tool({
  tabId,
  action: "javascript_exec",
  text: `new Promise(r => window.addEventListener('load', r))`
})

步骤 4:提取内容

步骤 4:提取内容

方法 A:使用 Readability.js(推荐,最准确)
typescript
// 读取 Readability 提取脚本
const readabilityScript = await fs.readFile(
  '~/.claude/skills/web-article-extractor/scripts/readability_extractor.js',
  'utf8'
);

// 执行提取
const result = await javascript_tool({
  tabId,
  action: "javascript_exec",
  text: readabilityScript
});

const article = JSON.parse(result);
console.log('提取结果:', article);
// article 包含: title, content, contentHtml, author, wordCount, images, headings 等
方法 B:使用简化提取算法(更快)
typescript
// 读取简化提取脚本
const extractScript = await fs.readFile(
  '~/.claude/skills/web-article-extractor/scripts/extract_article.js',
  'utf8'
);

const result = await javascript_tool({
  tabId,
  action: "javascript_exec",
  text: extractScript
});

const article = JSON.parse(result);
方法 C:使用内联 JavaScript 提取(最简单)
typescript
const content = await javascript_tool({
  tabId,
  action: "javascript_exec",
  text: `
    JSON.stringify({
      title: document.querySelector('h1')?.innerText || document.title,
      content: document.querySelector('article, main')?.innerText || document.body.innerText,
      author: document.querySelector('.author, [name="author"]')?.innerText || ''
    })
  `
})

方法 A:使用 Readability.js(推荐,最准确)
typescript
// 读取 Readability 提取脚本
const readabilityScript = await fs.readFile(
  '~/.claude/skills/web-article-extractor/scripts/readability_extractor.js',
  'utf8'
);

// 执行提取
const result = await javascript_tool({
  tabId,
  action: "javascript_exec",
  text: readabilityScript
});

const article = JSON.parse(result);
console.log('提取结果:', article);
// article 包含: title, content, contentHtml, author, wordCount, images, headings 等
方法 B:使用简化提取算法(更快)
typescript
// 读取简化提取脚本
const extractScript = await fs.readFile(
  '~/.claude/skills/web-article-extractor/scripts/extract_article.js',
  'utf8'
);

const result = await javascript_tool({
  tabId,
  action: "javascript_exec",
  text: extractScript
});

const article = JSON.parse(result);
方法 C:使用内联 JavaScript 提取(最简单)
typescript
const content = await javascript_tool({
  tabId,
  action: "javascript_exec",
  text: `
    JSON.stringify({
      title: document.querySelector('h1')?.innerText || document.title,
      content: document.querySelector('article, main')?.innerText || document.body.innerText,
      author: document.querySelector('.author, [name="author"]')?.innerText || ''
    })
  `
})

isProbablyReaderable - 快速预检测

isProbablyReaderable - 快速预检测

什么是 isProbablyReaderable?

什么是 isProbablyReaderable?

isProbablyReaderable()
是一个快速、轻量级的检测函数,用于判断页面是否适合使用 Readability 进行内容提取。它在不执行完整解析的情况下,快速评估页面的"可读性分数"。
isProbablyReaderable()
是一个快速、轻量级的检测函数,用于判断页面是否适合使用 Readability 进行内容提取。它在不执行完整解析的情况下,快速评估页面的"可读性分数"。

使用场景

使用场景

  1. 性能优化 - 避免在不合适的页面上运行完整的 Readability 解析
  2. 用户体验 - 提前判断是否显示"阅读模式"按钮
  3. 批量处理 - 快速筛选大量页面,只处理适合的内容
  1. 性能优化 - 避免在不合适的页面上运行完整的 Readability 解析
  2. 用户体验 - 提前判断是否显示"阅读模式"按钮
  3. 批量处理 - 快速筛选大量页面,只处理适合的内容

基本用法

基本用法

typescript
// 检查当前页面是否适合提取
if (isProbablyReaderable(document)) {
  // 页面适合提取,执行完整解析
  const article = new Readability(document.cloneNode(true)).parse();
  console.log('提取成功:', article.title);
} else {
  console.log('此页面可能不适合内容提取');
}
typescript
// 检查当前页面是否适合提取
if (isProbablyReaderable(document)) {
  // 页面适合提取,执行完整解析
  const article = new Readability(document.cloneNode(true)).parse();
  console.log('提取成功:', article.title);
} else {
  console.log('此页面可能不适合内容提取');
}

配置选项

配置选项

typescript
const options = {
  // 最小内容长度(字符数)
  minContentLength: 140, // 默认: 140

  // 最小可读性分数
  minScore: 20, // 默认: 20

  // 自定义可见性检查函数
  visibilityChecker: (node) => {
    if (!node || node.nodeType !== 1) return false;
    const style = window.getComputedStyle(node);
    return (
      style.display !== 'none' &&
      style.visibility !== 'hidden' &&
      style.opacity !== '0'
    );
  }
};

const isReaderable = isProbablyReaderable(document, options);
typescript
const options = {
  // 最小内容长度(字符数)
  minContentLength: 140, // 默认: 140

  // 最小可读性分数
  minScore: 20, // 默认: 20

  // 自定义可见性检查函数
  visibilityChecker: (node) => {
    if (!node || node.nodeType !== 1) return false;
    const style = window.getComputedStyle(node);
    return (
      style.display !== 'none' &&
      style.visibility !== 'hidden' &&
      style.opacity !== '0'
    );
  }
};

const isReaderable = isProbablyReaderable(document, options);

完整示例

完整示例

typescript
async function smartExtractArticle(url) {
  // 1. 导航到页面
  await navigate({ tabId, url });

  // 2. 等待页面加载
  await javascript_tool({
    tabId,
    action: "javascript_exec",
    text: `new Promise(r => {
      if (document.readyState === 'complete') r();
      else window.addEventListener('load', r);
    })`
  });

  // 3. 使用 isProbablyReaderable 预检测
  const readabilityCheck = await javascript_tool({
    tabId,
    action: "javascript_exec",
    text: `
      // 加载 Readability-readerable.js
      await new Promise((resolve, reject) => {
        const script = document.createElement('script');
        script.src = 'https://cdn.jsdelivr.net/npm/@mozilla/readability@0.6.0/Readability-readerable.min.js';
        script.onload = resolve;
        script.onerror = reject;
        document.head.appendChild(script);
      });

      // 检测是否可读
      const isReaderable = isProbablyReaderable(document, {
        minContentLength: 140,
        minScore: 20
      });

      JSON.stringify({
        isReaderable: isReaderable,
        pageType: document.querySelector('article') ? 'article' : 'other',
        hasMainContent: !!document.querySelector('main, article, .content')
      });
    `
  });

  const check = JSON.parse(readabilityCheck);

  if (!check.isReaderable) {
    console.warn('⚠️ 此页面可能不适合提取,但仍会尝试...');
  }

  // 4. 执行完整提取(即使 isReaderable 为 false,也可以尝试)
  const result = await javascript_tool({
    tabId,
    action: "javascript_exec",
    text: readabilityScript // 使用完整的提取脚本
  });

  return JSON.parse(result);
}
typescript
async function smartExtractArticle(url) {
  // 1. 导航到页面
  await navigate({ tabId, url });

  // 2. 等待页面加载
  await javascript_tool({
    tabId,
    action: "javascript_exec",
    text: `new Promise(r => {
      if (document.readyState === 'complete') r();
      else window.addEventListener('load', r);
    })`
  });

  // 3. 使用 isProbablyReaderable 预检测
  const readabilityCheck = await javascript_tool({
    tabId,
    action: "javascript_exec",
    text: `
      // 加载 Readability-readerable.js
      await new Promise((resolve, reject) => {
        const script = document.createElement('script');
        script.src = 'https://cdn.jsdelivr.net/npm/@mozilla/readability@0.6.0/Readability-readerable.min.js';
        script.onload = resolve;
        script.onerror = reject;
        document.head.appendChild(script);
      });

      // 检测是否可读
      const isReaderable = isProbablyReaderable(document, {
        minContentLength: 140,
        minScore: 20
      });

      JSON.stringify({
        isReaderable: isReaderable,
        pageType: document.querySelector('article') ? 'article' : 'other',
        hasMainContent: !!document.querySelector('main, article, .content')
      });
    `
  });

  const check = JSON.parse(readabilityCheck);

  if (!check.isReaderable) {
    console.warn('⚠️ 此页面可能不适合提取,但仍会尝试...');
  }

  // 4. 执行完整提取(即使 isReaderable 为 false,也可以尝试)
  const result = await javascript_tool({
    tabId,
    action: "javascript_exec",
    text: readabilityScript // 使用完整的提取脚本
  });

  return JSON.parse(result);
}

评分机制

评分机制

isProbablyReaderable
通过以下因素计算可读性分数:
因素权重说明
段落数量至少需要一定数量的
<p>
标签
内容长度文本内容需要达到最小长度阈值
链接密度链接与文本的比例不能过高
文章结构检测
<article>
,
<main>
等语义化标签
可见性内容必须可见(非 display:none)
isProbablyReaderable
通过以下因素计算可读性分数:
因素权重说明
段落数量至少需要一定数量的
<p>
标签
内容长度文本内容需要达到最小长度阈值
链接密度链接与文本的比例不能过高
文章结构检测
<article>
,
<main>
等语义化标签
可见性内容必须可见(非 display:none)

返回值说明

返回值说明

  • true
    - 页面很可能适合使用 Readability 提取
  • false
    - 页面可能不适合,但不代表完全无法提取
重要提示:即使返回
false
,仍可尝试运行完整的 Readability 解析。该函数只是一个快速预判,并非绝对准确。
  • true
    - 页面很可能适合使用 Readability 提取
  • false
    - 页面可能不适合,但不代表完全无法提取
重要提示:即使返回
false
,仍可尝试运行完整的 Readability 解析。该函数只是一个快速预判,并非绝对准确。

性能对比

性能对比

操作耗时说明
isProbablyReaderable()
~5-10ms快速扫描 DOM
Readability.parse()
~50-200ms完整解析和清理
使用预检测可以节省 90% 的不必要处理时间。

操作耗时说明
isProbablyReaderable()
~5-10ms快速扫描 DOM
Readability.parse()
~50-200ms完整解析和清理
使用预检测可以节省 90% 的不必要处理时间。

Readability.js 详解

Readability.js 详解

什么是 Readability.js?

什么是 Readability.js?

Readability.js 是 Mozilla 开发的开源文章提取算法,被 Firefox Reader View 功能使用。它能够智能识别网页中的主要内容,自动过滤广告、导航、评论等干扰元素。
Readability.js 是 Mozilla 开发的开源文章提取算法,被 Firefox Reader View 功能使用。它能够智能识别网页中的主要内容,自动过滤广告、导航、评论等干扰元素。

主要优势

主要优势

特性说明
智能内容识别使用复杂算法分析DOM结构,识别主要文章内容
自动清理移除广告、导航、社交分享按钮等干扰元素
保留格式保留文章的HTML格式(标题、段落、图片、列表等)
元数据提取自动提取标题、作者、摘要等元数据
跨网站兼容适用于绝大多数新闻、博客、文章类网站
特性说明
智能内容识别使用复杂算法分析DOM结构,识别主要文章内容
自动清理移除广告、导航、社交分享按钮等干扰元素
保留格式保留文章的HTML格式(标题、段落、图片、列表等)
元数据提取自动提取标题、作者、摘要等元数据
跨网站兼容适用于绝大多数新闻、博客、文章类网站

完整使用示例

完整使用示例

typescript
async function extractWithReadability(url) {
  // 1. 获取标签页
  const context = await tabs_context_mcp({ createIfEmpty: true });
  const tabId = context.availableTabs[0].tabId;

  // 2. 导航到目标页面
  await navigate({ tabId, url });

  // 3. 等待页面加载
  await javascript_tool({
    tabId,
    action: "javascript_exec",
    text: `new Promise(r => {
      if (document.readyState === 'complete') r();
      else window.addEventListener('load', r);
    })`
  });

  // 4. 读取并执行 Readability 提取脚本
  const readabilityScript = await fs.readFile(
    '~/.claude/skills/web-article-extractor/scripts/readability_extractor.js',
    'utf8'
  );

  const result = await javascript_tool({
    tabId,
    action: "javascript_exec",
    text: readabilityScript
  });

  // 5. 解析结果
  const article = JSON.parse(result);

  if (!article.success) {
    throw new Error(`提取失败: ${article.error}`);
  }

  return article;
}

// 使用示例
const article = await extractWithReadability('https://example.com/article');

console.log('标题:', article.title);
console.log('作者:', article.author);
console.log('字数:', article.wordCount);
console.log('阅读时长:', article.readingTime, '分钟');
console.log('可读性检测:', article.readerability.isReaderable);
console.log('正文:', article.content);
console.log('HTML:', article.contentHtml);
typescript
async function extractWithReadability(url) {
  // 1. 获取标签页
  const context = await tabs_context_mcp({ createIfEmpty: true });
  const tabId = context.availableTabs[0].tabId;

  // 2. 导航到目标页面
  await navigate({ tabId, url });

  // 3. 等待页面加载
  await javascript_tool({
    tabId,
    action: "javascript_exec",
    text: `new Promise(r => {
      if (document.readyState === 'complete') r();
      else window.addEventListener('load', r);
    })`
  });

  // 4. 读取并执行 Readability 提取脚本
  const readabilityScript = await fs.readFile(
    '~/.claude/skills/web-article-extractor/scripts/readability_extractor.js',
    'utf8'
  );

  const result = await javascript_tool({
    tabId,
    action: "javascript_exec",
    text: readabilityScript
  });

  // 5. 解析结果
  const article = JSON.parse(result);

  if (!article.success) {
    throw new Error(`提取失败: ${article.error}`);
  }

  return article;
}

// 使用示例
const article = await extractWithReadability('https://example.com/article');

console.log('标题:', article.title);
console.log('作者:', article.author);
console.log('字数:', article.wordCount);
console.log('阅读时长:', article.readingTime, '分钟');
console.log('可读性检测:', article.readerability.isReaderable);
console.log('正文:', article.content);
console.log('HTML:', article.contentHtml);

Readability 配置选项完整说明

Readability 配置选项完整说明

根据 Mozilla 官方文档,
new Readability(document, options)
支持以下配置选项:
选项类型默认值说明
debug
boolean
false
是否启用调试日志输出到控制台
maxElemsToParse
number
0
最大解析元素数量限制(0 = 无限制)
nbTopCandidates
number
5
分析候选内容时考虑的顶级候选者数量
charThreshold
number
500
文章必须达到的最小字符数才返回结果
classesToPreserve
string[]
[]
保留的 CSS 类名数组(当 keepClasses 为 false 时)
keepClasses
boolean
false
是否保留所有 HTML 元素的 class 属性
disableJSONLD
boolean
false
禁用 JSON-LD 格式的 Schema.org 元数据解析
serializer
function
el => el.innerHTML
自定义内容序列化函数(用于控制 content 属性的生成)
allowedVideoRegex
RegExp
内置正则允许保留的视频 URL 正则表达式
linkDensityModifier
number
0
链接密度阈值修正值(正数提高阈值,负数降低)
根据 Mozilla 官方文档,
new Readability(document, options)
支持以下配置选项:
选项类型默认值说明
debug
boolean
false
是否启用调试日志输出到控制台
maxElemsToParse
number
0
最大解析元素数量限制(0 = 无限制)
nbTopCandidates
number
5
分析候选内容时考虑的顶级候选者数量
charThreshold
number
500
文章必须达到的最小字符数才返回结果
classesToPreserve
string[]
[]
保留的 CSS 类名数组(当 keepClasses 为 false 时)
keepClasses
boolean
false
是否保留所有 HTML 元素的 class 属性
disableJSONLD
boolean
false
禁用 JSON-LD 格式的 Schema.org 元数据解析
serializer
function
el => el.innerHTML
自定义内容序列化函数(用于控制 content 属性的生成)
allowedVideoRegex
RegExp
内置正则允许保留的视频 URL 正则表达式
linkDensityModifier
number
0
链接密度阈值修正值(正数提高阈值,负数降低)

配置示例

配置示例

基础配置(推荐默认)
javascript
const reader = new Readability(documentClone, {
  debug: false,
  charThreshold: 500
});
严格模式(高质量文章)
javascript
const reader = new Readability(documentClone, {
  charThreshold: 1000,      // 更高的字符要求
  nbTopCandidates: 10,      // 更多候选者分析
  linkDensityModifier: -0.2 // 降低链接密度容忍度
});
宽松模式(短文章)
javascript
const reader = new Readability(documentClone, {
  charThreshold: 200,       // 较低的字符要求
  maxElemsToParse: 5000,    // 限制解析元素数
  linkDensityModifier: 0.3  // 提高链接密度容忍度
});
保留样式类(用于进一步处理)
javascript
const reader = new Readability(documentClone, {
  keepClasses: false,
  classesToPreserve: [
    'caption',     // 图片说明
    'credit',      // 图片版权
    'figure',      // 图片容器
    'highlight',   // 高亮文本
    'pullquote',   // 引用块
    'code-block'   // 代码块
  ]
});
返回 DOM 元素而非 HTML 字符串
javascript
const reader = new Readability(documentClone, {
  serializer: el => el  // 返回 DOM 元素本身
});

const article = reader.parse();
// article.content 现在是 DOM Element,可以进一步处理
const modifiedContent = processDOM(article.content);
自定义视频 URL 白名单
javascript
const reader = new Readability(documentClone, {
  allowedVideoRegex: /\/\/(youtube|vimeo|bilibili|youku)\.com/i
});
基础配置(推荐默认)
javascript
const reader = new Readability(documentClone, {
  debug: false,
  charThreshold: 500
});
严格模式(高质量文章)
javascript
const reader = new Readability(documentClone, {
  charThreshold: 1000,      // 更高的字符要求
  nbTopCandidates: 10,      // 更多候选者分析
  linkDensityModifier: -0.2 // 降低链接密度容忍度
});
宽松模式(短文章)
javascript
const reader = new Readability(documentClone, {
  charThreshold: 200,       // 较低的字符要求
  maxElemsToParse: 5000,    // 限制解析元素数
  linkDensityModifier: 0.3  // 提高链接密度容忍度
});
保留样式类(用于进一步处理)
javascript
const reader = new Readability(documentClone, {
  keepClasses: false,
  classesToPreserve: [
    'caption',     // 图片说明
    'credit',      // 图片版权
    'figure',      // 图片容器
    'highlight',   // 高亮文本
    'pullquote',   // 引用块
    'code-block'   // 代码块
  ]
});
返回 DOM 元素而非 HTML 字符串
javascript
const reader = new Readability(documentClone, {
  serializer: el => el  // 返回 DOM 元素本身
});

const article = reader.parse();
// article.content 现在是 DOM Element,可以进一步处理
const modifiedContent = processDOM(article.content);
自定义视频 URL 白名单
javascript
const reader = new Readability(documentClone, {
  allowedVideoRegex: /\/\/(youtube|vimeo|bilibili|youku)\.com/i
});

高级技巧

高级技巧

动态调整配置
javascript
async function smartExtract(url, pageType) {
  const configs = {
    'blog': { charThreshold: 300, linkDensityModifier: 0 },
    'news': { charThreshold: 500, nbTopCandidates: 8 },
    'academic': { charThreshold: 1500, disableJSONLD: false },
    'social': { charThreshold: 100, linkDensityModifier: 0.5 }
  };

  const config = configs[pageType] || configs['blog'];
  const reader = new Readability(documentClone, config);
  return reader.parse();
}
动态调整配置
javascript
async function smartExtract(url, pageType) {
  const configs = {
    'blog': { charThreshold: 300, linkDensityModifier: 0 },
    'news': { charThreshold: 500, nbTopCandidates: 8 },
    'academic': { charThreshold: 1500, disableJSONLD: false },
    'social': { charThreshold: 100, linkDensityModifier: 0.5 }
  };

  const config = configs[pageType] || configs['blog'];
  const reader = new Readability(documentClone, config);
  return reader.parse();
}

返回数据结构

返回数据结构

typescript
interface ReadabilityResult {
  // === 状态信息 ===
  success: boolean;
  extractionMethod: 'readability' | 'fallback';
  extractedAt: string; // ISO 8601 时间戳
  readabilityVersion: string; // Readability.js 版本号

  // === isProbablyReaderable 预检测结果 ===
  readerability: {
    isReaderable: boolean;
    checkedAt: string;
  };

  // === 核心内容(Readability 原生字段) ===
  title: string;
  content: string;          // 纯文本内容 (textContent)
  contentHtml: string;      // HTML 格式内容 (content)
  excerpt: string;          // 摘要/预览

  // === 元数据(Readability 原生 + 增强) ===
  author: string | null;
  byline: string | null;    // Readability 原生署名字段
  publishDate: string | null; // 增强提取的发布日期
  publishedTime: string | null; // Readability 原生发布时间(新增 v0.6.0)
  siteName: string | null;  // Readability 原生网站名称
  language: string | null;  // Readability 原生语言字段 (lang)
  dir: string | null;       // Readability 原生文本方向 (ltr/rtl)

  // === 内容分析 ===
  wordCount: number;
  contentLength: number;    // Readability 原生字段 (length)
  readingTime: number;      // 预估阅读时长(分钟)

  // === 文章结构 ===
  headings: Array<{ level: number; text: string }>;
  images: Array<{
    src: string;
    alt: string | null;
    width: number | null;
    height: number | null;
  }>;
  tags: string[];
  categories: string[];

  // === URL 信息 ===
  url: string;
  canonicalUrl: string;

  // === SEO 元数据 ===
  metaDescription: string | null;

  // Open Graph
  ogTitle: string | null;
  ogDescription: string | null;
  ogImage: string | null;

  // Twitter Card
  twitterCard: string | null;
  twitterTitle: string | null;
  twitterDescription: string | null;
  twitterImage: string | null;

  // === 其他 ===
  favicon: string | null;
  theme: string | null;
}
字段说明
字段来源说明
title
Readability文章标题
content
Readability (
textContent
)
纯文本内容,已去除所有 HTML 标签
contentHtml
Readability (
content
)
保留格式的 HTML 内容
excerpt
Readability文章摘要或预览文本
byline
Readability自动提取的作者署名
publishedTime
Readability (v0.6.0+)自动提取的发布时间
lang
Readability内容语言代码(如
en
,
zh-CN
dir
Readability文本方向(
ltr
rtl
length
Readability内容长度(字符数)
siteName
Readability网站名称
readerability
增强字段isProbablyReaderable 检测结果
author
增强提取多来源综合提取的作者信息
images
增强提取从内容中提取的所有图片信息
headings
增强提取文章标题结构(H1-H6)
readingTime
计算值基于 200 字/分钟计算的阅读时长
undefined
typescript
interface ReadabilityResult {
  // === 状态信息 ===
  success: boolean;
  extractionMethod: 'readability' | 'fallback';
  extractedAt: string; // ISO 8601 时间戳
  readabilityVersion: string; // Readability.js 版本号

  // === isProbablyReaderable 预检测结果 ===
  readerability: {
    isReaderable: boolean;
    checkedAt: string;
  };

  // === 核心内容(Readability 原生字段) ===
  title: string;
  content: string;          // 纯文本内容 (textContent)
  contentHtml: string;      // HTML 格式内容 (content)
  excerpt: string;          // 摘要/预览

  // === 元数据(Readability 原生 + 增强) ===
  author: string | null;
  byline: string | null;    // Readability 原生署名字段
  publishDate: string | null; // 增强提取的发布日期
  publishedTime: string | null; // Readability 原生发布时间(新增 v0.6.0)
  siteName: string | null;  // Readability 原生网站名称
  language: string | null;  // Readability 原生语言字段 (lang)
  dir: string | null;       // Readability 原生文本方向 (ltr/rtl)

  // === 内容分析 ===
  wordCount: number;
  contentLength: number;    // Readability 原生字段 (length)
  readingTime: number;      // 预估阅读时长(分钟)

  // === 文章结构 ===
  headings: Array<{ level: number; text: string }>;
  images: Array<{
    src: string;
    alt: string | null;
    width: number | null;
    height: number | null;
  }>;
  tags: string[];
  categories: string[];

  // === URL 信息 ===
  url: string;
  canonicalUrl: string;

  // === SEO 元数据 ===
  metaDescription: string | null;

  // Open Graph
  ogTitle: string | null;
  ogDescription: string | null;
  ogImage: string | null;

  // Twitter Card
  twitterCard: string | null;
  twitterTitle: string | null;
  twitterDescription: string | null;
  twitterImage: string | null;

  // === 其他 ===
  favicon: string | null;
  theme: string | null;
}
字段说明
字段来源说明
title
Readability文章标题
content
Readability (
textContent
)
纯文本内容,已去除所有 HTML 标签
contentHtml
Readability (
content
)
保留格式的 HTML 内容
excerpt
Readability文章摘要或预览文本
byline
Readability自动提取的作者署名
publishedTime
Readability (v0.6.0+)自动提取的发布时间
lang
Readability内容语言代码(如
en
,
zh-CN
dir
Readability文本方向(
ltr
rtl
length
Readability内容长度(字符数)
siteName
Readability网站名称
readerability
增强字段isProbablyReaderable 检测结果
author
增强提取多来源综合提取的作者信息
images
增强提取从内容中提取的所有图片信息
headings
增强提取文章标题结构(H1-H6)
readingTime
计算值基于 200 字/分钟计算的阅读时长
undefined

Readability vs 简化算法对比

Readability vs 简化算法对比

特性Readability.js简化算法
准确度⭐⭐⭐⭐⭐⭐⭐⭐
速度⭐⭐⭐⭐⭐⭐⭐⭐⭐
依赖需要加载外部库无依赖
文件大小~50KB~5KB
复杂网站支持优秀一般
自定义选择器不支持支持
特性Readability.js简化算法
准确度⭐⭐⭐⭐⭐⭐⭐⭐
速度⭐⭐⭐⭐⭐⭐⭐⭐⭐
依赖需要加载外部库无依赖
文件大小~50KB~5KB
复杂网站支持优秀一般
自定义选择器不支持支持

最佳实践

最佳实践

1. 提取流程优化

1. 提取流程优化

推荐流程(三层策略)
typescript
async function optimizedExtract(url) {
  // 第一层:isProbablyReaderable 快速预检
  const isReaderable = await checkReaderable();

  if (!isReaderable) {
    console.warn('页面可能不适合提取,但仍会尝试');
  }

  // 第二层:使用 Readability 完整提取
  try {
    const article = await extractWithReadability();
    if (article.success && article.contentLength > 500) {
      return article;
    }
  } catch (error) {
    console.error('Readability 失败:', error);
  }

  // 第三层:降级到简化算法
  return await extractWithSimpleAlgorithm();
}
推荐流程(三层策略)
typescript
async function optimizedExtract(url) {
  // 第一层:isProbablyReaderable 快速预检
  const isReaderable = await checkReaderable();

  if (!isReaderable) {
    console.warn('页面可能不适合提取,但仍会尝试');
  }

  // 第二层:使用 Readability 完整提取
  try {
    const article = await extractWithReadability();
    if (article.success && article.contentLength > 500) {
      return article;
    }
  } catch (error) {
    console.error('Readability 失败:', error);
  }

  // 第三层:降级到简化算法
  return await extractWithSimpleAlgorithm();
}

2. 针对不同网站类型的策略

2. 针对不同网站类型的策略

网站类型推荐方法配置建议
新闻网站Readability
charThreshold: 500
,
nbTopCandidates: 8
博客文章Readability
charThreshold: 300
, 默认配置
学术论文Readability
charThreshold: 1500
,
keepClasses: true
社交媒体简化算法Readability 可能过滤过多内容
微信公众号自定义选择器需要特殊处理(见微信专用流程)
知乎/掘金Readability + 自定义结合平台特定选择器
网站类型推荐方法配置建议
新闻网站Readability
charThreshold: 500
,
nbTopCandidates: 8
博客文章Readability
charThreshold: 300
, 默认配置
学术论文Readability
charThreshold: 1500
,
keepClasses: true
社交媒体简化算法Readability 可能过滤过多内容
微信公众号自定义选择器需要特殊处理(见微信专用流程)
知乎/掘金Readability + 自定义结合平台特定选择器

3. 性能优化建议

3. 性能优化建议

减少不必要的提取
typescript
// 使用 isProbablyReaderable 避免无效提取
if (isProbablyReaderable(document)) {
  await extractFull();
} else {
  // 只提取基本信息
  return { title: document.title, url: location.href };
}
批量提取时的优化
typescript
async function batchExtract(urls) {
  // 1. 快速预筛选
  const readableUrls = [];
  for (const url of urls) {
    await navigate(url);
    if (isProbablyReaderable(document)) {
      readableUrls.push(url);
    }
  }

  // 2. 只对通过预检的 URL 进行完整提取
  return Promise.all(readableUrls.map(extractWithReadability));
}
减少不必要的提取
typescript
// 使用 isProbablyReaderable 避免无效提取
if (isProbablyReaderable(document)) {
  await extractFull();
} else {
  // 只提取基本信息
  return { title: document.title, url: location.href };
}
批量提取时的优化
typescript
async function batchExtract(urls) {
  // 1. 快速预筛选
  const readableUrls = [];
  for (const url of urls) {
    await navigate(url);
    if (isProbablyReaderable(document)) {
      readableUrls.push(url);
    }
  }

  // 2. 只对通过预检的 URL 进行完整提取
  return Promise.all(readableUrls.map(extractWithReadability));
}

4. 错误处理和降级策略

4. 错误处理和降级策略

typescript
async function robustExtract(url) {
  const strategies = [
    // 策略 1: Readability with strict config
    () => extract({ charThreshold: 1000 }),

    // 策略 2: Readability with lenient config
    () => extract({ charThreshold: 200, linkDensityModifier: 0.5 }),

    // 策略 3: 简化算法
    () => simpleExtract(),

    // 策略 4: 基础提取
    () => ({ title: document.title, content: document.body.innerText })
  ];

  for (const strategy of strategies) {
    try {
      const result = await strategy();
      if (result.contentLength > 100) {
        return result;
      }
    } catch (error) {
      console.warn('策略失败,尝试下一个:', error);
    }
  }

  throw new Error('所有提取策略均失败');
}
typescript
async function robustExtract(url) {
  const strategies = [
    // 策略 1: Readability with strict config
    () => extract({ charThreshold: 1000 }),

    // 策略 2: Readability with lenient config
    () => extract({ charThreshold: 200, linkDensityModifier: 0.5 }),

    // 策略 3: 简化算法
    () => simpleExtract(),

    // 策略 4: 基础提取
    () => ({ title: document.title, content: document.body.innerText })
  ];

  for (const strategy of strategies) {
    try {
      const result = await strategy();
      if (result.contentLength > 100) {
        return result;
      }
    } catch (error) {
      console.warn('策略失败,尝试下一个:', error);
    }
  }

  throw new Error('所有提取策略均失败');
}

5. 内容质量验证

5. 内容质量验证

typescript
function validateExtractedContent(article) {
  const quality = {
    hasTitle: !!article.title && article.title.length > 5,
    hasContent: article.contentLength > 500,
    hasAuthor: !!article.author || !!article.byline,
    hasImages: article.images && article.images.length > 0,
    isReaderable: article.readerability?.isReaderable
  };

  const score = Object.values(quality).filter(Boolean).length;

  return {
    isValid: score >= 2,
    score: score,
    quality: quality,
    recommendation: score >= 4 ? '高质量' : score >= 2 ? '可用' : '质量较低'
  };
}
typescript
function validateExtractedContent(article) {
  const quality = {
    hasTitle: !!article.title && article.title.length > 5,
    hasContent: article.contentLength > 500,
    hasAuthor: !!article.author || !!article.byline,
    hasImages: article.images && article.images.length > 0,
    isReaderable: article.readerability?.isReaderable
  };

  const score = Object.values(quality).filter(Boolean).length;

  return {
    isValid: score >= 2,
    score: score,
    quality: quality,
    recommendation: score >= 4 ? '高质量' : score >= 2 ? '可用' : '质量较低'
  };
}

6. 特殊网站处理

6. 特殊网站处理

微信公众号
  • 使用自定义选择器(见微信专用流程)
  • 设置微信 User-Agent
  • 可能需要登录态
知乎
typescript
const zhihuConfig = {
  charThreshold: 300,
  classesToPreserve: ['RichText', 'Post-RichTextContainer']
};
Medium
typescript
const mediumConfig = {
  charThreshold: 500,
  keepClasses: false
};
微信公众号
  • 使用自定义选择器(见微信专用流程)
  • 设置微信 User-Agent
  • 可能需要登录态
知乎
typescript
const zhihuConfig = {
  charThreshold: 300,
  classesToPreserve: ['RichText', 'Post-RichTextContainer']
};
Medium
typescript
const mediumConfig = {
  charThreshold: 500,
  keepClasses: false
};

7. 调试技巧

7. 调试技巧

启用 Readability 调试模式
javascript
const reader = new Readability(documentClone, {
  debug: true  // 在控制台输出详细日志
});
对比不同配置的效果
typescript
async function compareConfigs(url) {
  const configs = [
    { name: '默认', options: {} },
    { name: '严格', options: { charThreshold: 1000 } },
    { name: '宽松', options: { charThreshold: 200 } }
  ];

  for (const config of configs) {
    const reader = new Readability(doc.cloneNode(true), config.options);
    const result = reader.parse();
    console.log(`${config.name}:`, {
      contentLength: result?.length,
      title: result?.title
    });
  }
}
启用 Readability 调试模式
javascript
const reader = new Readability(documentClone, {
  debug: true  // 在控制台输出详细日志
});
对比不同配置的效果
typescript
async function compareConfigs(url) {
  const configs = [
    { name: '默认', options: {} },
    { name: '严格', options: { charThreshold: 1000 } },
    { name: '宽松', options: { charThreshold: 200 } }
  ];

  for (const config of configs) {
    const reader = new Readability(doc.cloneNode(true), config.options);
    const result = reader.parse();
    console.log(`${config.name}:`, {
      contentLength: result?.length,
      title: result?.title
    });
  }
}

常见问题

常见问题

Q: Readability 无法加载怎么办?
A: 脚本会自动降级到基础提取,返回
success: false
extractionMethod: 'fallback'
Q: 如何处理动态加载的内容?
A: 在执行 Readability 之前,先等待内容加载完成(见上面的完整示例)。
Q: Readability 适用于所有网站吗?
A: Readability 针对文章类内容优化,对于电商、社交媒体等非文章类网站效果可能不佳。

Q: Readability 无法加载怎么办?
A: 脚本会自动降级到基础提取,返回
success: false
extractionMethod: 'fallback'
Q: 如何处理动态加载的内容?
A: 在执行 Readability 之前,先等待内容加载完成(见上面的完整示例)。
Q: Readability 适用于所有网站吗?
A: Readability 针对文章类内容优化,对于电商、社交媒体等非文章类网站效果可能不佳。

处理不同类型网站

处理不同类型网站

知乎

知乎

typescript
const zhihuContent = await javascript_tool({
  tabId,
  action: "javascript_exec",
  text: `
    JSON.stringify({
      title: document.querySelector('.Post-Title, h1')?.innerText,
      content: document.querySelector('.Post-RichText, .RichContent-inner')?.innerText,
      author: document.querySelector('.UserLink-link, .AuthorInfo-name')?.innerText,
      votes: document.querySelector('.VoteButton--up .CountValue')?.innerText
    })
  `
})
typescript
const zhihuContent = await javascript_tool({
  tabId,
  action: "javascript_exec",
  text: `
    JSON.stringify({
      title: document.querySelector('.Post-Title, h1')?.innerText,
      content: document.querySelector('.Post-RichText, .RichContent-inner')?.innerText,
      author: document.querySelector('.UserLink-link, .AuthorInfo-name')?.innerText,
      votes: document.querySelector('.VoteButton--up .CountValue')?.innerText
    })
  `
})

掘金

掘金

typescript
const juejinContent = await javascript_tool({
  tabId,
  action: "javascript_exec",
  text: `
    JSON.stringify({
      title: document.querySelector('.article-title')?.innerText,
      content: document.querySelector('.article-content, .markdown-body')?.innerText,
      author: document.querySelector('.user-name')?.innerText,
      views: document.querySelector('.view-count')?.innerText
    })
  `
})
typescript
const juejinContent = await javascript_tool({
  tabId,
  action: "javascript_exec",
  text: `
    JSON.stringify({
      title: document.querySelector('.article-title')?.innerText,
      content: document.querySelector('.article-content, .markdown-body')?.innerText,
      author: document.querySelector('.user-name')?.innerText,
      views: document.querySelector('.view-count')?.innerText
    })
  `
})

Medium

Medium

typescript
const mediumContent = await javascript_tool({
  tabId,
  action: "javascript_exec",
  text: `
    JSON.stringify({
      title: document.querySelector('h1')?.innerText,
      content: document.querySelector('article')?.innerText,
      author: document.querySelector('[data-testid="author-name"]')?.innerText,
      claps: document.querySelector('[data-testid="clap-count"]')?.innerText
    })
  `
})

typescript
const mediumContent = await javascript_tool({
  tabId,
  action: "javascript_exec",
  text: `
    JSON.stringify({
      title: document.querySelector('h1')?.innerText,
      content: document.querySelector('article')?.innerText,
      author: document.querySelector('[data-testid="author-name"]')?.innerText,
      claps: document.querySelector('[data-testid="clap-count"]')?.innerText
    })
  `
})

绕过常见反爬机制

绕过常见反爬机制

1. User-Agent 检测

1. User-Agent 检测

typescript
await javascript_tool({
  tabId,
  action: "javascript_exec",
  text: `
    Object.defineProperty(navigator, 'userAgent', {
      get: () => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'
    });
  `
})
typescript
await javascript_tool({
  tabId,
  action: "javascript_exec",
  text: `
    Object.defineProperty(navigator, 'userAgent', {
      get: () => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'
    });
  `
})

2. WebDriver 检测

2. WebDriver 检测

typescript
await javascript_tool({
  tabId,
  action: "javascript_exec",
  text: `
    // 移除 webdriver 标记
    delete navigator.__proto__.webdriver
    Object.defineProperty(navigator, 'webdriver', {
      get: () => undefined
    })
  `
})
typescript
await javascript_tool({
  tabId,
  action: "javascript_exec",
  text: `
    // 移除 webdriver 标记
    delete navigator.__proto__.webdriver
    Object.defineProperty(navigator, 'webdriver', {
      get: () => undefined
    })
  `
})

3. 懒加载内容

3. 懒加载内容

typescript
// 滚动页面触发懒加载
await javascript_tool({
  tabId,
  action: "javascript_exec",
  text: `
    async function scrollToBottom() {
      const scrollHeight = document.body.scrollHeight
      const steps = 5
      for (let i = 0; i < steps; i++) {
        window.scrollTo(0, (scrollHeight / steps) * (i + 1))
        await new Promise(r => setTimeout(r, 500))
      }
    }
    scrollToBottom()
  `
})
typescript
// 滚动页面触发懒加载
await javascript_tool({
  tabId,
  action: "javascript_exec",
  text: `
    async function scrollToBottom() {
      const scrollHeight = document.body.scrollHeight
      const steps = 5
      for (let i = 0; i < steps; i++) {
        window.scrollTo(0, (scrollHeight / steps) * (i + 1))
        await new Promise(r => setTimeout(r, 500))
      }
    }
    scrollToBottom()
  `
})

4. 弹窗处理

4. 弹窗处理

typescript
await javascript_tool({
  tabId,
  action: "javascript_exec",
  text: `
    // 关闭所有弹窗
    document.querySelectorAll('.modal, .popup, .dialog, [role="dialog"]')
      .forEach(el => el.style.display = 'none')
  `
})

typescript
await javascript_tool({
  tabId,
  action: "javascript_exec",
  text: `
    // 关闭所有弹窗
    document.querySelectorAll('.modal, .popup, .dialog, [role="dialog"]')
      .forEach(el => el.style.display = 'none')
  `
})

错误处理

错误处理

typescript
async function safeExtract(url) {
  try {
    const context = await tabs_context_mcp({ createIfEmpty: true })
    const tabId = context.availableTabs[0].tabId

    await navigate({ tabId, url })

    // 检查是否被阻止
    const blocked = await javascript_tool({
      tabId,
      action: "javascript_exec",
      text: `
        const indicators = [
          '.access-denied',
          '.error-403',
          '.unauthorized',
          '[data-blocked]'
        ]
        indicators.some(s => document.querySelector(s))
      `
    })

    if (blocked) {
      throw new Error('访问被阻止,可能需要特殊处理')
    }

    // 提取内容
    return await get_page_text({ tabId })

  } catch (error) {
    console.error('提取失败:', error.message)
    throw error
  }
}

typescript
async function safeExtract(url) {
  try {
    const context = await tabs_context_mcp({ createIfEmpty: true })
    const tabId = context.availableTabs[0].tabId

    await navigate({ tabId, url })

    // 检查是否被阻止
    const blocked = await javascript_tool({
      tabId,
      action: "javascript_exec",
      text: `
        const indicators = [
          '.access-denied',
          '.error-403',
          '.unauthorized',
          '[data-blocked]'
        ]
        indicators.some(s => document.querySelector(s))
      `
    })

    if (blocked) {
      throw new Error('访问被阻止,可能需要特殊处理')
    }

    // 提取内容
    return await get_page_text({ tabId })

  } catch (error) {
    console.error('提取失败:', error.message)
    throw error
  }
}

输出格式

输出格式

Markdown 格式

Markdown 格式

markdown
undefined
markdown
undefined

文章标题

文章标题

作者: 作者名称 发布时间: 2024-01-15
文章正文内容...

来源:链接
undefined
作者: 作者名称 发布时间: 2024-01-15
文章正文内容...

来源:链接
undefined

JSON 格式

JSON 格式

json
{
  "title": "文章标题",
  "author": "作者名称",
  "publishDate": "2024-01-15",
  "content": "完整文章内容...",
  "images": ["url1", "url2"],
  "metadata": {
    "url": "https://example.com/article",
    "wordCount": 1500,
    "readTime": 5
  }
}

json
{
  "title": "文章标题",
  "author": "作者名称",
  "publishDate": "2024-01-15",
  "content": "完整文章内容...",
  "images": ["url1", "url2"],
  "metadata": {
    "url": "https://example.com/article",
    "wordCount": 1500,
    "readTime": 5
  }
}

最佳实践

最佳实践

  1. 使用合适的等待时间 - 动态内容需要等待加载
  2. 模拟真实用户行为 - 随机延迟、鼠标移动
  3. 处理特殊情况 - 登录、付费墙、地区限制
  4. 尊重网站规则 - 遵守 robots.txt
  5. 设置合理的请求频率 - 避免被封禁
  6. 使用缓存 - 避免重复请求

  1. 使用合适的等待时间 - 动态内容需要等待加载
  2. 模拟真实用户行为 - 随机延迟、鼠标移动
  3. 处理特殊情况 - 登录、付费墙、地区限制
  4. 尊重网站规则 - 遵守 robots.txt
  5. 设置合理的请求频率 - 避免被封禁
  6. 使用缓存 - 避免重复请求

常见问题

常见问题

Q: 如何处理需要登录的内容?

Q: 如何处理需要登录的内容?

A: 使用已登录的浏览器实例,或者在代码中实现登录流程。
A: 使用已登录的浏览器实例,或者在代码中实现登录流程。

Q: 微信文章显示"请在微信中打开"?

Q: 微信文章显示"请在微信中打开"?

A: 需要设置微信 User-Agent,并可能需要处理登录态。
A: 需要设置微信 User-Agent,并可能需要处理登录态。

Q: 如何提高提取成功率?

Q: 如何提高提取成功率?

A:
  1. 使用最新版本的 Chrome DevTools MCP
  2. 设置正确的启动参数
  3. 模拟真实浏览器行为
  4. 处理 JavaScript 渲染的内容

A:
  1. 使用最新版本的 Chrome DevTools MCP
  2. 设置正确的启动参数
  3. 模拟真实浏览器行为
  4. 处理 JavaScript 渲染的内容

快速参考

快速参考

使用 Readability 提取文章(推荐)

使用 Readability 提取文章(推荐)

bash
undefined
bash
undefined

在 Claude Code 中使用

在 Claude Code 中使用

提取这个网页的内容:https://example.com/article

Claude 会自动:
1. 打开浏览器标签页
2. 加载 Readability.js 库
3. 提取文章内容
4. 返回结构化数据(标题、正文、作者、图片等)
提取这个网页的内容:https://example.com/article

Claude 会自动:
1. 打开浏览器标签页
2. 加载 Readability.js 库
3. 提取文章内容
4. 返回结构化数据(标题、正文、作者、图片等)

技术栈

技术栈

  • Chrome DevTools MCP - 浏览器控制
  • Readability.js v0.5.0 - 文章提取算法(Mozilla)
  • 自定义提取器 - 特殊网站支持

  • Chrome DevTools MCP - 浏览器控制
  • Readability.js v0.5.0 - 文章提取算法(Mozilla)
  • 自定义提取器 - 特殊网站支持

版本更新日志

版本更新日志

v2.0.0 (2025-12-28)

v2.0.0 (2025-12-28)

重大更新
  • ✅ 升级 Readability.js 至 v0.6.0(最新版本)
  • ✅ 新增
    isProbablyReaderable
    快速预检测功能
  • ✅ 新增 Readability 原生字段支持:
    • publishedTime
      - 自动提取发布时间
    • dir
      - 文本方向(ltr/rtl)
    • lang
      - 内容语言代码
  • ✅ 新增配置选项:
    linkDensityModifier
    (链接密度修正)
  • ✅ 增强 SEO 元数据提取(新增 Twitter Card 完整字段)
  • ✅ 返回数据中新增
    readerability
    预检测结果
  • ✅ 完善文档:新增配置选项详解、最佳实践、调试技巧
性能优化
  • 🚀 使用
    isProbablyReaderable
    预筛选,提升批量提取效率 90%
  • 🚀 优化脚本加载顺序,先加载 Readability-readerable.js
  • 🚀 改进错误处理和降级策略
文档改进
  • 📖 新增
    isProbablyReaderable
    完整使用指南
  • 📖 新增 Readability 所有配置选项详细说明
  • 📖 新增针对不同网站类型的提取策略表
  • 📖 新增最佳实践章节(7 个方向)
  • 📖 新增内容质量验证方法
  • 📖 新增调试技巧和配置对比工具
Breaking Changes
  • ⚠️ 返回数据结构新增
    readerability
    字段
  • ⚠️ 返回数据结构新增
    readabilityVersion
    字段
  • ⚠️ 部分字段名调整以匹配 Readability 原生字段
重大更新
  • ✅ 升级 Readability.js 至 v0.6.0(最新版本)
  • ✅ 新增
    isProbablyReaderable
    快速预检测功能
  • ✅ 新增 Readability 原生字段支持:
    • publishedTime
      - 自动提取发布时间
    • dir
      - 文本方向(ltr/rtl)
    • lang
      - 内容语言代码
  • ✅ 新增配置选项:
    linkDensityModifier
    (链接密度修正)
  • ✅ 增强 SEO 元数据提取(新增 Twitter Card 完整字段)
  • ✅ 返回数据中新增
    readerability
    预检测结果
  • ✅ 完善文档:新增配置选项详解、最佳实践、调试技巧
性能优化
  • 🚀 使用
    isProbablyReaderable
    预筛选,提升批量提取效率 90%
  • 🚀 优化脚本加载顺序,先加载 Readability-readerable.js
  • 🚀 改进错误处理和降级策略
文档改进
  • 📖 新增
    isProbablyReaderable
    完整使用指南
  • 📖 新增 Readability 所有配置选项详细说明
  • 📖 新增针对不同网站类型的提取策略表
  • 📖 新增最佳实践章节(7 个方向)
  • 📖 新增内容质量验证方法
  • 📖 新增调试技巧和配置对比工具
Breaking Changes
  • ⚠️ 返回数据结构新增
    readerability
    字段
  • ⚠️ 返回数据结构新增
    readabilityVersion
    字段
  • ⚠️ 部分字段名调整以匹配 Readability 原生字段

v1.0.0 (2025-11-15)

v1.0.0 (2025-11-15)

初始版本
  • ✅ 集成 Mozilla Readability.js v0.5.0
  • ✅ 支持微信公众号等有安全限制的网站
  • ✅ 提供多种提取方法(Readability、简化算法、自定义)
  • ✅ 增强元数据提取(SEO、Open Graph)
  • ✅ 支持图片、标题结构提取
  • ✅ 自动计算阅读时长

初始版本
  • ✅ 集成 Mozilla Readability.js v0.5.0
  • ✅ 支持微信公众号等有安全限制的网站
  • ✅ 提供多种提取方法(Readability、简化算法、自定义)
  • ✅ 增强元数据提取(SEO、Open Graph)
  • ✅ 支持图片、标题结构提取
  • ✅ 自动计算阅读时长

快速开始

快速开始

使用示例(v2.0.0)

使用示例(v2.0.0)

typescript
// 方式 1: 使用优化后的提取脚本(推荐)
const readabilityScript = await fs.readFile(
  '~/.claude/skills/web-article-extractor/scripts/readability_extractor.js',
  'utf8'
);

const result = await javascript_tool({
  tabId,
  action: "javascript_exec",
  text: readabilityScript
});

const article = JSON.parse(result);

// 检查提取质量
console.log('是否适合提取:', article.readerability.isReaderable);
console.log('内容长度:', article.contentLength);
console.log('阅读时长:', article.readingTime, '分钟');

// 方式 2: 使用 isProbablyReaderable 预检测
const isReaderable = isProbablyReaderable(document, {
  minContentLength: 140,
  minScore: 20
});

if (isReaderable) {
  // 执行完整提取
  const reader = new Readability(document.cloneNode(true), {
    charThreshold: 500,
    keepClasses: false,
    disableJSONLD: false
  });
  const article = reader.parse();
}

typescript
// 方式 1: 使用优化后的提取脚本(推荐)
const readabilityScript = await fs.readFile(
  '~/.claude/skills/web-article-extractor/scripts/readability_extractor.js',
  'utf8'
);

const result = await javascript_tool({
  tabId,
  action: "javascript_exec",
  text: readabilityScript
});

const article = JSON.parse(result);

// 检查提取质量
console.log('是否适合提取:', article.readerability.isReaderable);
console.log('内容长度:', article.contentLength);
console.log('阅读时长:', article.readingTime, '分钟');

// 方式 2: 使用 isProbablyReaderable 预检测
const isReaderable = isProbablyReaderable(document, {
  minContentLength: 140,
  minScore: 20
});

if (isReaderable) {
  // 执行完整提取
  const reader = new Readability(document.cloneNode(true), {
    charThreshold: 500,
    keepClasses: false,
    disableJSONLD: false
  });
  const article = reader.parse();
}

技术栈

技术栈

  • Chrome DevTools MCP - 浏览器控制和页面操作
  • Readability.js v0.6.0 - Mozilla 文章提取算法
  • Readability-readerable.js - 快速可读性检测
  • 自定义提取器 - 特殊网站支持(微信、知乎等)

  • Chrome DevTools MCP - 浏览器控制和页面操作
  • Readability.js v0.6.0 - Mozilla 文章提取算法
  • Readability-readerable.js - 快速可读性检测
  • 自定义提取器 - 特殊网站支持(微信、知乎等)

参考资源

参考资源


最后更新: 2025-12-28 | 版本: 2.0.0 | 作者: AI 技能开发团队

最后更新: 2025-12-28 | 版本: 2.0.0 | 作者: AI 技能开发团队