Node系列 · ORM:数据抓取
数据抓取(web scraping)是 Node 后端的常见需求:竞品监控、内容聚合、SEO 分析、自动化测试。本章讲清楚"HTTP 客户端 + HTML 解析"的最小可行方案,以及何时需要上浏览器自动化。
一、数据抓取的三个核心步骤
| 步骤 | 核心库 | 何时升级 |
|---|---|---|
| 发送 HTTP 请求 | axios / fetch | 静态页面够用 |
| 解析 HTML | cheerio | 静态 HTML 够用 |
| 渲染 JS 页面 | puppeteer / playwright | 遇到 SPA / 反爬时升级 |
二、axios 发送 HTTP 请求
bash
npm install axiosjavascript
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 cheeriocheerio 是服务端 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 puppeteerjavascript
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
抓取前必须确认:
- 网站
robots.txt——https://example.com/robots.txt说明哪些路径禁止抓取 - 服务条款——很多网站明确禁止爬虫
- 公开数据 vs 私密数据——只抓公开可见的数据
- 频率控制——
setTimeout间隔 1-3 秒,不要打爆对方服务器 - 用户隐私——抓取后存储涉及个人信息需符合 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)
