npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

readability-plus

v1.0.1

Published

Enhanced article locator based on Mozilla Readability with Chinese site support

Readme

ReadabilityPlus

English

从任何网页中精准提取文章内容——专为中文互联网优化

基于 Mozilla Readability 的增强版文章定位器。两轮检测 + 中文站点识别 + 标题清洗,让文章提取不再误判、不再遗漏、不再噪声。

为什么需要 ReadabilityPlus?

想象一下:你正在构建一个 AI 阅读助手,需要从网页中提取文章内容喂给大模型。你选择了 Mozilla Readability——这是 Firefox 阅读模式背后的引擎,黄金标准。但很快你发现问题来了:

场景 1:误判——首页被当成文章

用户访问:https://www.csdn.net
Readability:✅ "检测到文章!" → 提取了整个首页的内容
实际:❌ 这是 CSDN 首页,不是文章

场景 2:标题噪声——站点后缀污染标题

Readability 提取标题:"DeepSeek-R1技术梳理笔记 - CSDN博客"
你期望的标题:"DeepSeek-R1技术梳理笔记"

场景 3:漏检——非标准页面被拒绝

用户访问:一篇结构非标准的博客文章
isProbablyReaderable:❌ "这不是文章"
实际:✅ 页面包含 2000 字的有效文章内容

ReadabilityPlus 完美解决了这些问题:

| 问题 | 原生 Readability | ReadabilityPlus | |------|-----------------|-----------------| | 首页误判 | 经常误判 | 内置中文站点名模式,自动过滤 | | 标题噪声 | 保留站点后缀 | 自动清洗 - CSDN博客_澎湃新闻 等 | | 非标准页面漏检 | 直接拒绝 | 第二轮兜底检测,捕获遗漏文章 | | DOM 安全 | 直接修改原始 DOM | 所有操作使用 cloneNode,原始 DOM 完全不被修改 |

安装

npm install readability-plus

快速开始

Node.js

const { JSDOM } = require("jsdom");
const { locateArticle } = require("readability-plus");

const html = await fetch(url).then(r => r.text());
const doc = new JSDOM(html).window.document;

const result = locateArticle(doc);
if (result.hasArticle) {
  console.log(result.title);       // 清洗后的文章标题
  console.log(result.textContent);  // 纯文本内容
  console.log(result.content);      // HTML 内容(保留 <img>)
}

浏览器注入(Puppeteer / Playwright)

const fs = require("fs");
const browserScript = fs.readFileSync(
  require.resolve("readability-plus/readability-plus.iife.js"),
  "utf-8"
);

// Puppeteer
await page.evaluate(browserScript);
const result = await page.evaluate(() => {
  return window.ReadabilityPlus.locateArticle(document);
});
# Playwright (Python)
import importlib.resources

browser_script = (
    importlib.resources.files("readability_plus")
    .joinpath("readability-plus.iife.js")
    .read_text()
)

page.evaluate(browser_script)
result = page.evaluate("() => window.ReadabilityPlus.locateArticle(document)")

浏览器控制台

  1. 打开任意网页
  2. 按 F12 打开开发者工具
  3. readability-plus.iife.js 的内容粘贴到 Console 中
  4. 执行:
const result = window.ReadabilityPlus.locateArticle(document);
console.log(result);

使用示例

示例 1:CSDN 文章——标题自动清洗

const result = locateArticle(doc);
console.log(result.title);
// 原始标题:"DeepSeek-R1技术梳理笔记 - CSDN博客"
// 输出结果:"DeepSeek-R1技术梳理笔记"
//          ↑ 站点后缀 "- CSDN博客" 已被自动去除

示例 2:CSDN 首页——正确拒绝非文章页

// 访问 https://www.csdn.net(首页,非文章)
const result = locateArticle(doc);
console.log(result);
// { hasArticle: false, confidence: "none", source: "first-round-rejected" }
//          ↑ 站点名 "CSDN - 专业开发者社区" 被识别,拒绝误判

示例 3:非标准结构页面——第二轮兜底捕获

// 某博客页面,DOM 结构不标准,isProbablyReaderable 返回 false
// 但实际包含 2000 字的有效文章
const result = locateArticle(doc);
console.log(result);
// {
//   hasArticle: true,
//   confidence: "medium",        ← 第二轮检测,置信度为 medium
//   source: "second-round",      ← 来源标记为第二轮
//   title: "我的技术博客文章",
//   textContent: "...(2000字正文)...",
//   ...
// }

示例 4:根据置信度做不同处理

const result = locateArticle(doc);

if (result.hasArticle) {
  if (result.confidence === "high") {
    // 第一轮检测通过,结构标准,直接使用
    sendToLLM(result.textContent);
  } else if (result.confidence === "medium") {
    // 第二轮兜底检测,结构非标准,可能需要人工确认
    sendToLLMWithWarning(result.textContent, "低置信度文章");
  }
} else {
  // 确实不是文章页面
  console.log("该页面不包含文章内容");
}

示例 5:搜索引擎结果页——正确过滤

// 访问百度搜索结果页
const result = locateArticle(doc);
console.log(result);
// { hasArticle: false, confidence: "none", source: "both-rounds-rejected" }
//          ↑ "百度一下,你就知道" 被识别为站点名,正确拒绝

示例 6:标题清洗——多种后缀格式

const { cleanTitle } = require("readability-plus");

cleanTitle("封装hook | 掘金");                // "封装hook"
cleanTitle("年中发力真抓实干_澎湃新闻");        // "年中发力真抓实干"
cleanTitle("React 18 新特性 - 简书");          // "React 18 新特性"
cleanTitle("前端面试题 - SegmentFault 思否");   // "前端面试题"
cleanTitle("AI 大模型落地实践 - 36氪");         // "AI 大模型落地实践"

示例 7:站点名识别

const { isLikelySiteName } = require("readability-plus");

// 搜索引擎
isLikelySiteName("百度一下,你就知道", doc);     // true
isLikelySiteName("Google Search", doc);           // true

// 电商首页
isLikelySiteName("淘宝网 - 淘!我喜欢", doc);     // true
isLikelySiteName("京东(JD.COM)-正品低价", doc);   // true

// 社交/视频门户
isLikelySiteName("哔哩哔哩", doc);                // true
isLikelySiteName("微博 - 随时随地发现新鲜事", doc); // true

// 正常文章标题
isLikelySiteName("DeepSeek-R1技术梳理笔记", doc);  // false
isLikelySiteName("2026年前端趋势展望", doc);       // false

示例 8:自定义检测参数

// 提高阈值,只检测长文章(避免短文误判)
const result = locateArticle(doc, {
  minLength: 500,              // 第一轮最少 500 字
  secondRoundMinLength: 1000,  // 第二轮最少 1000 字
  enableSecondRound: true      // 启用第二轮兜底
});

// 关闭第二轮检测(追求速度,只信任第一轮)
const result2 = locateArticle(doc, {
  enableSecondRound: false
});

示例 9:Playwright 批量提取文章

import asyncio
from playwright.async_api import async_playwright

async def extract_articles(urls):
    """批量提取多个 URL 的文章内容"""
    async with async_playwright() as p:
        browser = await p.chromium.launch()
        page = await browser.new_page()

        # 注入 ReadabilityPlus
        with open("readability-plus.iife.js", "r", encoding="utf-8") as f:
            script = f.read()

        results = []
        for url in urls:
            await page.goto(url)
            await page.evaluate(script)

            result = await page.evaluate("""() => {
                const r = window.ReadabilityPlus.locateArticle(document);
                return {
                    url: location.href,
                    hasArticle: r.hasArticle,
                    confidence: r.confidence,
                    title: r.title,
                    textLength: r.textLength
                };
            }""")

            results.append(result)
            print(f"[{result['confidence']}] {result['title']} ({result['textLength']}字)")

        await browser.close()
        return results

# 运行
urls = [
    "https://blog.csdn.net/...",
    "https://juejin.cn/...",
    "https://www.zhihu.com/...",
]
articles = asyncio.run(extract_articles(urls))
# [high] DeepSeek-R1技术梳理笔记 (3245字)
# [high] 封装hook (1876字)
# [medium] 前端面试经验分享 (2150字)

示例 10:与 LLM 集成——构建网页阅读助手

const { JSDOM } = require("jsdom");
const { locateArticle } = require("readability-plus");

async function readWebPage(url) {
  const html = await fetch(url).then(r => r.text());
  const doc = new JSDOM(html).window.document;
  const result = locateArticle(doc);

  if (!result.hasArticle) {
    return { error: "该页面不包含可读文章内容" };
  }

  // 将清洗后的文章内容发送给 LLM
  return {
    title: result.title,
    content: result.textContent,
    metadata: {
      confidence: result.confidence,
      author: result.byline,
      siteName: result.siteName,
      lang: result.lang,
      wordCount: result.textLength
    }
  };
}

// 使用示例
const article = await readWebPage("https://example.com/blog/post");
// {
//   title: "DeepSeek-R1技术梳理笔记",
//   content: "本文将从技术角度...",
//   metadata: { confidence: "high", author: "张三", siteName: "CSDN", ... }
// }

API 文档

locateArticle(document, options?)

在给定文档中定位文章正文。

参数:

| 参数 | 类型 | 默认值 | 说明 | |------|------|--------|------| | document | Document | 必填 | 页面的 document 对象 | | options.minLength | number | 200 | 第一轮检测的最小文本长度 | | options.secondRoundMinLength | number | 500 | 第二轮兜底检测的最小文本长度 | | options.enableSecondRound | boolean | true | 是否启用第二轮检测 |

返回值(检测到文章):

{
  hasArticle: true,
  confidence: "high" | "medium",  // high = 第一轮, medium = 第二轮
  source: "first-round" | "second-round",
  title: "清洗后的文章标题",
  content: "<div>HTML 内容,保留 <img> 标签</div>",
  textContent: "纯文本内容",
  textLength: 1234,
  excerpt: "文章摘要",
  byline: "作者",
  lang: "zh",
  siteName: "站点名称"
}

返回值(未检测到文章):

{
  hasArticle: false,
  confidence: "none",
  source: "first-round-rejected" | "both-rounds-rejected"
}

isLikelySiteName(title, document)

判断标题是否像站点名而非文章标题。

isLikelySiteName("百度一下,你就知道", document);   // true
isLikelySiteName("DeepSeek-R1技术梳理笔记", document);  // false

cleanTitle(title)

去除文章标题中的站点后缀。

cleanTitle("DeepSeek-R1技术梳理笔记 - CSDN博客");  // "DeepSeek-R1技术梳理笔记"
cleanTitle("封装hook | 掘金");                      // "封装hook"
cleanTitle("年中发力真抓实干_澎湃新闻");              // "年中发力真抓实干"

检测策略

页面 DOM
  │
  ├─ 第一轮:isProbablyReaderable() 快速判断
  │   ├─ true → cloneNode → parse() → 验证长度 + 标题 → confidence: "high"
  │   └─ false → 进入第二轮
  │
  └─ 第二轮:cloneNode → parse() 二次确认
      ├─ 长度 ≥ 500 且标题不是站点名 → confidence: "medium"
      └─ 否则 → hasArticle: false
  • 第一轮速度快——利用 isProbablyReaderable() 快速筛选,然后在克隆 DOM 上 parse()
  • 第二轮捕获非标准结构但内容丰富的页面
  • 所有 parse() 调用使用 cloneNode——原始 DOM 完全不被修改

中文站点支持

ReadabilityPlus 内置中文站点名和标题后缀的识别模式:

站点名模式(过滤为非文章页):

  • 搜索引擎:百度、Google、Bing、搜狗、360搜索
  • 电商首页:淘宝、天猫、京东、拼多多
  • 社交/视频门户:哔哩哔哩、抖音、快手、微博、小红书
  • 新闻门户:凤凰网、搜狐、网易、腾讯网
  • 科技媒体:虎嗅、IT之家、钛媒体、极客公园

标题后缀模式(从文章标题中去除):

  • - CSDN博客 | 掘金_澎湃新闻 - 简书 - 知乎
  • - 新浪_少数派 - SegmentFault - 博客园
  • - 36氪 - 虎嗅 - IT之家 - 新华网

构建

修改源文件后重新构建浏览器可注入的 IIFE 包:

npm run build

该命令读取 Readability.jsReadability-readerable.jslocate-article.js,去除 CommonJS 语法,打包为 readability-plus.iife.js

测试

# 完整测试套件(54+ 真实网站,可能需要几分钟)
npm test

# 浏览器版本测试(单元测试 + 6 个真实网站)
npm run test:browser

基准测试

| 指标 | 结果 | |------|------| | 浏览器版本单元测试 | 28/28 通过 | | 浏览器版本真实网站准确率 | 6/6 | | Node.js / 浏览器一致性 | 100% | | 完整真实网站准确率(54 站) | 92.7%(排除 SPA/反爬后 98.0%) |

文件说明

| 文件 | 说明 | |------|------| | locate-article.js | Node.js 版本(CommonJS 模块) | | readability-plus.iife.js | 浏览器注入版本(IIFE,自动生成) | | build-browser.js | 浏览器版本构建脚本 |

许可证

MIT License

Copyright (c) 2026 Yufeng Guo, Qiong Wu (Beijing Fengtong Technology Co., Ltd.)

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


Readability.js 组件遵循 Apache License 2.0,版权归 Mozilla 所有。