Reader 实战:给 Node.js 应用接入网页正文提取
用 ReaderClient 批量处理文章 URL,统一标题、正文与来源字段,保留失败去向,并正确关闭浏览器资源。
把几个网页接入搜索或摘要系统,常见做法是每个业务函数都写一遍页面加载和正文清理。结果是输出字段不一致,失败时有的返回空字符串,有的抛异常,浏览器资源也容易漏关。
本文用 @vakra-dev/reader 把明确给出的 URL 整理为统一记录。输出包含标题、Markdown、来源、采集时间与状态,便于下游决定是否继续处理。这里的 Reader 指 vakra-dev/reader,不是名字相近的远程 Reader API。
准备输入地址
先在浏览器找到下列页面并确认内容,再从地址栏复制完整地址。服务或仓库地址从相应账户设置、官方项目的接入说明或克隆菜单取得。不要从示例输出中反推真实地址。
DOC_URL_1:Python 教程 introduction.html 页的完整地址。DOC_URL_2:同目录 controlflow.html 页的完整地址。
在执行后续命令的同一个 Bash/WSL 终端中设置:
export DOC_URL_1='<paste-the-actual-doc-url-1>'
export DOC_URL_2='<paste-the-actual-doc-url-2>'下文 JSON 或任务参数中的 <变量名> 是待替换标记,提交前填写刚复制的实际值;JSON 不会自动展开环境变量。结果示例中的 example.16yun.cn 只表示来源字段的形状,不是本文部署的演示站。
安装并确认依赖边界
准备 Node.js 24。在新目录安装 Reader 0.3.5,并按项目要求准备 Chromium:
mkdir article-reader
cd article-reader
npm init -y
npm install @vakra-dev/reader@0.3.5
npx playwright install chromium包的 HTTP/浏览器提取逻辑在本地运行,首次使用仍需下载浏览器。安装包成功不代表机器已有可执行 Chromium;浏览器下载失败应先处理安装问题,不要把它计为文章正文为空。
保留锁文件。如果项目已经有自己的 Playwright 依赖,先检查版本和浏览器缓存路径,避免更换运行账户后找不到此前安装的二进制。
约定一条文章记录
下游不应读取多个互相冲突的成功字段。本文统一记录 status,只有正文非空、标题存在、HTTP 状态符合预期时才标为 accepted。失败仍保留输入地址和原因。
标题来自 Reader 的网页元数据,Markdown 来自请求的格式,来源保留请求 URL 与 Reader 提供的 baseUrl。不要把 canonical 链接当成无条件覆盖来源的依据:canonical 可能指向另一版本页面。
一段完整的批量接入代码
创建 read_articles.mts:
import { writeFile } from 'node:fs/promises';
import { ReaderClient } from '@vakra-dev/reader';
class ArticleRejected extends Error {
code: string;
constructor(code: string) {
super(code);
this.name = 'ArticleRejected';
this.code = code;
}
}
const urls = process.argv.slice(2);
if (!urls.length) throw new Error('请在命令行提供至少一个文章 URL');
for (const value of urls) {
if (!['http:', 'https:'].includes(new URL(value).protocol)) {
throw new Error('只接受 HTTP(S) 页面');
}
}
const reader = new ReaderClient();
const records: unknown[] = [];
try {
for (const requestedUrl of urls) {
try {
const result = await reader.scrape({
urls: [requestedUrl],
formats: ['markdown'],
});
const item = result.data[0];
if (!item) throw new ArticleRejected('reader_returned_no_document');
const title = item.metadata.website.title?.trim();
const markdown = item.markdown?.trim();
if (item.metadata.statusCode !== 200) {
throw new ArticleRejected('unexpected_http_status');
}
if (!title || !markdown) throw new ArticleRejected('missing_title_or_body');
records.push({
requested_url: requestedUrl,
source_url: item.metadata.baseUrl,
title,
markdown,
observed_at: item.metadata.scrapedAt,
engine: item.metadata.engine,
status: 'accepted',
});
} catch (error) {
records.push({
requested_url: requestedUrl,
status: 'failed',
reason: error instanceof ArticleRejected ? error.code : 'reader_call_failed',
error_type: error instanceof Error ? error.name : 'UnknownError',
});
}
}
} finally {
await reader.close();
}
await writeFile('articles.json', JSON.stringify(records, null, 2));
console.log({ inputs: urls.length, records: records.length });运行时显式提供来源:
node read_articles.mts "$DOC_URL_1" "$DOC_URL_2"这是文档页面示例,不承诺目标站始终可访问。你可以替换为自己的文章页。每个输入获得一条 accepted 或 failed 记录,不会因为某页异常而悄悄从结果消失。
代码按顺序处理便于定位问题,同时复用一个 ReaderClient。项目也支持一次 scrape 提交多个 URL 与 batchConcurrency;扩大并发前,先验证输出完整度,并按资源和目标响应调整,而不是把 URL 数量直接当作并发数。
如何检查正文是否可用
articles.json 中的合格记录结构如下,字段值是示意:
{
"requested_url": "https://example.16yun.cn/articles/storage",
"source_url": "https://example.16yun.cn/articles/storage",
"title": "存储使用指南",
"markdown": "# 存储使用指南\n\n页面正文……",
"observed_at": "2026-10-28T09:00:00Z",
"engine": "由 Reader 返回",
"status": "accepted"
}先比较输入数和结果数,再人工检查几页:标题是否对应目标文档,正文是否包含主要章节,是否混入菜单或登录提示。示例的非空检查只是入库的最低条件,不是自动事实验收。
对于固定站点,应增加该站正文身份规则,例如明确的文章标题区域、文档版本或预期章节。不要用“正文超过 100 字”替代页面身份判断,错误页也可能很长。
observed_at 使用库返回的开始时间,不代表网页最后修改时间。需要新鲜度判断时,另存页面声明的更新时间,并区分它与本次采集时间。
放进已有服务时怎样管理 ReaderClient
在一个批处理里创建一次客户端,在 finally 关闭。若放到长期运行的 Node 服务中,由应用生命周期持有客户端:启动时初始化,任务期间复用,服务退出时关闭。不要每接收一条 URL 就反复创建客户端,也不要让每个请求都关闭其他请求正在使用的共享客户端。
本文给的是命令行接入示例,没有对外暴露任意 URL 抓取 API。若后续让不受信任用户提交 URL,需要在服务边界控制可访问范围,不能只凭协议检查就允许访问内部网络。
代理配置也只在目标网络出口确实有要求时添加。正文清理失败、空标题或错误元数据,不应先被归因成代理节点不可用。
常见问题与修复
找不到浏览器。 使用运行程序的同一账户重新确认 Chromium 安装与缓存位置。容器内的浏览器与宿主机缓存不是同一个环境,修复后应先能启动页面,再检查提取规则。
没有 data[0]。 查看库的 batchMetadata.errors 和受控日志,判断下载、浏览器或转换阶段是否失败。示例用 reason 区分没有文档、HTTP 状态不符、标题或正文缺失;库调用异常记为 reader_call_failed,并保留 error_type。诊断时可临时查看脱敏细节,不把代理密码或敏感 URL 写进公共日志。
Markdown 有菜单没有正文。 回到原页面确认是否需要登录、正文是否动态加载,以及提取器是否选错区域。必要时先用浏览器取得正确材料,再做内容清理;增加并发不能修复材料缺失。
返回 200 仍是登录提示。 增加目标页面身份检查,修复会话访问条件。不能因为标题非空就把提示页作为文章接入知识库。
程序完成后仍有资源占用。 检查所有退出路径是否经过 reader.close(),以及应用是否还持有其他浏览器会话。不要通过无差别终止全部 Chrome 进程清理当前任务。
重复或旧文混入结果。 检查输入 URL 的语言、版本与参数。保留请求地址和来源字段,按业务文档身份管理版本,不按标题相同就覆盖。
接入正文提取库最有用的设计,是让调用方始终知道处理了哪个输入、拿到了什么内容、哪些输入需要重试。把这些约定建立好,后续换提取器或增加站点时,下游就不必跟着重写。
需要企业代理方案?
我们可根据目标站点、并发规模与稳定性目标提供定制方案。