news 2026/10/27 4:01:31

axios与jsdom实现服务端网页抓取与解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
axios与jsdom实现服务端网页抓取与解析

1. 当HTTP请求遇上DOM解析:axios与jsdom的化学反应

最近在重构一个需要从第三方网站抓取数据并实时解析的项目时,我遇到了一个典型的前端工程难题:如何在不依赖浏览器环境的情况下,既能发送复杂的HTTP请求,又能像在浏览器中一样解析和操作HTML文档?经过多次技术选型对比,最终选择了axios+jsdom这对黄金组合。这个方案不仅完美解决了问题,还带来了许多意料之外的开发便利。

axios作为前端领域最受欢迎的HTTP客户端,以其简洁的API和强大的拦截器机制著称;而jsdom则是Node.js环境下模拟浏览器DOM操作的利器。当这两者结合在一起时,我们就能在服务器端实现完整的"请求-解析-操作"工作流。这种组合特别适合以下场景:

  • 需要爬取并分析网页内容但又不希望启动无头浏览器的轻量级爬虫
  • 服务端渲染(SSR)应用中需要预取和预处理HTML的场景
  • 自动化测试中需要模拟完整页面交互的情况
  • 需要提取网页特定数据但又要避免CORS限制的后端服务

2. 环境搭建与基础配置

2.1 项目初始化与依赖安装

首先创建一个新的Node.js项目(确保Node版本≥14.x),然后安装核心依赖:

npm init -y npm install axios jsdom

对于需要更复杂DOM操作的场景,可以额外安装这些实用工具:

npm install axios-retry @types/jsdom

axios-retry提供了自动重试机制,对于处理不稳定的网络请求非常有用;而@types/jsdom则为TypeScript项目提供类型支持。

2.2 基础请求与解析示例

下面是一个最基本的请求+解析流程:

const axios = require('axios'); const { JSDOM } = require('jsdom'); async function fetchAndParse(url) { try { const response = await axios.get(url); const dom = new JSDOM(response.data); // 现在可以像浏览器中一样使用DOM API const document = dom.window.document; const title = document.querySelector('title').textContent; return { status: response.status, title, headers: response.headers }; } catch (error) { console.error(`请求失败: ${error.message}`); throw error; } } // 使用示例 fetchAndParse('https://example.com') .then(data => console.log(data));

这个简单示例已经展示了axios和jsdom协同工作的核心模式:axios负责获取原始HTML,jsdom将其转换为可操作的DOM树。

3. 高级应用场景与实战技巧

3.1 处理动态加载的内容

现代网站大量使用JavaScript动态加载内容,这对传统的请求-解析模式提出了挑战。我们可以通过组合axios和jsdom的进阶功能来解决:

async function fetchDynamicContent(url) { const response = await axios.get(url); const virtualConsole = new jsdom.VirtualConsole(); const dom = new JSDOM(response.data, { runScripts: "dangerously", // 允许执行页面中的脚本 resources: "usable", // 加载外部资源 virtualConsole // 捕获控制台输出 }); // 等待特定元素出现 await new Promise((resolve) => { const observer = new dom.window.MutationObserver(() => { if (dom.window.document.querySelector('.dynamic-content')) { observer.disconnect(); resolve(); } }); observer.observe(dom.window.document, { childList: true, subtree: true }); // 设置超时 dom.window.setTimeout(() => { observer.disconnect(); resolve(); }, 5000); }); return dom; }

重要提示:runScripts: "dangerously"会执行页面中的所有脚本,可能存在安全风险,仅应在信任的源上使用

3.2 模拟用户会话与表单提交

很多网站需要登录后才能访问特定内容。我们可以用axios维护会话,再用jsdom处理表单:

const axiosInstance = axios.create({ withCredentials: true, // 保持cookies headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...' } }); async function loginAndScrape(loginUrl, targetUrl) { // 1. 获取登录页面提取CSRF令牌 const loginPage = await axiosInstance.get(loginUrl); const loginDom = new JSDOM(loginPage.data); const csrfToken = loginDom.window.document .querySelector('input[name="_csrf"]').value; // 2. 提交登录表单 await axiosInstance.post(loginUrl, { username: 'your_username', password: 'your_password', _csrf: csrfToken }, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }); // 3. 访问目标页面 const targetResponse = await axiosInstance.get(targetUrl); const targetDom = new JSDOM(targetResponse.data); // ...处理目标页面数据... }

4. 性能优化与错误处理

4.1 请求优化策略

axios的拦截器与jsdom的灵活配置可以显著提升性能:

// 配置axios实例 const scraper = axios.create({ timeout: 10000, maxRedirects: 2, // 启用HTTP/2如果服务端支持 httpAgent: new http.Agent({ keepAlive: true }), httpsAgent: new https.Agent({ keepAlive: true }) }); // 请求拦截器 scraper.interceptors.request.use(config => { // 压缩支持 config.headers['Accept-Encoding'] = 'gzip, deflate, br'; return config; }); // 响应拦截器 scraper.interceptors.response.use(response => { // 预处理HTML,移除不必要的部分减小jsdom解析负担 const cleanedHtml = response.data .replace(/<script\b[^>]*>([\s\S]*?)<\/script>/gi, '') .replace(/<style\b[^>]*>([\s\S]*?)<\/style>/gi, ''); response.data = cleanedHtml; return response; }, error => { // 错误处理逻辑 return Promise.reject(error); });

4.2 内存管理与jsdom配置

长时间运行的爬虫需要注意内存泄漏问题:

const { JSDOM, ResourceLoader } = require('jsdom'); const resourceLoader = new ResourceLoader({ // 限制资源加载 userAgent: "Mozilla/5.0 ...", strictSSL: true }); function createLightweightDOM(html) { return new JSDOM(html, { // 禁用不必要的功能 runScripts: "outside-only", resources: resourceLoader, // 限制DOM大小 storageQuota: 5000000, // 禁用本地存储 url: "about:blank", referrer: "", contentType: "text/html", includeNodeLocations: false, pretendToBeVisual: false }); }

5. 真实案例:构建一个商品价格监控工具

让我们通过一个实际案例来展示axios+jsdom的强大组合。假设我们需要监控某电商网站的商品价格变化:

const axios = require('axios'); const { JSDOM } = require('jsdom'); const cron = require('node-cron'); class PriceTracker { constructor() { this.client = axios.create({ baseURL: 'https://example-ecommerce.com', headers: { 'Accept': 'text/html,application/xhtml+xml', 'Accept-Language': 'en-US,en' } }); } async getProductPrice(productId) { try { const { data } = await this.client.get(`/products/${productId}`); const dom = new JSDOM(data); const priceElement = dom.window.document.querySelector( '[itemprop="price"]' ); const price = parseFloat( priceElement.getAttribute('content') || priceElement.textContent ); const stockElement = dom.window.document.querySelector( '.stock-availability' ); const inStock = stockElement.textContent.includes('In Stock'); return { price, inStock, timestamp: new Date() }; } catch (error) { console.error(`获取产品价格失败: ${error.message}`); return null; } } startMonitoring(productId, interval = '0 */30 * * * *') { cron.schedule(interval, async () => { const data = await this.getProductPrice(productId); if (data) { this.notifyPriceChange(data); } }); } notifyPriceChange(data) { // 实现通知逻辑(邮件、Slack等) console.log(`价格更新: $${data.price} | 库存: ${data.inStock}`); } } // 使用示例 const tracker = new PriceTracker(); tracker.startMonitoring('B08L5VJWCV');

这个案例展示了如何将axios的HTTP能力与jsdom的DOM解析能力结合,构建一个功能完整的价格监控系统。通过合理设计,我们可以轻松扩展它来支持多个产品、多个网站,甚至加入价格历史分析和预测功能。

6. 常见问题与解决方案

6.1 处理AJAX内容加载

对于完全依赖AJAX加载内容的页面,我们可以直接分析网络请求:

async function fetchAjaxData(apiUrl) { // 首先获取主页面提取API密钥或令牌 const { data } = await axios.get('https://target-site.com/main'); const dom = new JSDOM(data); const apiKey = dom.window.document .querySelector('meta[name="api-key"]').content; // 然后直接调用AJAX接口 const { data: apiData } = await axios.get(apiUrl, { headers: { 'X-API-Key': apiKey } }); return apiData; }

6.2 处理无限滚动页面

对于采用无限滚动的页面,我们可以模拟滚动行为:

async function fetchPaginatedData(baseUrl, pages = 3) { let allItems = []; for (let page = 1; page <= pages; page++) { const url = `${baseUrl}?page=${page}`; const { data } = await axios.get(url); const dom = new JSDOM(data); const items = Array.from( dom.window.document.querySelectorAll('.item') ).map(el => ({ title: el.querySelector('.title').textContent, // 提取其他字段... })); allItems = [...allItems, ...items]; // 模拟人类阅读速度 await new Promise(resolve => setTimeout(resolve, 2000 + Math.random() * 3000) ); } return allItems; }

6.3 反爬虫策略应对

现代网站常用各种反爬虫技术,我们需要相应调整策略:

const axios = require('axios'); const axiosRetry = require('axios-retry'); const { JSDOM } = require('jsdom'); // 配置axios-retry axiosRetry(axios, { retries: 3, retryDelay: (retryCount) => { return retryCount * 1000; // 指数退避 }, retryCondition: (error) => { // 只在特定情况下重试 return axiosRetry.isNetworkError(error) || (error.response && error.response.status === 429); } }); async function stealthRequest(url) { const headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)...', 'Accept-Language': 'en-US,en;q=0.9', 'Accept': 'text/html,application/xhtml+xml...', 'Referer': 'https://www.google.com/', 'DNT': '1' }; // 随机延迟 await new Promise(resolve => setTimeout(resolve, 1000 + Math.random() * 4000) ); try { const response = await axios.get(url, { headers, // 使用代理(此处省略具体实现) }); // 验证响应是否被拦截 const dom = new JSDOM(response.data); if (dom.window.document.querySelector('.captcha-container')) { throw new Error('触发验证码'); } return dom; } catch (error) { console.error(`请求失败: ${error.message}`); throw error; } }

7. 测试与调试技巧

7.1 使用VSCode调试组合

在VSCode中配置launch.json来调试axios+jsdom应用:

{ "version": "0.2.0", "configurations": [ { "type": "node", "request": "launch", "name": "Debug Scraper", "skipFiles": ["<node_internals>/**"], "program": "${workspaceFolder}/scraper.js", "outFiles": ["${workspaceFolder}/**/*.js"], "env": { "NODE_DEBUG": "axios", "DEBUG": "jsdom*" } } ] }

这会在调试时显示axios和jsdom的详细日志,帮助定位问题。

7.2 保存和重放测试数据

为了不重复请求开发过程中的测试页面,我们可以保存响应数据:

const fs = require('fs'); const path = require('path'); async function fetchWithCache(url, cacheDir = 'cache') { const cacheFile = path.join(cacheDir, `${encodeURIComponent(url)}.html`); // 如果缓存存在且未过期 if (fs.existsSync(cacheFile) && Date.now() - fs.statSync(cacheFile).mtimeMs < 3600000) { const html = fs.readFileSync(cacheFile, 'utf-8'); return new JSDOM(html); } // 否则发起新请求 const { data } = await axios.get(url); // 确保缓存目录存在 if (!fs.existsSync(cacheDir)) { fs.mkdirSync(cacheDir); } // 写入缓存 fs.writeFileSync(cacheFile, data); return new JSDOM(data); }

7.3 可视化调试DOM结构

在开发过程中,有时需要查看jsdom生成的DOM结构。我们可以创建一个简单的调试工具:

function debugDOM(dom, selector = null) { const element = selector ? dom.window.document.querySelector(selector) : dom.window.document.documentElement; if (!element) { console.log('未找到匹配元素'); return; } // 输出简洁的DOM结构 const walker = dom.window.document.createTreeWalker( element, dom.window.NodeFilter.SHOW_ELEMENT ); let indent = 0; const indentStr = ' '; while (walker.nextNode()) { const node = walker.currentNode; if (walker.previousSibling() === null) { indent++; } else if (walker.nextSibling() === null) { indent--; } console.log( indentStr.repeat(indent) + `<${node.tagName.toLowerCase()}>` + (node.id ? `#${node.id}` : '') + (node.className ? `.${node.className.split(' ').join('.')}` : '') ); } }

这个工具可以帮助我们快速理解页面结构,特别是在处理复杂的嵌套DOM时非常有用。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/27 4:01:22

Criteo广告推荐系统架构与机器学习优化实践

1. 项目背景与核心价值Criteo项目是互联网广告领域最具代表性的实战案例之一。作为全球领先的效果营销平台&#xff0c;Criteo每天要处理超过60亿次广告展示决策&#xff0c;这对实时计算系统提出了极高要求。我在2018年首次接触这个项目时&#xff0c;就被它精巧的工程架构所震…

作者头像 李华
网站建设 2026/10/27 4:01:22

YOLOv11在猜拳手势识别中的工业级应用实践

1. 项目概述&#xff1a;当YOLOv11遇上猜拳游戏猜拳识别听起来像是个玩具项目&#xff0c;但实际蕴含着计算机视觉落地的典型挑战。三年前我接手过一个工业分拣项目&#xff0c;需要实时识别工人手势来触发机械臂动作&#xff0c;当时用YOLOv5勉强实现了85%的准确率。如今YOLOv…

作者头像 李华
网站建设 2026/10/27 4:01:22

IOTC SDK 3.1.4.33接入实战:P2P穿透、AV收帧与重连降级

简介&#xff1a;这份压缩包是TUTK IoTC官方SDK 3.1.4.33跨平台P2P监控开发包&#xff0c;面向需要在Android、iOS、Windows与Mac端接入摄像头、构建低延迟实时音视频链路的开发者&#xff0c;重点解决设备直连、设备管理与安全传输等工程问题&#xff0c;适合具备一定移动端或…

作者头像 李华
网站建设 2026/10/27 4:00:55

从执行到操盘:一套可落地的营销技能体系与成长路径

很多人在做营销的时候&#xff0c;最大的困惑不是“不会写文案”&#xff0c;也不是“不会投广告”&#xff0c;而是学了一堆零碎技巧&#xff0c;真到用的时候发现对不上号。今天我想把这几年实际跑过的市场工作、踩过的坑和沉淀下来的方法&#xff0c;做一个系统性的梳理。这…

作者头像 李华
网站建设 2026/10/27 4:00:53

Kimi版OpenClaw:模块化AI工具的技术架构与实战应用

1. 项目概述&#xff1a;Kimi版OpenClaw的技术革命去年第一次接触OpenClaw时&#xff0c;我就被其模块化设计理念所吸引。如今Kimi团队推出的增强版本&#xff0c;直接将可用Skills数量突破5000大关&#xff0c;这标志着AI工具生态进入新阶段。作为早期测试者&#xff0c;我完整…

作者头像 李华
网站建设 2026/10/27 4:00:39

一套稳定可落地的Web端到端测试方案:从选型到CI实践

做Web开发这么些年&#xff0c;e2e这个词大家应该都不陌生&#xff0c;但我发现真正把端到端测试玩明白的人&#xff0c;比想象中少得多。很多人一提到e2e就想到脆弱的测试用例、动不动就失败的CI流程、维护成本高到想删库跑路的测试代码。我自己也经历过那个阶段&#xff0c;后…

作者头像 李华