Skip to content

Node系列 · ORM:数据抓取

数据抓取(web scraping)是 Node 后端的常见需求:竞品监控、内容聚合、SEO 分析、自动化测试。本章讲清楚"HTTP 客户端 + HTML 解析"的最小可行方案,以及何时需要上浏览器自动化。

一、数据抓取的三个核心步骤

步骤核心库何时升级
发送 HTTP 请求axios / fetch静态页面够用
解析 HTMLcheerio静态 HTML 够用
渲染 JS 页面puppeteer / playwright遇到 SPA / 反爬时升级

二、axios 发送 HTTP 请求

bash
npm install axios
javascript
const axios = require('axios');

const res = await axios.get('https://api.example.com/users', {
  params: { page: 1, limit: 20 },        // URL query
  headers: { 'User-Agent': 'Mozilla/5.0' },
  timeout: 5000,                          // 5 秒超时
});

console.log(res.data);     // 响应体(自动 JSON.parse)
console.log(res.status);   // 200
console.log(res.headers);  // 响应头

2.1 完整配置选项

javascript
const res = await axios({
  url: 'https://api.example.com/users',
  method: 'POST',
  params: { v: '1.0' },             // URL query
  data: { name: 'Alice' },          // body
  headers: {
    'Authorization': 'Bearer xxx',
    'Content-Type': 'application/json',
  },
  timeout: 10000,
  responseType: 'json',             // 'arraybuffer' / 'blob' / 'stream'
  maxRedirects: 5,
  validateStatus: (status) => status < 500,  // 哪些状态码视为成功
});

2.2 拦截器

请求 / 响应拦截,统一处理 token、重试、日志:

javascript
// 请求拦截器:自动加 token
axios.interceptors.request.use((config) => {
  const token = getToken();
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// 响应拦截器:统一错误处理
axios.interceptors.response.use(
  (res) => res,
  (err) => {
    if (err.response?.status === 401) {
      redirectToLogin();
    }
    if (err.response?.status >= 500) {
      console.error('服务端错误:', err.config.url);
    }
    return Promise.reject(err);
  }
);

2.3 错误处理

javascript
try {
  const res = await axios.get(url);
} catch (err) {
  if (err.code === 'ECONNABORTED') {
    // 超时
  } else if (err.response) {
    // 服务端返回了非 2xx
    console.log(err.response.status, err.response.data);
  } else if (err.request) {
    // 请求发出但没响应(网络断)
  } else {
    // 其他错误
  }
}

三、cheerio 解析 HTML

bash
npm install cheerio

cheerio 是服务端 jQuery——用熟悉的 $ 选择器语法解析 HTML 字符串:

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

const html = (await axios.get('https://news.example.com')).data;
const $ = cheerio.load(html);

// 选择所有 h2.title 元素
$('h2.title').each((i, el) => {
  const $el = $(el);
  console.log({
    text: $el.text().trim(),
    link: $el.find('a').attr('href'),
  });
});

// 一次性提取多条数据
const articles = $('article.post').map((i, el) => ({
  title: $(el).find('.title').text().trim(),
  author: $(el).find('.author').text().trim(),
  date: $(el).find('.date').text().trim(),
  url: $(el).find('a').attr('href'),
})).get();

console.log(articles);

3.1 常用选择器

选择器含义
$('div')所有 div
$('.class')class 选择器
$('#id')id 选择器
$('a[href]')带 href 属性的 a
$('parent > child')直接子元素
$('div.article, p.summary')多选择器
$('div').first() / .last()首/尾
$('div').eq(2)索引

3.2 数据清洗

javascript
const $ = cheerio.load(html);

// 提取纯文本(去掉 HTML 标签)
$('div.content').text().trim();

// 提取属性
const src = $('img').attr('src');

// 提取 HTML(保留标签)
$('div.content').html();

// 替换元素
$('script').remove();               // 移除所有 script
$('div.ad').remove();               // 移除广告

// 提取时去除空白
const text = $('p').text().replace(/\s+/g, ' ').trim();

四、完整案例:抓取新闻列表

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

async function scrapeNews(url) {
  const html = (await axios.get(url, {
    headers: { 'User-Agent': 'Mozilla/5.0 (compatible; Bot/1.0)' },
    timeout: 10000,
  })).data;

  const $ = cheerio.load(html);

  return $('.news-item').map((i, el) => ({
    title: $(el).find('.title').text().trim(),
    summary: $(el).find('.summary').text().trim(),
    link: new URL($(el).find('a').attr('href'), url).href,
    date: $(el).find('.date').attr('datetime'),
  })).get();
}

const news = await scrapeNews('https://news.example.com');
console.log(`抓取到 ${news.length} 条新闻`);
console.log(news[0]);

五、遇到反爬怎么办

普通 axios + cheerio 解决 80% 场景。剩下 20% 是反爬:

反爬手段应对
User-Agent 检查User-Agent: Mozilla/5.0 ...
Cookie 验证GET 拿 Set-Cookie,再用该 Cookie 请求
IP 限频代理 IP 池(成本高)
JS 渲染(SPA 页面)puppeteer / playwright
验证码第三方打码平台(合规风险)
签名参数(加密 token)逆向 JS 或调用内部接口

5.1 Puppeteer 渲染 SPA

bash
npm install puppeteer
javascript
const puppeteer = require('puppeteer');

const browser = await puppeteer.launch({
  headless: 'new',                  // 无头模式
  args: ['--no-sandbox'],
});

const page = await browser.newPage();
await page.goto('https://spa.example.com', { waitUntil: 'networkidle0' });

// 等待关键元素出现
await page.waitForSelector('.product-list .item');

// 在浏览器里跑选择器
const items = await page.$$eval('.product-list .item', (els) =>
  els.map((el) => ({
    name: el.querySelector('.name')?.textContent,
    price: el.querySelector('.price')?.textContent,
  }))
);

console.log(items);

await browser.close();

Puppeteer 把整个 Chromium 装到 Node 里——重,但能渲染任何 JS。page.evaluate() 还能在浏览器上下文跑任意 JS。

5.2 何时不要用 Puppeteer

场景建议
简单 GET 抓取用 axios + cheerio
JSON API直接 axios,不用 cheerio
大量并发抓取Puppeteer 内存重,建议加队列
长期大规模爬虫Scrapy(Python 生态成熟)

六、合规与道德

DANGER

抓取前必须确认

  1. 网站 robots.txt——https://example.com/robots.txt 说明哪些路径禁止抓取
  2. 服务条款——很多网站明确禁止爬虫
  3. 公开数据 vs 私密数据——只抓公开可见的数据
  4. 频率控制——setTimeout 间隔 1-3 秒,不要打爆对方服务器
  5. 用户隐私——抓取后存储涉及个人信息需符合 GDPR / 个保法

国内《网络安全法》《数据安全法》《个人信息保护法》对数据抓取有明确限制。商用前最好咨询律师。

七、最佳实践

场景推荐
简单 API 调用axios / fetch
静态 HTML 抓取axios + cheerio
动态 SPA 抓取puppeteer / playwright
频率控制setTimeout / p-limit 并发限制
错误重试拦截器统一处理 + 指数退避
数据存储解析后直接写入数据库 / 文件
合规遵守 robots.txt,控制频率

八、小结

  • 数据抓取三件套:axios(请求)+ cheerio(解析)+ puppeteer(必要时渲染)
  • cheerio 是服务端 jQuery,用熟悉的 $ 选择器解析 HTML
  • 遇到 SPA / 反爬再上 puppeteer,开销大但能处理 JS 渲染
  • 必须遵守 robots.txt、控制频率、不抓私密数据
  • 复杂爬虫场景考虑专门的工具(Scrapy、Playwright)