如何用Node.js实现一个高效的爬虫程序?

来源:菜鸟站长作者:长沙网站建设头衔:草根站长
导读:本期聚焦于长沙网站建设创作的《如何用Node.js实现一个高效的爬虫程序?》,敬请观看详情。很多开发者想要用Node.js开发爬虫程序,但是不清楚如何提升爬虫的运行效率。本文会介绍Node.js实现爬虫的核心思路,包括请求发送、页面解析、数据处理的完整流程,同时讲解异步并发控制、请求去重、错误重试等优化技巧,帮助开发者避开常见的性能陷阱,打造运行稳定、采集效率高的爬虫程序,满足不同场景下的数据采集需求。

Node.js 爬虫的技术优势与整体思路

Node.js 凭借非阻塞 I/O 和事件驱动模型,在处理大量网络请求时能够保持较高的吞吐能力。爬虫程序的核心工作通常是发起请求、等待响应、解析页面、提取数据并进入下一轮请求,这种流程天然适合异步执行。相比同步阻塞模型,Node.js 可以在等待网络响应时继续处理其他任务,因此更适合构建轻量、可扩展的采集程序。

一个高效的爬虫并不是单纯追求请求速度,而是需要在速度、稳定性、资源占用和合规边界之间取得平衡。基础阶段通常要解决三件事:如何可靠地获取页面内容,如何准确解析目标字段,以及如何把多个页面串联成可重复执行的流程。当基础流程跑通后,再引入并发控制、去重机制、重试策略和结果存储,才能形成真正可用于生产场景的采集方案。

从工程角度看,Node.js 爬虫的优势还体现在生态丰富和启动成本低。通过 npm 可以快速引入 HTTP 客户端、HTML 解析器、并发限制工具和文件读写模块,开发者可以把精力集中在选择器设计、任务调度和数据清洗上。本文围绕这一思路展开,先构建最小可用爬虫,再逐步加入效率优化与稳定运行所需的机制。

此外,爬虫程序还需要考虑数据结构的一致性。不同页面可能返回相似但细节不同的字段,例如标题缺失、链接为空、分页参数变化等。早期建立统一的数据模型,有助于后续并发采集、去重和入库时减少兼容成本。

基础爬虫的依赖与核心流程

实现一个基础爬虫,通常需要两类核心能力:第一类是网络请求能力,用于向目标地址发送请求并获取响应内容;第二类是页面解析能力,用于从返回的 HTML 字符串中提取结构化数据。本文使用 axios 发送请求,使用 cheerio 解析页面,两者都是 Node.js 生态中常见且稳定的选择。

安装核心依赖

在项目目录中执行以下命令,即可安装请求库和解析库。安装完成后,代码中就可以通过 require 引入对应模块,开始编写采集逻辑。

npm install axios cheerio

发送请求并解析页面

请求阶段需要设置合理的请求头,尤其是 User-Agent,用于模拟常见浏览器访问。解析阶段则可以通过 CSS 选择器定位目标节点,再提取文本或属性。下面示例假设页面中存在 .article-item 节点,每个节点内部包含标题和链接。为了先验证解析逻辑,可以先使用一段本地 HTML 片段进行测试,再扩展到真实请求。

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

// 发送请求获取页面内容
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;
  }
}

// 解析页面提取文章标题和链接
function parsePage(html) {
  if (!html) return [];
  const $ = cheerio.load(html);
  const result = [];

  $('.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;
}

// 使用本地 HTML 片段验证解析逻辑
const sampleHtml = '<div class="article-item"><span class="title">示例标题</span><a href="/article/1">查看</a></div>';
console.log(parsePage(sampleHtml));

这个示例体现了爬虫中最常见的两段式结构:fetchPage 只负责拿回原始页面,parsePage 只负责把页面转换成数组。职责分离的好处是,当页面结构变化时,通常只需要调整解析函数;当请求方式变化时,也只需要修改请求函数,不会让逻辑互相纠缠。

串联基础采集流程

将请求和解析组合后,就可以完成单页面采集。下面的 basicCrawler 函数接收一个目标地址,先获取页面,再解析数据,最后输出结果。它适合作为调试入口,用来验证选择器是否有效、请求头是否可用、返回数据结构是否符合预期。

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

// 发送请求获取页面内容
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;
  }
}

// 解析页面提取文章标题和链接
function parsePage(html) {
  if (!html) return [];
  const $ = cheerio.load(html);
  const result = [];

  $('.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;
}

// 基础单页面爬虫
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 是一个轻量级并发控制工具,可以限制同一时间正在执行的异步任务数量。下面的示例最多同时发起 5 个请求,既提升吞吐,又避免瞬时压力过大。实际项目中,并发数应根据目标站点承受能力、网络环境和代理资源进行调节。

npm install p-limit
const axios = require('axios');
const cheerio = require('cheerio');
const pLimit = require('p-limit');

// 发送请求获取页面内容
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;
  }
}

// 解析页面提取文章标题和链接
function parsePage(html) {
  if (!html) return [];
  const $ = cheerio.load(html);
  const result = [];

  $('.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;
}

// 最多同时 5 个请求
const limit = pLimit(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 axios = require('axios');
const cheerio = require('cheerio');

// 发送请求获取页面内容
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;
  }
}

// 解析页面提取文章标题和链接
function parsePage(html) {
  if (!html) return [];
  const $ = cheerio.load(html);
  const result = [];

  $('.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;
}

// 已访问 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;
}

crawlWithDedup('https://ipipp.com/page/1').then((data) => {
  console.log('去重采集结果:', data);
});

去重机制适合处理分页、面包屑、相关推荐等容易产生循环链接的页面。若 URL 存在参数顺序不同但实际指向同一页面的情况,可以先做标准化处理,例如排序查询参数、去除追踪参数,再写入 Set。这样能进一步减少重复请求。

加入错误重试机制提升稳定性

网络请求失败并不罕见,超时、连接重置、临时 5xx 响应都可能导致单次请求失败。对于爬虫而言,一次失败不一定代表目标不可用,加入有限次数的重试可以显著提高成功率。重试次数不宜过多,否则会把临时故障放大为长时间阻塞。

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;
  }
}

// 最多重试 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;
}

fetchPageWithRetry('https://ipipp.com/page/1').then((html) => {
  console.log('重试后的页面内容:', html ? '获取成功' : '获取失败');
});

更完善的重试策略还可以加入间隔等待,例如每次失败后暂停一段时间再尝试,避免连续冲击同一接口。对于批量任务,可以把重试、去重和并发控制组合使用:先用 Set 过滤已访问地址,再用并发限制控制请求规模,最后对失败任务进行有限重试,形成稳定可恢复的采集链路。

合规边界、数据落地与工程化建议

爬虫程序在技术实现之外,还必须考虑使用边界。目标站点可能通过 robots.txt 声明禁止访问的路径,也可能通过登录、验证码、频率限制等方式保护资源。合规采集不仅有助于降低法律与平台风险,也能减少因触发反爬机制导致的数据中断。对于公开数据,建议优先选择官方开放接口或允许抓取的页面;对于私有数据,则应明确授权来源。

优化方向解决的问题建议做法
并发控制避免请求过多导致限流或本地资源紧张使用 p-limit 控制同时请求数,并根据目标站点承受能力调整
请求去重避免重复访问同一地址使用 Set 保存已访问 URL,必要时先做 URL 标准化
错误重试降低临时网络故障造成的失败率设置最大重试次数,并加入间隔等待
频率控制减少对目标服务器的压力在批量任务中加入延迟或令牌桶限流
代理资源降低单一 IP 被封禁的风险在大规模采集时引入代理池,并监控代理可用性
  • 先小批量验证选择器,再扩大页面范围。
  • 记录失败 URL,便于后续重跑。
  • 对采集字段做清洗和校验,避免脏数据入库。

采集结果需要可靠落地,才能用于后续分析、展示或入库。最简单的存储方式是将结构化数据写入 JSON 文件,这种方式便于调试和查看,也方便后续转换为其他格式。对于生产系统,可以进一步接入数据库、对象存储或消息队列,实现增量写入、失败重放和任务监控。

const fs = require('fs');
const axios = require('axios');
const cheerio = require('cheerio');
const pLimit = require('p-limit');

// 发送请求获取页面内容
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;
  }
}

// 解析页面提取文章标题和链接
function parsePage(html) {
  if (!html) return [];
  const $ = cheerio.load(html);
  const result = [];

  $('.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;
}

// 最多同时 5 个请求
const limit = pLimit(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();
}

// 将采集结果保存到 JSON 文件
function saveDataToFile(data, filename = 'crawl_result.json') {
  fs.writeFileSync(filename, JSON.stringify(data, null, 2), 'utf-8');
  console.log(`数据已保存到${filename}`);
}

const urlList = [
  'https://ipipp.com/page/1',
  'https://ipipp.com/page/2',
  'https://ipipp.com/page/3'
];

batchCrawl(urlList).then((data) => {
  saveDataToFile(data);
});

在长期运行中,日志和监控也是重要组成部分。记录每个 URL 的状态码、解析结果数量、重试次数和失败原因,可以帮助判断是选择器失效、网络波动还是目标站点策略变化。对于批量任务,还可以按批次统计成功率,及时发现异常并调整并发数、代理策略或解析规则。

高效爬虫的关键不在于单次请求有多快,而在于整条链路是否稳定、可控、可维护。请求、解析、去重、重试、存储这些环节一旦形成清晰边界,后续扩展代理、数据库、任务队列时会更加平滑。

综合来看,Node.js 适合构建轻量且高并发的爬虫程序,但真正决定采集质量的,是异步调度、错误恢复和数据落地的工程细节。建议从最小可用流程开始,先验证选择器与数据结构,再逐步加入并发限制、去重和重试机制,最后结合合规要求与存储策略形成完整方案。这样既能保证开发效率,也能让爬虫在长期运行中保持稳健。

Node.js爬虫cheerioaxios异步并发修改时间:2026-07-12 15:03:24

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。