Puppeteer性能对比:$eval与innerHTML爬虫方案选型咨询
问题:Puppeteer爬虫两种字段提取方式的性能对比
我目前正在使用Puppeteer开发一款网页爬虫,用于抓取单篇文章的详情(类似亚马逊商品信息)。
此前我通过获取所有文章元素的Element Handles,再使用$eval结构化提取所需内容,抓取400个元素耗时12-15秒。
我尝试改用$$eval获取400个元素的innerHTML,仅耗时5秒,考虑配合HTML解析器处理。
我的问题是:针对每个元素需提取5个字段的场景,使用$eval逐个处理Element Handles是否比HTML解析更优?
原方案代码(耗时12-15秒)
//getting element handles for articles let articleHandles = await page.$$('.SearchResult_searchResultItemWrapper__VVVnZ') //looping through handles to get info and push to array for (handle of articleHandles) { const articleLink = await handle.$eval('a', e => e.getAttribute('href')); const articleImg = await handle.$eval('a>div>span>img', e => e.getAttribute('src')); const articleDesc = await handle.$eval('div>div', e => e.innerText); const dealerHandle = await handle.$('div.SearchResult_companyWrapper__W5gTQ', e => e.innerText); const dealer = dealerHandle ? await dealerHandle.evaluate(e => e.innerText) : "Not found"; let detailsOne = (cleanupArticleDescription(articleDesc)); let detailsTwo = { link: articleLink, img: "https://www.****" + articleImg, dealer: dealer.substring(dealer.indexOf(' ') + 1) } const article = { ...detailsOne, ...detailsTwo } articlesAll.push(article); }
innerHTML方案代码(耗时5秒)
let articleHandles = await page.$$eval('.SearchResult_searchResultItemWrapper__VVVnZ', e => { return e.map(a => a.innerHTML) }) for (handle of articleHandles) { console.log(handle) }
补充信息
- 目标页面采用无限滚动,我滚动10次加载400个元素
- 我也可通过URL参数切换页面,但因使用Bright Data的爬虫浏览器需支付流量费用,为避免额外开销,当前每抓取400个元素后跳过10页(从第1页跳转至第11页)。页面本身对无限滚动加载的元素数量有限制,我发现每10页通过URL参数加载新页面是兼顾性能与开销的最优方式。
回答
从你给出的耗时数据来看,$$eval批量处理后配合HTML解析(或直接在浏览器端提取结构化数据)的方案,性能远优于逐个$eval处理Element Handles的方式,尤其针对百万级的抓取需求,这种性能差距会被持续放大,是更优选择。
原方案慢的核心原因
原方案的瓶颈在于频繁的跨进程通信:每次$eval和evaluate都是Node.js环境与浏览器页面环境之间的异步交互,400个元素每个要触发4-5次这类交互,累计上千次的异步调用延迟,直接导致了12-15秒的耗时。
最优优化方向
直接在浏览器端完成所有字段提取:
完全可以把所有提取逻辑整合到$$eval的回调中,一次性返回结构化数据,只需要一次跨进程通信,耗时会和你获取innerHTML的5秒持平,还省去了本地解析HTML的步骤:const articlesAll = await page.$$eval('.SearchResult_searchResultItemWrapper__VVVnZ', items => { // 把cleanupArticleDescription的逻辑移到这里,避免数据来回传输 const cleanupArticleDescription = (desc) => { // 原函数的处理逻辑 return /* 处理后的结果 */; }; return items.map(item => { const articleLink = item.querySelector('a')?.getAttribute('href') || ''; const articleImg = item.querySelector('a>div>span>img')?.getAttribute('src') || ''; const articleDesc = item.querySelector('div>div')?.innerText || ''; const dealerEl = item.querySelector('div.SearchResult_companyWrapper__W5gTQ'); const dealer = dealerEl ? dealerEl.innerText.substring(dealerEl.innerText.indexOf(' ') + 1) : "Not found"; const detailsOne = cleanupArticleDescription(articleDesc); return { ...detailsOne, link: articleLink, img: "https://www.****" + articleImg, dealer: dealer }; }); });若需使用HTML解析器:
推荐用cheerio这类轻量库解析innerHTML,解析速度快,整体耗时仍远低于原方案,但相比直接在浏览器端提取,会多一步本地解析的开销,属于次优选择。
额外性能建议
- 保持当前每10页跳转一次的策略,减少Bright Data的流量开销和页面跳转耗时
- 无限滚动时,用
page.waitForSelector配合滚动逻辑,确保元素完全加载后再抓取,避免数据遗漏 - 百万级数据抓取建议分批次写入数据库,避免内存溢出
内容的提问来源于stack exchange,提问作者EnKaya
相关产品推荐
相关产品推荐

