爬虫系统是 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-Agent、Referer、Accept-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-chain、puppeteer-page-proxy等,在 Puppeteer 中也能配置代理。 - 隧道代理:部分服务商会提供“每次请求随机出口 IP”的隧道代理,无需手动管理池。
在代码中集成代理:
const axiosInstance = axios.create({
proxy: {
host: 'proxy.example.com',
port: 8080,
auth: { username: 'user', password: 'pass' }
}
});
4. 处理 JavaScript 检测与验证码
一些网站利用 navigator.webdriver、window.chrome 对象检测无头浏览器。可在 Puppeteer 中通过 page.evaluateOnNewDocument 注入脚本掩盖特征。
await page.evaluateOnNewDocument(() => {
Object.defineProperty(navigator, 'webdriver', { get: () => false });
});
对于验证码(图形验证码、滑动验证码),简单场景可以集成打码平台(如 2captcha、超级鹰),由平台返回识别结果;复杂交互(如滑块)则需要通过模拟鼠标轨迹逐个处理。对于量级不大的业务,人工打码辅助也是一种折中选择。
5. 登录与 Cookie 维持
需要登录的站点,可以先模拟登录获取 Cookie,后续请求携带该 Cookie。使用 axios 的 withCredentials 或 Cookie 头传递。
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 解析库及无头浏览器控制能力,使其成为爬虫开发的高效工具。通过组合上述技术,从简单的静态采集到复杂的反爬对抗,都可以构建出健壮的自动化数据获取系统。