如果你常年使用 JavaScript,那么抓取网页结构化数据所需的大部分工具其实已经在手了。Node.js 提供了快速的运行时、庞大的包生态,以及能够毫不费力地处理并发请求的异步 I/O 模型。在此之上再加两个小型库,只需几十行代码就能写出一个可用的爬虫。

本指南将以实用的方式向你展示如何使用 Node.js 构建网络爬虫。我们从标准技术栈出发, 用于 HTTP 请求的 axios 和用于 jQuery 风格 HTML 解析的 cheerio, 构建一个能够抓取页面、选取所需字段、遍历列表并将结果写入 JSON 和 CSV 的爬虫。然后我们会坦诚地讨论普通 HTTP 请求的局限(JavaScript 渲染页面和规模化时的封锁)以及应对方案。

你将构建什么

一个小型 Node.js 脚本,接收 URL,下载 HTML,用 cheerio 解析,并为每个条目提取一条整洁的记录。我们以通用的商品列表布局作为贯穿全文的示例,因为该模式(包含标题、价格和链接的重复卡片)涵盖了大多数真实的爬取任务。完成后你将拥有:

  • 基于 axios 和 cheerio 构建的单页抓取器。
  • 将 CSS 选择器映射到字段的提取器。
  • 遍历分页列表并收集所有行的循环。
  • 输出至 JSON 和 CSV 的写入功能。
  • 针对封锁页面或客户端渲染页面的即插即用升级路径。

为什么用 Node.js 做抓取

Node.js 在浏览器外运行 JavaScript,基于 Chrome 的 V8 引擎,该引擎编译为机器码,运行速度快。其非阻塞、事件驱动的模型天然适合爬取工作, 爬取时大部分时间都在等待网络响应:你可以在单线程上并发发出多个请求,而无需为每个连接创建独立线程。加上 npm 生态,几乎每种解析、队列或存储需求都有经过实战检验的包,你拥有了一个专为此类工作打造的运行时。Netflix 和 PayPal 等公司在生产环境中使用 Node.js,原因正在于此。

静态爬取的两个核心库是 axios(基于 Promise 的 HTTP 客户端)和 cheerio(一个轻量级解析器,无需附加浏览器,即可在服务端 HTML 上提供 jQuery 选择器)。如果你想专门回顾请求部分,请参阅如何在 Node.js 中使用 Fetch API 发送 HTTP 请求

前置条件

没有任何特殊要求,写代码前只需准备三样东西。

基础 JavaScript 和 Node.js 知识。你需要能够编写脚本、从终端运行脚本,以及使用 npm 安装包。async/await 能让代码读起来更清晰,因此对 Promise 有一定了解会有所帮助。

Node.js 18 或更高版本。使用 node --version 检查版本。如果尚未安装,请从 nodejs.org 安装当前 LTS 版本。

代码编辑器。任何编辑器均可,VS Code 是常见选择。

搭建项目

创建文件夹,初始化项目,安装两个依赖。

bash
mkdir node-scraper && cd node-scraper
npm init -y

npm install axios cheerio

要使用现代 import 语法,请在 package.json 中添加 "type": "module"。如果你更习惯 require,下面的代码同样适用于 CommonJS,只需将 import 行替换为 const axios = require("axios") 即可。

第 1 步:获取页面

从下载原始 HTML 开始。axios 在 response.data 上返回响应体。设置一个真实的 User-Agent 请求头,使请求看起来像是来自浏览器而非默认的 Node 客户端, 许多网站会对后者保持警惕。

javascript
import axios from "axios";

const fetchPage = async (url) => {
  const { data } = await axios.get(url, {
    headers: {
      "User-Agent":
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
    },
    timeout: 15000,
  });
  return data;
};

timeout 防止响应缓慢或无响应的主机无限挂起运行。axios 会在任何非 2xx 状态码时拒绝 Promise,因此将调用包裹在 try/catch 中(后面会示范)可以让错误显现而非静默消失。

第 2 步:用 cheerio 解析并提取

cheerio 加载 HTML 字符串,并提供一个行为类似 jQuery 的 $ 函数。你通过 CSS 选择器查找元素并读取其文本或属性。模式是:找到重复容器,然后对每一个提取你关心的字段。

javascript
import * as cheerio from "cheerio";

const parseProducts = (html) => {
  const $ = cheerio.load(html);
  const products = [];

  $(".product-card").each((_, el) => {
    const card = $(el);
    products.push({
      title: card.find("h2.title").text().trim(),
      price: card.find(".price").text().trim(),
      url: card.find("a").attr("href"),
    });
  });

  return products;
};

这里有两个细节值得注意。.text() 返回元素的合并文本,因此 .trim() 用于去除标记留下的空白。读取链接使用 .attr("href") 而非 .text(),因为你需要的值在属性中,而非可见文本中。将选择器(.product-cardh2.title.price)调整为你实际目标页面对应的选择器;在浏览器开发者工具中检查页面以找到正确的选择器。

Selectors are not forever

网站改版时 class 名称会发生变化,上个月还有效的选择器可能悄无声息地返回空字符串。将选择器视为需要持续维护的内容,而非一次性设置完毕。当某个字段返回空值时,重新检查线上页面并更新选择器。定期维护选择器是任何生产级爬虫的正常工作。

第 3 步:遍历列表页

单页抓取只是演示。真实任务需要遍历分页。大多数分页列表在 URL 中暴露页码(?page=2)或"下一页"链接。最简单可靠的方法是遍历已知的页码范围,抓取每一页,解析内容,当某页没有返回任何条目时停止。

javascript
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

const scrapeAll = async (baseUrl, maxPages = 10) => {
  const all = [];

  for (let page = 1; page <= maxPages; page++) {
    try {
      const html = await fetchPage(`${baseUrl}?page=${page}`);
      const rows = parseProducts(html);
      if (rows.length === 0) break;
      all.push(...rows);
      console.log(`Page ${page}: ${rows.length} items`);
    } catch (err) {
      console.error(`Page ${page} failed: ${err.message}`);
    }
    await sleep(1000);
  }

  return all;
};

请求之间的 sleep 并非可有可无的点缀。1 秒的暂停防止你在紧密循环中轰炸服务器,这既是礼貌之举,也是避免被限速的最快方法。try/catch 意味着一个失败的页面只会记录错误并继续运行,而不会在第 200 个条目的第 4 个时使整个运行崩溃。

第 4 步:写入 JSON 和 CSV

收集到的数据只有离开内存才能发挥作用。JSON 是无需依赖的默认选择,Node 内置的 fs 模块可以直接写入。手工生成 CSV 也同样简单,且可以直接在电子表格中打开。

javascript
import { writeFileSync } from "fs";

const saveJson = (rows, file) =>
  writeFileSync(file, JSON.stringify(rows, null, 2));

const saveCsv = (rows, file) => {
  const headers = Object.keys(rows[0]);
  const escape = (v) => `"${String(v ?? "").replace(/"/g, '""')}"`;
  const lines = [
    headers.join(","),
    ...rows.map((r) => headers.map((h) => escape(r[h])).join(",")),
  ];
  writeFileSync(file, lines.join("\n"));
};

escape 辅助函数将每个值用引号包裹,并将内部的引号加倍, 这是 CSV 规则,防止商品标题中的逗号或引号导致列错位。对于更复杂的情况(嵌套数据、大量数据),可以使用 csv-stringify 等库,但对于扁平记录集来说,这已经足够。

整合起来

将四个部分串联成一个可运行的脚本。

javascript
const main = async () => {
  const rows = await scrapeAll("https://example.com/products");
  if (rows.length === 0) {
    console.log("No data collected.");
    return;
  }
  saveJson(rows, "products.json");
  saveCsv(rows, "products.csv");
  console.log(`Saved ${rows.length} items.`);
};

main();

使用 node scraper.js 运行。你会看到每页一条进度信息,以及磁盘上的两个文件。这就是一个不到一百行的完整静态爬虫。

静态 HTTP 的力有不逮之处

axios 加 cheerio 的技术栈快速且简洁,对于服务端渲染的页面完全够用。但在真实目标上很快会遇到两道墙。

JavaScript 渲染内容。许多现代网站发送的是几乎为空的 HTML 外壳,并在浏览器中通过 JavaScript 构建页面。axios 只抓取这个初始外壳,不运行脚本,因此 cheerio 在数据应该所在的位置什么也找不到。如果你的选择器在明明有内容显示的页面上返回空值,几乎可以断定就是这个原因。

规模化时的封锁。从你的 IP 发出少量请求是没问题的。但来自同一数据中心地址、以可识别模式发出的数百次请求,会让你遭遇限速、CAPTCHA 墙或直接封锁。自定义 User-Agent 能争取一点空间,但无法解决 IP 问题。

前进的方向有两条。第一是使用无头浏览器:PuppeteerPlaywright 驱动真实的 Chrome 或 Firefox,运行页面的 JavaScript,让你能抓取渲染后的 DOM。这解决了渲染问题,但代价较高:每个实例都是一个完整的浏览器,消耗大量内存和 CPU,而且在规模化时你仍然需要自行管理代理池以保持不被封锁。如果你想走这条路,请参阅我们的 Playwright 网络爬取指南。

第二是将这两个问题都交给 API 来解决。

用 Crawling API 获取已渲染、未被拦截的页面

Crawling API 将渲染和 IP 轮换整合为单次请求。你发送一个 URL,它通过可信的轮换住宅 IP 抓取页面(可选先渲染 JavaScript),并返回完整的 HTML。你的现有 cheerio 解析器无需任何改动,只有抓取步骤需要替换。

安装官方 Node 客户端。

bash
npm install crawlbase

然后将 fetchPage 替换为通过 API 请求的版本。下游的一切(解析、循环、保存)保持你原来的写法不变。

javascript
import { CrawlingAPI } from "crawlbase";
import * as cheerio from "cheerio";

const api = new CrawlingAPI({ token: "YOUR_CRAWLBASE_TOKEN" });

const fetchPage = async (url) => {
  const response = await api.get(url, { ajax_wait: true, page_wait: 3000 });
  if (response.statusCode === 200 && response.pcStatus === 200) {
    return response.body;
  }
  throw new Error(`Crawl failed: ${response.statusCode} / ${response.pcStatus}`);
};

有两点值得注意。客户端同时返回 statusCode(目标网站的响应)和 pcStatus(抓取本身是否成功),两者都检查可以防止软性失败被当作有效 HTML 通过。ajax_waitpage_wait 选项用于处理 JavaScript 渲染的目标:ajax_wait 告知 API 等待异步内容,page_wait 在加载后等待几秒,使延迟出现的元素能够在捕获前渲染完成。对于纯静态页面,可以去掉这两个选项,同样享有 IP 轮换的好处,而无需渲染开销。

Normal vs JS token

Crawlbase 令牌有两种类型。普通令牌抓取静态 HTML,适合服务端渲染的页面。JavaScript(JS)令牌会先在真实浏览器中渲染页面,适用于客户端渲染的目标。如果使用普通令牌后页面返回的是空壳,请切换到 JS 令牌。

Crawlbase Crawling API

无需自行运行无头浏览器集群和管理代理池。Crawling API 在需要时渲染 JavaScript,在服务端轮换住宅 IP,并在单次调用中返回完整的 HTML,让你的 cheerio 解析器保持不变即可继续工作。从免费套餐开始,将其指向那些之前封锁你的页面。

保持抓取器健康的技巧

无论你是继续使用 axios 还是迁移到 API,以下几个习惯都能让 Node.js 爬虫运行顺畅。

  • 先阅读网站条款和 robots.txt。在将循环指向目标网站之前,了解你被允许收集的内容和请求频率。
  • 控制请求节奏。调用之间添加延迟,并设置合理的并发数,避免给服务器造成压力,也避免看起来像攻击行为。
  • 发送真实的请求头。类似浏览器的 User-Agent 和标准接受请求头,可以降低被标记为机器人的概率。
  • 按条目处理错误。将每次抓取包裹在错误处理中,使单次失败只记录日志并继续,而不会中断整个运行。
  • 开发时缓存数据。在迭代选择器时将抓取到的 HTML 保存到磁盘,这样每次修改代码时不必重复请求目标网站。
  • 关注状态码。挑战或 4xx 响应比例上升,是放慢速度或轮换 IP 的信号,不能忽视。

完整的反封锁策略(包括 IP 轮换和指纹识别),请参阅如何在不被封锁的情况下抓取网站。如果你更愿意通过轮换池路由自己的流量而非使用托管 API,Smart AI Proxy(也称 AI Proxy)以即插即用代理端点的形式提供住宅 IP 轮换。

回顾

核心要点

  • axios plus cheerio is the static stack. 用 axios 抓取 HTML,用 cheerio 的 jQuery 风格选择器解析,不到一百行即可完成一个可用的爬虫。
  • The pattern is fetch, select, loop, save. 找到重复容器,将选择器映射到字段,带延迟地遍历分页,并写入 JSON 和 CSV。
  • Static HTTP has two limits. 无法运行 JavaScript,因此会漏掉客户端渲染的内容;单一 IP 在规模化时会被封锁。
  • Puppeteer and Playwright solve rendering but are heavy. 每个实例需要一个真实浏览器,消耗内存和 CPU,且你仍须自行管理代理。
  • The Crawling API folds in both. 单次调用返回轮换住宅 IP 后的渲染 HTML,cheerio 解析器无需任何改动。

常见问题

Node.js 适合做网页抓取吗?

是的。Node.js 基于快速的 V8 引擎运行 JavaScript,其非阻塞 I/O 模型允许在单线程上并发处理大量网络请求,而这正是爬取工作的需求所在。npm 生态也为每个步骤提供了成熟的库,从 HTTP 请求到 HTML 解析再到无头浏览器,大多数任务都能迅速上手。

axios 和 cheerio 有什么区别?

它们各自负责一半工作。axios 是 HTTP 客户端:通过网络抓取页面的原始 HTML。cheerio 是解析器:加载该 HTML 字符串,并提供 jQuery 风格的 CSS 选择器来提取所需字段。几乎总是两者配合使用, axios 负责下载,cheerio 负责提取。

为什么 cheerio 在某些页面上返回空结果?

通常是因为页面以客户端方式通过 JavaScript 渲染内容。axios 只抓取初始 HTML 外壳,cheerio 解析它所获取的内容,因此如果数据是在加载后由脚本注入的,就什么也找不到。解决方法是先渲染页面,使用 Puppeteer 或 Playwright 等无头浏览器,或使用带有 JavaScript 渲染选项的 Crawling API。

用 Node.js 抓取时如何避免被封?

通过添加延迟来控制请求节奏,发送真实的浏览器请求头,保持合理的并发数,并轮换 IP 地址,避免单个 IP 触发限速。自定义 User-Agent 有所帮助,但单独使用无法解决 IP 问题。轮换住宅代理或像 Crawling API 这样的托管服务会为你处理轮换,无需自行维护代理池。

我该用 Puppeteer 还是 Crawling API?

当你需要对真实浏览器进行精细控制(如点击多步骤流程或截图),并且愿意自行运行和扩展浏览器时,使用 Puppeteer(或 Playwright)。当你主要需要在规模化场景下获取渲染后的、不被封锁的 HTML,而不想自己管理无头浏览器集群和代理池时,使用 Crawling API。许多团队先用无头浏览器做原型,当请求量和封锁率上升后再迁移到 API。

我可以把抓取的数据写入数据库而不是文件吗?

可以。收集到的记录是普通的 JavaScript 对象,因此一旦你得到数组,就可以将其写入任何地方:JSON 文件、CSV,或使用其 Node.js 驱动的 PostgreSQL、MongoDB 或 SQLite 数据库。保存步骤与爬取逻辑相互独立,只需将 saveJson 替换为插入调用,无需触及抓取或解析代码。

开始构建

大规模爬取任何站点,无需与基础设施对抗。

Crawlbase 负责处理代理、指纹和 CAPTCHA,让你的团队专注于交付数据流水线,而非维护爬取管道。1,000 次请求免费,无需信用卡。

自助开通 · 无需销售通话 · 提供企业级爬取量