Node.js凭借非阻塞I/O和事件驱动的特性,非常适合用来开发爬虫程序,能够高效处理大量网络请求。下面我们从基础实现到性能优化,逐步讲解如何搭建一个高效的Node.js爬虫。

基础爬虫的核心依赖
实现爬虫首先需要两个核心库,分别是用于发送HTTP请求的axios和用于解析HTML页面的cheerio,可以通过npm安装:
npm install axios cheerio
基础爬虫实现步骤
1. 发送请求获取页面内容
使用axios发送GET请求获取目标页面的HTML字符串,这里需要注意设置合理的请求头,模拟浏览器访问避免被反爬:
const axios = require('axios');
// 发送请求获取页面内容
async function fetchPage(url) {
try {
const response = await axios.get(url, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
}
});
return response.data;
} catch (error) {
console.error(`请求${url}失败:`, error.message);
return null;
}
}
2. 解析页面提取目标数据
拿到HTML字符串后,使用cheerio加载内容,通过CSS选择器提取需要的数据,用法和jQuery类似:
const cheerio = require('cheerio');
// 解析页面提取文章标题和链接
function parsePage(html) {
if (!html) return [];
const $ = cheerio.load(html);
const result = [];
// 假设目标数据在class为article-item的div中
$('.article-item').each((index, element) => {
const title = $(element).find('.title').text().trim();
const link = $(element).find('a').attr('href');
if (title && link) {
result.push({ title, link });
}
});
return result;
}
3. 基础流程串联
将请求和解析步骤结合,实现一个简单的单页面爬虫:
async function basicCrawler(targetUrl) {
const html = await fetchPage(targetUrl);
const data = parsePage(html);
console.log('采集到的数据:', data);
return data;
}
// 调用示例
basicCrawler('https://ipipp.com/article-list');
提升爬虫效率的优化方案
异步并发控制
单请求串行采集效率很低,Node.js支持异步并发,但如果不加控制同时发送大量请求,会导致目标服务器压力过大被封禁,也会占用过多本地资源。我们可以使用p-limit库控制并发数量:
npm install p-limit
控制并发的采集示例:
const pLimit = require('p-limit');
const limit = pLimit(5); // 最多同时5个请求
// 批量采集多个页面
async function batchCrawl(urlList) {
const tasks = urlList.map(url => {
return limit(async () => {
const html = await fetchPage(url);
return parsePage(html);
});
});
const results = await Promise.all(tasks);
// 合并所有结果
return results.flat();
}
// 调用示例
const urlList = [
'https://ipipp.com/page/1',
'https://ipipp.com/page/2',
'https://ipipp.com/page/3'
];
batchCrawl(urlList).then(data => console.log('批量采集结果:', data));
请求去重
避免重复采集同一个页面,可以使用Set存储已经访问过的URL:
const visitedUrls = new Set();
async function crawlWithDedup(url) {
if (visitedUrls.has(url)) {
console.log(`URL ${url} 已经采集过,跳过`);
return [];
}
visitedUrls.add(url);
const html = await fetchPage(url);
const data = parsePage(html);
// 假设页面中有下一页链接,继续采集
const $ = cheerio.load(html);
const nextPage = $('.next-page').attr('href');
if (nextPage) {
const nextData = await crawlWithDedup(nextPage);
return data.concat(nextData);
}
return data;
}
错误重试机制
网络请求偶尔会失败,添加重试机制可以提升爬虫的稳定性,这里实现最多重试3次的逻辑:
async function fetchPageWithRetry(url, retryCount = 3) {
for (let i = 0; i < retryCount; i++) {
const html = await fetchPage(url);
if (html) return html;
console.log(`第${i + 1}次请求${url}失败,正在重试...`);
}
console.error(`请求${url}失败,已达最大重试次数`);
return null;
}
注意事项
- 遵守目标网站的robots.txt规则,不要采集禁止访问的内容
- 控制请求频率,避免对目标服务器造成过大压力
- 如果需要采集大量数据,建议添加代理IP池,避免本地IP被封禁
- 采集到的数据可以存储到数据库或者本地文件中,方便后续使用
数据存储示例
将采集到的数据存储到JSON文件中:
const fs = require('fs');
function saveDataToFile(data, filename = 'crawl_result.json') {
fs.writeFileSync(filename, JSON.stringify(data, null, 2), 'utf-8');
console.log(`数据已保存到${filename}`);
}
// 结合之前的批量采集使用
batchCrawl(urlList).then(data => {
saveDataToFile(data);
});