Express中Puppeteer-Cluster+Cheerio的API返回空响应问题
问题排查与修复方案
核心问题分析
你的API返回空数组,主要是以下几个关键错误导致:
- 错误获取页面内容:Node环境中无法直接访问
document对象,必须通过Puppeteer的page.evaluate()获取页面HTML。 - 未声明的变量/函数:多处使用未定义的标识符(
topics、tempUrls、exceptions、getImageFromElement),导致代码静默失败。 - 全局数据污染:
result数组定义在路由外部,会导致跨请求数据累积,且异步执行顺序问题会导致首次请求返回空。 - 异步逻辑误用:对非异步函数使用
await,无意义且可能引发逻辑混乱。
修复后的完整代码(cluster.js)
const express = require("express"); const { Cluster } = require("puppeteer-cluster"); const puppeteer = require("puppeteer-extra"); const cheerio = require("cheerio"); const StealthPlugin = require("puppeteer-extra-plugin-stealth"); var router = express.Router(); const newspapers = [ { "name": "CNN", "address": "https://edition.cnn.com/specials/world/cnn-climate", "base": "https://edition.cnn.com" }, { "name": "The Guardian", "address": "https://www.theguardian.com/environment/climate-crisis", "base": "https://www.theguardian.com" } ]; // 补全缺失的配置变量 const topics = ["climate", "global warming", "emissions"]; // 示例合法关键词,可按需扩展 const exceptions = ["facebook.com", "twitter.com", "instagram.com"]; // 示例要跳过的社交链接 puppeteer.use(StealthPlugin()); router.get("/", async (req, res) => { // 将result移到路由内部,避免跨请求污染 const result = []; // 补全去重数组,每个请求独立维护 const tempUrls = []; // Query String const query = checkForQuery(req); const wordsToSearch = query ? verifyQuery(query) : null; console.log("Running tests.."); // 补全缺失的getImageFromElement函数 function getImageFromElement(imgElement) { return imgElement.attr("src") || imgElement.attr("data-src") || ""; } function checkForQuery(request) { if (request.originalUrl.indexOf("?") !== -1) { console.log(request.query); return request.query; } else { return false; } } function verifyQuery(queryString) { const queryParams = { only: queryString.only ? queryString.only : "", also: queryString.also ? queryString.also : "", }; var newList = { only: [], also: [], }; for (const [key, value] of Object.entries(queryParams)) { const tempId = key.toString(); const tempVal = value.length >= 2 ? value.split(",") : [value]; // 统一转为数组处理,避免单值判断问题 console.log(value, " and ", tempVal); tempVal.forEach((term) => { if (topics.includes(term.trim())) { // 用includes替代indexOf,更直观 newList[tempId].push(term.trim()); } }); } console.log(newList); // 如果两个数组都为空,返回null return newList.only.length > 0 || newList.also.length > 0 ? newList : null; } function storeData(element, base, name) { const results = []; element.find("style").remove(); const title = element.text().trim(); const urlRaw = element.attr("href"); if (!urlRaw) return results; // 跳过无href的a标签 const url = urlRaw.startsWith("http") ? urlRaw : `${base}${urlRaw}`; // 检查重复链接 if (!tempUrls.includes(url)) { // 跳过社交链接 if (!exceptions.some(el => url.toLowerCase().includes(el))) { tempUrls.push(url); const imageElement = element.find("img"); const imgUrl = imageElement.length > 0 ? getImageFromElement(imageElement) : ""; results.push({ title: title.replace(/(\r\n|\n|\r)/gm, ""), url, source: name, ...(imgUrl && { imgUrl }) // 仅当有图片时添加imgUrl字段 }); } } return results; } function getElementsCheerio(html, base, name, searchterms) { console.log(base, name); // 移除html打印,避免控制台输出过载 const $ = cheerio.load(html); const concatInfo = []; if (searchterms) { const termsAlso = searchterms.also; const termsOnly = searchterms.only; termsAlso.forEach((term) => { $(`a:contains("climate"):contains(${term})`).each(function () { const tempData = storeData($(this), base, name); concatInfo.push(...tempData); // 用扩展运算符替代map+push,更高效 }); }); termsOnly.forEach((term) => { $(`a:contains(${term})`).each(function () { const tempData = storeData($(this), base, name); concatInfo.push(...tempData); }); }); } else { $('a:contains("climate")').each(function () { const tempData = storeData($(this), base, name); concatInfo.push(...tempData); }); } return concatInfo; } try { const cluster = await Cluster.launch({ concurrency: Cluster.CONCURRENCY_CONTEXT, maxConcurrency: 2, puppeteerOptions: { headless: "new", // 使用新版无头模式,稳定性更好 args: ["--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage"], // 添加参数避免内存问题 userDataDir: "./tmp", defaultViewport: false, }, }); await cluster.task(async ({ page, data }) => { await page.goto(data.address, { waitUntil: "networkidle2" }); // 等待网络空闲,确保页面渲染完成 // 通过page.evaluate获取页面HTML const pageHtml = await page.evaluate(() => document.body.innerHTML); // getElementsCheerio是同步函数,无需await const elements = getElementsCheerio( pageHtml, data.base, data.name, wordsToSearch ); if (elements.length > 0) { result.push(...elements); // 合并结果,避免嵌套数组 } }); newspapers.forEach(newspaper => cluster.queue(newspaper)); // 用forEach替代map,语义更清晰 await cluster.idle(); await cluster.close(); res.json(result); } catch (error) { console.error("Scraping failed:", error); res.status(500).json({ error: "Internal server error", details: error.message }); } }); module.exports = router;
关键修复点说明
- 页面内容获取:用
page.evaluate(() => document.body.innerHTML)替代直接访问document,这是Node环境中获取Puppeteer页面内容的正确方式。 - 补全缺失依赖:添加了
topics(合法关键词列表)、exceptions(跳过的链接域名)、getImageFromElement(提取图片URL的函数),并将tempUrls移到路由内部,确保每个请求独立去重。 - 修复异步逻辑:移除了
getElementsCheerio前的await,因为它是同步函数;在page.goto中添加waitUntil: "networkidle2",确保页面JS渲染完成。 - 数据结构优化:用扩展运算符
...合并结果数组,避免返回嵌套数组;修复verifyQuery中的数组处理逻辑,统一转为数组遍历,避免单值时的判断错误。 - 错误处理:添加
try/catch块捕获爬取过程中的错误,返回500状态码和错误信息,便于调试。 - Puppeteer配置优化:使用新版无头模式
headless: "new",添加--disable-dev-shm-usage参数避免容器环境下的内存问题。
内容的提问来源于stack exchange,提问作者Bella
相关产品推荐
相关产品推荐

