🎯 一款可定制、具备反检测功能的云浏览器,由自主研发的 Chromium驱动,专为网页爬虫AI 代理设计。👉立即试用
返回博客

TypeScript网页爬虫:使用Cheerio和Node的类型化提取

Isabella Garcia
Isabella Garcia

Web Data Collection Specialist

21-Jul-2026

TL;DR:

  • Node 22 直接使用 node --experimental-strip-types 运行 TypeScript,因此爬虫无需构建步骤和打包工具。
  • fetch 内置于运行时,使得 cheerio 成为解析 HTML 和 CSS 选择的唯一依赖项。
  • 类型化你提取的记录使爬虫可维护:编译器在使用重命名字段时标记错误,而不是在数据已经下游传送后才标记。
  • 类型描述的是你期望的结构,而不是你接收到的页面——客户端渲染的页面仍然会返回一个有效的响应,但解析为零条记录。
  • Scrapeless 通用抓取 API 首先渲染页面,而相同的不变 cheerio 选择器随后返回所有 10 条记录。
  • Scrapeless 免费计划 开始,并将支点示例指向你自己的目标。

TypeScript 在爬虫中的地位只因一个原因:你提取的数据有一个结构,而这个结构会漂移。一个网站重命名字段,一个选择器开始返回空字符串,而一个普通的 JavaScript 爬虫会默默地将损害带入任何消费它的地方。一个类型化的记录将其转化为编译错误。

最近改变的是设置成本。Node 22 原生移除类型,因此没有 tsc 步骤,没有打包工具,也没有 ts-node 在依赖树中。

你需要的

在 2026 年进行 TypeScript 网络抓取需要 Node 22 或更高版本和一个依赖。下面的版本是这些示例运行时的版本:

组件 版本 作用
Node.js 22.22.3 运行时、原生 fetch、原生类型剥离
cheerio 1.2.0 HTML 解析和 CSS 选择器

fetch 内置于运行时,因此不需要导入也不需要 HTTP 库。cheerio 提供了一个类似 jQuery 的 API 用于解析文档,这是 Node 生态系统中服务器端 HTML 查询的最接近标准的东西。它根据 HTML 解析规范 来解析,而不是将标记当作文本来匹配模式。

安装

创建项目并添加这一个依赖:

bash Copy
mkdir ts-scraper && cd ts-scraper
npm init -y
npm pkg set type=module
npm install cheerio@1.2.0

type=module 设置是重要的:下面的示例使用了顶级 await,这需要 ES 模块语法。

提取类型化记录

首先声明结构,然后使提取产生该结构。编译器会对此进行约束:

typescript Copy
import * as cheerio from "cheerio";

interface Quote {
  text: string;
  author: string;
  tags: string[];
}

const res = await fetch("https://quotes.toscrape.com/");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const $ = cheerio.load(await res.text());

const quotes: Quote[] = $("div.quote")
  .map((_, el) => ({
    text: $(el).find("span.text").text(),
    author: $(el).find("small.author").text(),
    tags: $(el).find("a.tag").map((_, t) => $(t).text()).get(),
  }))
  .get();

console.log(`解析的 quotes 数量: ${quotes.length}`);
console.log(JSON.stringify(quotes[0], null, 2));

运行它时没有构建步骤:

bash Copy
node --experimental-strip-types static.ts
text Copy
解析的 quotes 数量: 10
{
  "text": "“我们所创造的世界是我们思维的过程。没有改变我们的思维,世界是无法改变的。”",
  "author": "阿尔伯特·爱因斯坦",
  "tags": [
    "改变",
    "深思",
    "思维",
    "世界"
  ]
}

其中有三件事情在真正发挥作用。

quotes 上的 Quote 注释使 .map() 回调进行了类型检查。返回缺少 tags 的对象,或者拼写为 tag,错误将在该行出现,而不是在任何消费数组的地方后来显示为 undefined

res.ok 是人们跳过的检查。fetch 在 404 或 403 上不会抛出异常——它会正常解析并将 ok 设置为 false,错误页面解析为零条匹配,正如一个空结果一样。表现如此的状态类定义在 HTTP 语义规范 中。

嵌套的 .map(...).get() 是 cheerio 将选择转换为真正数组的习惯用法。内部调用收集标签字符串,因此 tags 作为 string[] 到达,而不是作为 cheerio 对象。

类型帮助到此为止

类型描述的是你期望的记录,而不是你接收到的页面。fetchcheerio 都不会运行 JavaScript,因此在一个在浏览器中构建内容的页面上,选择器不会匹配任何内容,类型由一个空数组满足。

上面的站点在 /js/ 上发布了相同数据的客户端渲染双胞胎。指向它的同样解析代码:

typescript Copy
import * as cheerio from "cheerio";

const res = await fetch("https://quotes.toscrape.com/js/");

如果 (!res.ok) throw new Error(HTTP ${res.status});
const html = await res.text();
const $ = cheerio.load(html);

console.log(html 字节数: ${html.length});
console.log(解析的引用: ${$("div.quote").length});

Copy
```text
html 字节数: 5806
解析的引用: 0

请求成功,res.ok 为真,解析了 5,806 个有效 HTML 字符,而没有出现任何问题。 Quote[] 是一个完全类型安全的空数组。这是值得设计应对的失败模式,因为类型系统或 HTTP 层的任何内容都不会报告它——引用标记是在脚本运行后写入 DOM 的。

先渲染,再解析

Scrapeless 通用抓取 API 通过在云浏览器中渲染页面并返回生成的 HTML 来填补这个空白,因此 TypeScript 端保持一个类型化的 fetch 调用。

设置你的密钥:

bash Copy
export SCRAPELESS_API_KEY="您的_api_key_在这里"

只有抓取层发生了变化——Quote 接口和选择器代码与第一个示例相同:

typescript Copy
import * as cheerio from "cheerio";

interface Quote {
  text: string;
  author: string;
  tags: string[];
}

const res = await fetch("https://api.scrapeless.com/api/v2/unlocker/request", {
  method: "POST",
  headers: {
    "x-api-token": process.env.SCRAPELESS_API_KEY!,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    actor: "unlocker.webunlocker",
    input: {
      url: "https://quotes.toscrape.com/js/",
      js_render: true,
      headless: true,
    },
  }),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);

const envelope: { data: string } = await res.json();
const $ = cheerio.load(envelope.data);

const quotes: Quote[] = $("div.quote")
  .map((_, el) => ({
    text: $(el).find("span.text").text(),
    author: $(el).find("small.author").text(),
    tags: $(el).find("a.tag").map((_, t) => $(t).text()).get(),
  }))
  .get();

console.log(`html 字节数: ${envelope.data.length}`);
console.log(`解析的引用: ${quotes.length}`);
console.log(`第一位作者: ${quotes[0]?.author}`);
text Copy
html 字节数: 8940
解析的引用: 10
第一位作者: 阿尔伯特·爱因斯坦

同一页面,同样的选择器,同样的接口。记录数从 0 变为 10,负载从 5,806 增长到 8,940 字符,唯一的区别是获取 HTML 的层次。

这个调用中的两个 TypeScript 细节值得复制。将信封注释为 { data: string } 可以阻止 await res.json() 在文件的其余部分传播 any,这正是类型安全通常在抓取器中泄漏的地方。而 quotes[0]?.author 尊重数组索引可能为 undefined 的事实——在启用 noUncheckedIndexedAccess 时,编译器对此有要求。

data 字段将渲染文档作为字符串保存,这就是它直接传入 cheerio.load 的原因。渲染选项在 Scrapeless 文档 中covered,而相同的 js_render 行为在 JS 渲染指南 中有进一步探讨。

排查故障

.ts 文件上的 ERR_UNKNOWN_FILE_EXTENSION 缺少 --experimental-strip-types 标志,或者 Node 版本低于 22。类型剥离在加载时移除注释;它不进行类型检查,因此当你想获得编译器的看法时,请单独运行 tsc --noEmit

Cannot use import statement outside a module 包缺少 "type": "module"。顶层 await 需要 ES 模块。

类型剥离拒绝枚举或参数属性。 这些构造生成实际的运行时代码,而不是可擦除的,因此剥离无法处理它们。使用字符串文字的联合体代替枚举,并明确分配构造函数字段。

在浏览器中匹配选择器但在脚本中不匹配。 应与查看源代码进行比较,而不是检查器。检查器显示脚本运行后的 DOM,这不是 fetch 接收到的——首先打印响应长度,就像上面的示例一样。

在将其指向实时目标之前,请检查网站的条款及其 /robots.txt 指令,这符合 机器人排除协议标准,并将收集限制在网站可舒适提供的公共数据量内。

结论

TypeScript 为抓取器提供了一份合同:声明记录,编译器会告诉你提取何时不再满足它。借助 Node 22 本机剥离类型和内置的 fetch,该合同只需一个依赖项且无需构建步骤。
无法告诉你的是,您获取的页面是否包含数据。这一检查必须是明确的——一个类型良好的空数组就是客户端渲染页面返回的结果,它看起来与没有结果的页面完全相同。衡量差异是一种值得保持的习惯:服务器渲染了 10 条记录,JavaScript 双胞胎显示 0 条记录,一旦在 cheerio 看到之前有东西渲染了页面,又是 10 条记录。

从 Scrapeless 免费计划开始 针对您自己的目标运行渲染步骤,并在您确定工作规模时查看当前的 Scrapeless 定价

常见问题解答

问:我需要编译 TypeScript 才能使用它进行抓取吗?

不需要。Node 22 及更高版本可以直接使用 node --experimental-strip-types 运行 .ts 文件,这会在加载时移除类型注释。这意味着没有构建步骤,也不需要打包器来进行抓取。类型剥离不会检查类型,因此当您希望编译器实际验证时,请在 CI 中运行 tsc --noEmit

问:我应该使用哪个 HTML 解析库与 TypeScript 配合使用?

cheerio 涵盖了大部分工作——它使用符合 HTML 规范的解析器进行解析,并提供带有 TypeScript 定义的 jQuery 风格选择器 API。当内容通过脚本写入 DOM 时,没有任何解析器能独立恢复,因此请寻找无头浏览器或渲染 API。

问:在 Node 中,fetch 在 404 时会抛出异常吗?

不会,这会让人感到困惑。fetch 对任何 HTTP 响应都会正常解析,仅在网络级故障时会拒绝,所以您必须自己检查 res.ok。没有该检查,错误页面会解析为零匹配,无法与确实没有结果的页面区分开来。

问:当响应为 JSON 时,我该如何保持类型的准确性?

在边界处进行注释。await res.json() 返回 any,因此将其分配给如 const envelope: { data: string } 的类型变量,可以阻止 any 在文件的其他部分传播。对于不可信的上游数据,请使用模式库在运行时进行验证,而不是仅依赖注释。

问:TypeScript 能否抓取在浏览器中渲染的页面?

不能单独完成。该语言对 JavaScript 是否执行没有影响——fetch 返回服务器发送的字节,上面的示例显示这些字节在客户端渲染页面上解析为零记录。渲染必须在别处进行,无论是您操作的无头浏览器,还是通过返回已渲染 DOM 的 API。

在Scrapeless,我们仅访问公开可用的数据,并严格遵循适用的法律、法规和网站隐私政策。本博客中的内容仅供演示之用,不涉及任何非法或侵权活动。我们对使用本博客或第三方链接中的信息不做任何保证,并免除所有责任。在进行任何抓取活动之前,请咨询您的法律顾问,并审查目标网站的服务条款或获取必要的许可。

最受欢迎的文章

目录