使用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
相关产品推荐
相关产品推荐

