人人都会AI编程

26.4 爬虫系统:页面抓取、数据解析、反爬应对

更新时间:2026-07-11

爬虫系统是 Node.js 的常见用武之地。得益于非阻塞 I/O 和丰富的 HTTP 客户端、HTML 解析库,使用 Node.js 可以快速搭建从轻量级页面抓取到分布式爬虫的完整解决方案。这一节将聚焦在单机爬虫的核心三要素:页面抓取、数据解析、反爬应对

页面抓取:选择合适的工具

根据目标页面是静态 HTML 还是需要 JavaScript 渲染,需要选择不同抓取方式。

静态页面:axios/undici + cheerio

对于无需浏览器渲染的页面,直接用 HTTP 客户端获取 HTML 字符串,再用 cheerio 解析效率最高。

const axios = require('axios');
const cheerio = require('cheerio');

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

动态页面:Puppeteer / Playwright

当页面依靠前端渲染、存在反爬检测时,就需要无头浏览器。Puppeteer(Chrome DevTools Protocol)和 Playwright(多浏览器支持)都可以控制完整的浏览器环境,执行 JavaScript、截屏、获取渲染后的 DOM。

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({ headless: 'new' });
  const page = await browser.newPage();
  await page.goto('https://example.com', { waitUntil: 'networkidle2' });
  const html = await page.content();
  // 使用 cheerio 进一步解析
  await browser.close();
})();

权衡:无头浏览器资源占用高,并发受限,一般用于少量复杂页面的抓取;大量静态页面优先用 axios + cheerio。

数据解析:从 HTML 到结构化信息

数据解析的本质是从半结构化的 HTML 中提取字段。常用方案有:

  • CSS 选择器(cheerio):仿 jQuery 风格,适合大部分网页。
  • 正则表达式:处理难以用选择器匹配的文本块,但可维护性较差。
  • XPath:在 Puppeteer/Playwright 中可直接使用 page.$x()

Cheerio 基本用法

const $ = cheerio.load(html);
const title = $('h1').text().trim();
const links = [];
$('a.item').each((i, el) => {
  links.push({
    text: $(el).text(),
    href: $(el).attr('href')
  });
});

对于列表页、分页,通常需要先解析总页数或“下一页”链接,再递归或循环抓取。为控制内存和并发,建议使用队列 + 并发限制。

反爬应对:见招拆招的实战策略

大部分网站都有程度不一的反爬措施,爬虫开发中约有七成精力消耗在应对上。以下是常见的反爬手段和破解思路。

1. 请求头伪装

设置合理的 User-AgentRefererAccept-Language 等头,模拟真实浏览器。避免直接使用 Node.js 默认的 axios 标识。

const headers = {
  'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ...',
  'Accept': 'text/html,application/xhtml+xml',
  'Accept-Language': 'zh-CN,zh;q=0.9',
  'Referer': 'https://www.google.com/'
};

对于严格检测的网站,还可以随机轮换 User-Agent 库,或者使用 user-agents 包生成真实分布的用户代理。

2. 请求频率与间隔

连续高频率请求很容易触发 IP 封锁或验证码。需要控制请求间隔,并引入随机延迟。

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

// 每次请求前随机等待 2~5 秒
const delay = () => sleep(2000 + Math.random() * 3000);

对于大量 URL 的抓取,可以使用 p-limit 等库限制并发数(如 5 个并发),确保不会对目标服务器造成过大压力,也降低被封锁风险。

3. IP 代理池

当单一 IP 访问过于频繁时,目标站会封禁 IP。维护一个代理池(免费或付费代理)是常见方案。

  • 付费代理:稳定、节点多,很多云厂商提供 API 实时获取可用代理。
  • 代理中间件:使用 proxy-chainpuppeteer-page-proxy 等,在 Puppeteer 中也能配置代理。
  • 隧道代理:部分服务商会提供“每次请求随机出口 IP”的隧道代理,无需手动管理池。

在代码中集成代理:

const axiosInstance = axios.create({
  proxy: {
    host: 'proxy.example.com',
    port: 8080,
    auth: { username: 'user', password: 'pass' }
  }
});

4. 处理 JavaScript 检测与验证码

一些网站利用 navigator.webdriverwindow.chrome 对象检测无头浏览器。可在 Puppeteer 中通过 page.evaluateOnNewDocument 注入脚本掩盖特征。

await page.evaluateOnNewDocument(() => {
  Object.defineProperty(navigator, 'webdriver', { get: () => false });
});

对于验证码(图形验证码、滑动验证码),简单场景可以集成打码平台(如 2captcha、超级鹰),由平台返回识别结果;复杂交互(如滑块)则需要通过模拟鼠标轨迹逐个处理。对于量级不大的业务,人工打码辅助也是一种折中选择。

5. 登录与 Cookie 维持

需要登录的站点,可以先模拟登录获取 Cookie,后续请求携带该 Cookie。使用 axioswithCredentialsCookie 头传递。

Puppeteer 可直接使用真实浏览器登录,持久化用户数据目录(userDataDir),下次启动复用登录态:

const browser = await puppeteer.launch({
  headless: false, // 首次可能需要可见界面手动登录
  userDataDir: './profile'
});

6. 服务端渲染(SSR)站点

部分网站虽然看起来是动态页面,但实际上是通过服务器端渲染的,只需调整 HTTP 请求头中的 Accept 或添加参数即可拿到最终 HTML。先尝试 curl 分析,避免直接上无头浏览器浪费资源。

简单爬虫系统架构示例

一个稳健的单机爬虫通常会包含以下模块:

  • URL 管理器:维护待抓取队列和去重集合(可使用 Redis SET 或本地 Set)。
  • 下载器:封装 HTTP 客户端,统一处理重试、代理、Cookie。
  • 解析器:根据页面类型调用不同解析逻辑。
  • 数据存储:写入数据库或导出 CSV。
  • 调度器:控制并发和速度,异常处理与重试。

以下为简化示例:

const pLimit = require('p-limit');
const limit = pLimit(3); // 最多3个并发

async function crawl(url) {
  try {
    await delay();
    const $ = await fetchPage(url);
    const items = parse($);
    await saveItems(items);
    // 提取新链接加入队列
    const nextLinks = extractNextLinks($);
    for (const link of nextLinks) {
      if (!visited.has(link)) {
        visited.add(link);
        queue.push(link);
      }
    }
  } catch (err) {
    console.error(`抓取 ${url} 失败: ${err.message}`);
    // 可重试或记录到失败队列
  }
}

// 启动多个并发消费
for (let i = 0; i < 3; i++) {
  (async () => {
    while (queue.length) {
      const url = queue.shift();
      await limit(() => crawl(url));
    }
  })();
}

法律与道德底线

在构建爬虫系统时,务必注意合规性:

  • robots.txt:查看目标站点的爬虫协议,遵守 Disallow 规则。
  • 速率控制:不要对网站发起 DDoS 式攻击,设置合理的间隔。
  • 数据用途:不得用于非法竞争或侵犯隐私,应遵循相关法律法规。
  • 鉴权数据:若网站有明确的版权声明或付费墙,应停止抓取。

综合来看,Node.js 完整的 HTTP 客户端、强大的 DOM 解析库及无头浏览器控制能力,使其成为爬虫开发的高效工具。通过组合上述技术,从简单的静态采集到复杂的反爬对抗,都可以构建出健壮的自动化数据获取系统。