You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Express中Puppeteer-Cluster+Cheerio的API返回空响应问题

问题排查与修复方案

核心问题分析

你的API返回空数组,主要是以下几个关键错误导致:

  1. 错误获取页面内容:Node环境中无法直接访问document对象,必须通过Puppeteer的page.evaluate()获取页面HTML。
  2. 未声明的变量/函数:多处使用未定义的标识符(topics、tempUrls、exceptions、getImageFromElement),导致代码静默失败。
  3. 全局数据污染:result数组定义在路由外部,会导致跨请求数据累积,且异步执行顺序问题会导致首次请求返回空。
  4. 异步逻辑误用:对非异步函数使用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;

关键修复点说明

  1. 页面内容获取:用page.evaluate(() => document.body.innerHTML)替代直接访问document,这是Node环境中获取Puppeteer页面内容的正确方式。
  2. 补全缺失依赖:添加了topics(合法关键词列表)、exceptions(跳过的链接域名)、getImageFromElement(提取图片URL的函数),并将tempUrls移到路由内部,确保每个请求独立去重。
  3. 修复异步逻辑:移除了getElementsCheerio前的await,因为它是同步函数;在page.goto中添加waitUntil: "networkidle2",确保页面JS渲染完成。
  4. 数据结构优化:用扩展运算符...合并结果数组,避免返回嵌套数组;修复verifyQuery中的数组处理逻辑,统一转为数组遍历,避免单值时的判断错误。
  5. 错误处理:添加try/catch块捕获爬取过程中的错误,返回500状态码和错误信息,便于调试。
  6. Puppeteer配置优化:使用新版无头模式headless: "new",添加--disable-dev-shm-usage参数避免容器环境下的内存问题。

内容的提问来源于stack exchange,提问作者Bella

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.07 14:45:24