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

使用Cheerio与Node.js做Web Scraping时的Promise问题求助

嘿,我之前用Cheerio和Node.js做爬虫的时候,也碰到过这种列表+详情页异步抓取的Promise问题!你的核心需求应该是先拿到文章列表的所有详情链接,再逐个抓取每个链接的内容对吧?大概率是你在处理多个异步请求的时候,没正确管理Promise的等待逻辑,导致要么部分请求没执行完程序就结束了,要么结果没正确收集。

先给你补全并修正代码,我基于你提供的片段重构一下,重点解决Promise的异步管理问题:

import rp from 'request-promise';
import cheerio from 'cheerio';
import conn from './connection';

const flexJob = `https://www.flexjobs.com`;
const flexJobCategory = ['account-management', 'bilingual'];

class WebScraper {
  constructor() {
    // 配置rp默认选项,加请求头避免被反爬拦截
    this.rpOptions = {
      transform: body => cheerio.load(body),
      headers: {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36'
      },
      timeout: 10000
    };
  }

  // 抓取单个分类的列表页面,返回所有详情页链接
  async getJobList(category) {
    const url = `${flexJob}/search?category=${category}`;
    try {
      const $ = await rp({ ...this.rpOptions, uri: url });
      // 这里的选择器需要你根据实际页面调整,假设列表项的标题链接是.job-title a
      const jobLinks = $('.job-title a').map((i, el) => {
        const href = $(el).attr('href');
        // 拼接完整URL,避免相对路径问题
        return href.startsWith('http') ? href : `${flexJob}${href}`;
      }).get(); // 把Cheerio类数组转成普通JS数组
      return jobLinks;
    } catch (err) {
      console.error(`抓取分类列表失败:${category}`, err);
      return [];
    }
  }

  // 抓取单个详情页的内容
  async getJobDetails(link) {
    try {
      const $ = await rp({ ...this.rpOptions, uri: link });
      // 解析详情页数据,选择器同样需要根据实际页面调整
      const jobData = {
        title: $('.job-header h1').text().trim(),
        description: $('.job-description').text().trim(),
        category: link.split('category=')[1]?.split('&')[0] || '未知',
        url: link
      };
      return jobData;
    } catch (err) {
      console.error(`抓取详情页失败:${link}`, err);
      // 单个请求失败时返回标记对象,避免影响Promise.all整体执行
      return { url: link, error: '抓取失败' };
    }
  }

  // 主方法:批量抓取所有分类的详情页数据
  async scrapeAllJobs() {
    const allJobData = [];
    // 用for...of循环保证分类抓取是串行的,避免同时请求多个分类页被封
    for (const category of flexJobCategory) {
      console.log(`开始抓取分类:${category}`);
      const jobLinks = await this.getJobList(category);
      if (jobLinks.length === 0) continue;

      // 把每个链接转换成Promise,用Promise.all等待所有请求完成
      const jobDetailsPromises = jobLinks.map(link => this.getJobDetails(link));
      const categoryJobData = await Promise.all(jobDetailsPromises);
      
      allJobData.push(...categoryJobData);
      console.log(`分类${category}抓取完成,共${categoryJobData.length}条数据`);
    }

    // 这里可以把数据存入数据库,用你导入的conn对象
    // await conn.collection('jobs').insertMany(allJobData);
    console.log('所有数据抓取完成!');
    return allJobData;
  }
}

// 启动爬虫
const scraper = new WebScraper();
scraper.scrapeAllJobs();

关键知识点解释

  • 为什么用map+Promise.all:如果用forEach循环处理异步请求,循环本身是同步的,不会等待内部的异步操作完成,程序会直接往下走,导致你还没拿到所有详情页数据就结束了。而map会把每个链接转换成Promise对象,Promise.all会等待所有Promise都完成(或失败)才继续执行。
  • 错误处理细节:每个异步方法都加了try/catch,而且在getJobDetails里即使单个请求失败,也会返回带错误标记的对象,这样不会导致Promise.all整体失败(如果想让个别失败的请求不影响全局结果,这个处理很重要)。
  • 并发控制提示:如果列表里有几百个链接,直接用Promise.all会一次性发起大量请求,很容易被网站封IP。可以用p-limit库控制并发数,比如限制同时只发5个请求:
    import pLimit from 'p-limit';
    const limit = pLimit(5);
    // 把map改成下面这样
    const jobDetailsPromises = jobLinks.map(link => limit(() => this.getJobDetails(link)));
    

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.26 11:09:12