Node.js部分Promise返回undefined问题排查与优化咨询
解决Node.js + Axios + Cheerio爬取彩虹六号干员时部分operatorStats为undefined的问题
问题根源分析
出现部分operatorStats为undefined的核心原因有两个:
- 请求失败未处理:批量请求Ubisoft页面时,部分请求被反爬拦截或网络异常,进入
scrapeSpecificOperatorStats的catch块,但该块仅打印错误未返回任何值,导致operatorStats为undefined。 - Cheerio集合处理异常:Cheerio的
map方法返回的是Cheerio对象而非标准数组,Promise.all处理时可能出现异常,导致部分Promise未正确执行。 - (次要)部分干员页面的HTML结构可能存在差异,原选择器
span.operator__header__roles无法匹配到元素,但这种情况会返回{specialty: ""}而非undefined。
具体解决方案
1. 修复catch块返回逻辑
确保请求失败时返回默认值,避免operatorStats为undefined:
const scrapeSpecificOperatorStats = async (link) => { try { // 添加浏览器请求头,降低被拦截概率 const response = await axios.get(link, { headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36' } }); const $ = cheerio.load(response.data); // 改用更通用的选择器,兼容不同标签的情况 const specialty = $(".operator__header__roles").text().trim(); return { specialty: specialty || "未获取到角色信息" }; } catch (error) { console.error(`请求干员页面失败 ${link}:`, error.message); // 返回默认值,保证operatorStats不为undefined return { specialty: "请求失败" }; } };
2. 修正Cheerio集合转Promise数组的方式
将Cheerio集合转换为标准数组后再生成Promise,确保Promise.all能正确处理:
// 替换原statsPromises生成代码 const statsPromises = Array.from(statsElement).map(async (element) => { const $el = $(element); const name = $el.find("span").text().trim(); const imageUrl = $el.find("img").attr("src"); const linkUrl = "https://www.ubisoft.com" + $el.attr("href"); const operatorStats = await scrapeSpecificOperatorStats(linkUrl); return { name, imageUrl, linkUrl, operatorStats }; });
3. 添加请求节流避免反爬
使用p-limit限制并发请求数,降低被网站拦截的概率:
- 安装依赖:
npm install p-limit
- 修改代码:
const pLimit = require('p-limit'); // 限制同时发起3个请求,可根据情况调整 const limit = pLimit(3); // 生成带节流的Promise数组 const statsPromises = Array.from(statsElement).map(element => { return limit(async () => { const $el = $(element); const name = $el.find("span").text().trim(); const imageUrl = $el.find("img").attr("src"); const linkUrl = "https://www.ubisoft.com" + $el.attr("href"); const operatorStats = await scrapeSpecificOperatorStats(linkUrl); return { name, imageUrl, linkUrl, operatorStats }; }); });
完整修正后的代码
const axios = require("axios"); const cheerio = require("cheerio"); const pLimit = require('p-limit'); const url = "https://www.ubisoft.com/en-us/game/rainbow-six/siege/game-info/operators"; const limit = pLimit(3); const scrapeSpecificOperatorStats = async (link) => { try { const response = await axios.get(link, { headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36' } }); const $ = cheerio.load(response.data); const specialty = $(".operator__header__roles").text().trim(); return { specialty: specialty || "未获取到角色信息" }; } catch (error) { console.error(`请求干员页面失败 ${link}:`, error.message); return { specialty: "请求失败" }; } }; const scrapeOperatorStats = async (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/118.0.0.0 Safari/537.36' } }); const $ = cheerio.load(response.data); const statsElement = $(".oplist__card"); const statsPromises = Array.from(statsElement).map(element => { return limit(async () => { const $el = $(element); const name = $el.find("span").text().trim(); const imageUrl = $el.find("img").attr("src"); const linkUrl = "https://www.ubisoft.com" + $el.attr("href"); const operatorStats = await scrapeSpecificOperatorStats(linkUrl); return { name, imageUrl, linkUrl, operatorStats }; }); }); return Promise.all(statsPromises); } catch (error) { console.error("请求干员列表页面失败:", error.message); } }; scrapeOperatorStats(url) .then((results) => console.log(results)) .catch((error) => console.error(error));
内容的提问来源于stack exchange,提问作者Guido Aguiar
相关产品推荐
相关产品推荐

