Node.js实战:使用Axios和Cheerio编写轻量级网页数据抓取脚本

作者:栖木

时间:2026-10-30

浏览:470

深入讲解如何使用Node.js结合Axios与Cheerio构建轻量级网页抓取脚本,涵盖环境搭建、DOM解析、数据提取、限速防封及与无头浏览器的选型对比。

文章目录

假设你需要每天从若干技术博客、文档聚合站或论坛首页收集最新的文章标题与链接。如果直接引入 Puppeteer 或 Playwright 这种完整的无头浏览器,往往需要先下载数百兆的 Chromium 二进制文件,每次启动都要消耗可观的内存与 CPU 资源。而对于大多数基于服务端渲染(SSR)或传统后端模板生成的静态网页,网页内容在 HTTP 响应的 HTML 中就已经完整存在。此时,使用 Node.js 配合 Axios 发起网络请求,再由 Cheerio 解析 HTML 并提取节点,是一种启动仅需几毫秒、内存占用低至数兆且开发直观的高效方案。

一、核心架构与执行机制

Axios 和 Cheerio 的组合之所以轻量,是因为它们各司其职,完全省去了浏览器渲染引擎、样式计算和 JavaScript 执行环境的巨大开销:

  • Axios:负责基于 Promise 的 HTTP 通信。它向目标服务器发送 GET 请求,携带预设的请求头(如 User-Agent),接收远端返回的原始 HTML 文本字符串。
  • Cheerio:负责在 Node.js 内存中快速构建轻量级虚拟 DOM。它采用与 jQuery 几乎完全一致的语法(如 $('div.card')、.find()、.text()),能够直接以极高的性能对 HTML 字符串执行 CSS 选择器查询,而不解析 CSS 样式或执行客户端脚本。
Node.js实战:使用Axios和Cheerio编写轻量级网页数据抓取脚本
图 1:Axios 与 Cheerio 协同工作的无头抓取流水线架构

二、前置准备与开发步骤

编写抓取脚本前,需要明确五个标准操作步骤:从初始化项目环境开始,到借助开发者工具定位关键节点,再到发起请求、解析提取与结果落地。

Node.js实战:使用Axios和Cheerio编写轻量级网页数据抓取脚本
图 2:轻量级网页数据抓取脚本开发全生命周期流程

1. 项目创建与依赖安装

首先在本地终端中创建一个新的项目目录并执行初始化:

# 创建并进入目录
mkdir node-scraper && cd node-scraper

# 初始化 package.json
npm init -y

# 安装 axios 与 cheerio 依赖
npm install axios cheerio

安装完成后,检查 package.json。这两个库均属于常规的纯 JavaScript 模块,不包含 C++ 原生编译扩展,因此无论在 macOS、Linux 还是 Windows 环境下都能秒级安装完毕。

2. 审查目标页面结构

在编写代码前,必须先确认目标数据是否存在于服务端返回的静态 HTML 中:

  1. 在 Chrome 或 Edge 浏览器中打开目标页面,右键选择“查看网页源代码”(View Page Source),而不是仅仅看“检查”(Inspect Elements)。
  2. 在源代码页面按 Ctrl+F(或 Cmd+F)搜索你想要抓取的数据文本。如果能搜到,说明是标准的静态页面或服务端渲染,适合本教程方案;如果搜不到,说明数据是通过异步 Ajax/Fetch 接口由前端渲染生成,此时应优先抓取其背后的 JSON API,或使用无头浏览器。
  3. 在 DevTools 中检查容器元素的 Class 名或标签层级。例如列表项为 <li class="post-item">,标题链接为 <a class="post-title">。

三、编写抓取脚本:完整实战代码

以下是一个结构规范、具备超时控制、User-Agent 伪装、相对链接补全以及异常捕获的完整脚本 scraper.js:

/**
 * scraper.js - 基于 Axios 与 Cheerio 的轻量级数据抓取脚本
 */
const axios = require('axios');
const cheerio = require('cheerio');
const fs = require('fs');
const path = require('path');

// 目标网址(以合法的公开示例站或技术博客为例)
const TARGET_URL = 'https://news.ycombinator.com/';

// 创建配置好的 Axios 实例
const httpClient = axios.create({
  timeout: 10000, // 10秒超时
  headers: {
    // 伪装常见桌面浏览器的 User-Agent,避免被基础 WAF 阻断
    'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
    'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
  }
});

/**
 * 抓取指定页面的文章列表
 * @param {string} url 目标URL
 */
async function fetchArticles(url) {
  try {
    console.log(`[1/3] 正在请求目标页面: ${url}`);
    const response = await httpClient.get(url);

    // 状态码校验
    if (response.status !== 200) {
      throw new Error(`请求失败,HTTP 状态码: ${response.status}`);
    }

    console.log('[2/3] 页面下载完成,开始解析 HTML DOM 结构...');
    const html = response.data;
    
    // 使用 Cheerio 载入 HTML
    const $ = cheerio.load(html);
    const articles = [];

    // Hacker News 结构:每一项包含在 tr.athing 中
    $('tr.athing').each((index, element) => {
      // 提取标题与主链接
      const titleLink = $(element).find('span.titleline > a');
      const title = titleLink.text().trim();
      let link = titleLink.attr('href') || '';

      // 提取副信息行(紧随其后的同级 tr)
      const subtext = $(element).next();
      const score = subtext.find('.score').text().trim() || '0 points';
      const author = subtext.find('.hnuser').text().trim() || 'anonymous';

      // 补全相对路径链接
      if (link && !link.startsWith('http')) {
        link = new URL(link, url).href;
      }

      if (title && link) {
        articles.push({
          id: index + 1,
          title,
          link,
          score,
          author
        });
      }
    });

    console.log(`[3/3] 成功提取到 ${articles.length} 条有效记录!`);
    return articles;

  } catch (error) {
    if (error.response) {
      console.error(`服务端响应错误: ${error.response.status} - ${error.response.statusText}`);
    } else if (error.code === 'ECONNABORTED') {
      console.error('网络请求超时,请检查网络或目标服务器响应状况。');
    } else {
      console.error(`抓取过程发生异常: ${error.message}`);
    }
    return [];
  }
}

/**
 * 主执行流程
 */
async function main() {
  const data = await fetchArticles(TARGET_URL);

  if (data.length > 0) {
    // 格式化输出前 3 条作为预览
    console.log('\n--- 抓取数据示例预览(前3条)---');
    console.log(JSON.stringify(data.slice(0, 3), null, 2));

    // 保存为本地 JSON 文件
    const outputPath = path.join(__dirname, 'articles.json');
    fs.writeFileSync(outputPath, JSON.stringify(data, null, 2), 'utf-8');
    console.log(`\n数据已成功保存至本地文件: ${outputPath}`);
  } else {
    console.warn('\n未抓取到有效数据,已终止写入。');
  }
}

// 启动执行
main();

执行命令与输出示例

在终端中直接运行该脚本:

node scraper.js

终端控制台的实际输出示意如下:

[1/3] 正在请求目标页面: https://news.ycombinator.com/
[2/3] 页面下载完成,开始解析 HTML DOM 结构...
[3/3] 成功提取到 30 条有效记录!

--- 抓取数据示例预览(前3条)---
[
  {
    "id": 1,
    "title": "Show HN: A lightweight SQLite engine for WebAssembly",
    "link": "https://github.com/example/wasm-sqlite",
    "score": "142 points",
    "author": "tech_author"
  },
  {
    "id": 2,
    "title": "Why we migrated our microservices back to a modular monolith",
    "link": "https://blog.engineering-post.com/monolith-journey",
    "score": "285 points",
    "author": "devops_guru"
  },
  {
    "id": 3,
    "title": "Fast algorithms for integer factorization",
    "link": "https://arxiv.org/abs/2401.00000",
    "score": "89 points",
    "author": "math_geek"
  }
]

数据已成功保存至本地文件: /Users/username/node-scraper/articles.json

四、方案对比与选型决策

在 Node.js 生态中抓取网页数据,主要有两大流派:一是以 Axios + Cheerio 为代表的轻量级 HTTP 文本解析流派;二是以 Puppeteer / Playwright 为代表的完整无头浏览器自动化流派。在决定采用哪种工具前,可参考下方的选型维度与决策对比:

Node.js实战:使用Axios和Cheerio编写轻量级网页数据抓取脚本
图 3:Axios+Cheerio 与无头浏览器在各核心维度的决策对比

实战选型黄金法则:能用 HTTP 库直接拿到 HTML 或 JSON 接口的,永远优先用 Axios + Cheerio;只有当页面强依赖客户端 JavaScript 动态生成内容且无公开 API 可逆向时,才考虑启动 Puppeteer 等无头浏览器。

五、进阶技巧:分页循环抓取与限速防封

抓取多页数据时,很多初学者容易使用 Promise.all 一次性并发请求所有页码,这极易导致:

  1. 瞬间高并发给目标服务器带来瞬时负载,触发服务端的限流防护(HTTP 429 Too Many Requests)。
  2. 触发防火墙机制导致 IP 被直接加入临时黑名单(HTTP 403 Forbidden)。

推荐采用串行循环结合随机延时(Jitter)的方式优雅抓取:

/**
 * 延时等待工具函数
 * @param {number} ms 毫秒数
 */
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));

async function fetchMultiplePages(totalPage = 3) {
  const allResults = [];

  for (let page = 1; page <= totalPage; page++) {
    const pageUrl = `https://example.com/posts?page=${page}`;
    console.log(`正在抓取第 ${page} 页: ${pageUrl}`);

    const pageData = await fetchArticles(pageUrl);
    allResults.push(...pageData);

    if (page < totalPage) {
      // 引入 1.5 ~ 3 秒的随机延时,模拟真人访问间隔
      const delay = Math.floor(Math.random() * 1500) + 1500;
      console.log(`等待 ${delay} 毫秒后抓取下一页...\n`);
      await sleep(delay);
    }
  }

  return allResults;
}

六、生产级避坑指南与最佳实践

在实际投产中,网页抓取脚本往往需要长期稳定运行。以下总结了四个在实战中最为关键的注意事项:

Node.js实战:使用Axios和Cheerio编写轻量级网页数据抓取脚本
图 4:网页数据抓取脚本生产规范与避坑要点

常见问题排查

  • 问题 1:Cheerio 提取出的字段始终为空字符串?
    原因:该网页内容很可能是前端单页应用(SPA)通过异步接口后渲染生成的,或者页面的选择器类名被动态哈希化。
    解决:在浏览器查看“网页源代码”,若搜索不到目标文字,请切换为直接抓取其网络请求接口(Network 面板),或使用无头浏览器等待页面渲染完成。
  • 问题 2:偶发 ECONNABORTED 或 ETIMEDOUT 超时?
    原因:国内访问境外站点网络波动,或目标服务器实施了反爬限速阻断。
    解决:设置合理的 timeout(如 8000ms),并在外层配合轻量的重试机制(如使用 axios-retry 库)最多重试 2~3 次。
  • 问题 3:抓取到的中文全是类似 � 的乱码?
    原因:目标网页编码非 UTF-8,常见于部分早期的政企或地方高校网站采用 GB2312/GBK 编码。
    解决:将 Axios 的 responseType 设为 'arraybuffer',再使用 iconv-lite 库的 iconv.decode(response.data, 'gbk') 进行解码。

总结

使用 Node.js 构建网页爬虫并不总是需要笨重的浏览器运行时。通过深入理解目标站点的渲染形态,将 Axios 的 HTTP 请求能力与 Cheerio 内存中的 jQuery 式 DOM 解析结合,可以在极低的计算资源消耗与极高的执行速度下完成日常数据抓取任务。在编写脚本时,合理设置超时、补充请求头、加入遍历延时,不仅能有效提升脚本的容错率与存活率,也是网络爬虫开发中应当遵循的基本礼仪。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章