如何在Node.js+Puppeteer中用异步IO循环实现多标签页爬取
在Node.js + Puppeteer中实现固定并发数的标签页爬取池
你在Go里用Worker Pool+通道控制并发的思路非常靠谱,放到Node.js+Puppeteer的场景里,我们可以通过异步任务队列+并发数控制来实现一模一样的效果——固定数量的标签页并行运行,同时在等待网络IO的间隙处理新任务,最大化爬取效率。
先给你一个完整的可运行示例,然后拆解关键逻辑:
const puppeteer = require('puppeteer'); async function runCrawler() { // 配置参数 const maxThreads = 5; // 你想要的固定标签页数量 const uncrawledLinks = new Map([ ['https://example.com/page1', false], ['https://example.com/page2', false], ['https://example.com/page3', false], // 这里替换成你的实际链接集合 ]); // 把未爬取链接转成任务队列(方便动态取任务) const taskQueue = Array.from(uncrawledLinks.keys()); let runningThreads = 0; const browser = await puppeteer.launch({ headless: 'new' }); // 单个任务的处理函数(Worker) async function handleLink(link) { let page = null; try { runningThreads++; console.log(`启动新任务:${link},当前并发数:${runningThreads}`); page = await browser.newPage(); // 这里写你的爬取逻辑:比如跳转页面、提取数据等 await page.goto(link, { waitUntil: 'networkidle2' }); const pageTitle = await page.title(); console.log(`完成爬取:${link},标题:${pageTitle}`); // 标记为已爬取 uncrawledLinks.set(link, true); } catch (error) { console.error(`爬取失败:${link},错误:${error.message}`); } finally { // 不管成功失败,都要关闭页面、释放并发名额 if (page) await page.close(); runningThreads--; console.log(`任务结束:${link},当前并发数:${runningThreads}`); // 队列还有任务的话,继续启动下一个 if (taskQueue.length > 0) { const nextLink = taskQueue.shift(); await handleLink(nextLink); } } } // 初始化启动第一批任务,填满并发池 while (runningThreads < maxThreads && taskQueue.length > 0) { const link = taskQueue.shift(); // 这里用void,避免阻塞初始化流程,让任务异步执行 void handleLink(link); } // 等待所有任务完成(可选,如果你需要在所有任务结束后做清理) while (runningThreads > 0) { await new Promise(resolve => setTimeout(resolve, 100)); } await browser.close(); console.log("所有爬取任务完成"); } runCrawler();
关键逻辑拆解
任务队列+并发计数器:
- 用
taskQueue存储待爬取的链接,避免直接在for...of里用await导致的串行执行问题。 runningThreads跟踪当前活跃的标签页数量,确保不超过maxThreads的限制,防止浏览器资源耗尽。
- 用
Worker函数的自驱动:
- 每个
handleLink任务完成后,会自动检查队列是否还有剩余任务,如果有就启动下一个,这样就能在网络IO等待的间隙自动填充并发池,不用手动循环等待。 finally块里的清理逻辑非常重要:必须关闭页面释放资源,同时减少并发计数器,否则会导致浏览器资源泄漏或者并发数失控。
- 每个
初始化并发池:
- 启动时先填满
maxThreads个任务,让并发池一开始就处于满负荷状态,最大化爬取效率。 - 用
void handleLink(link)是为了让任务异步执行,不会阻塞初始化的循环,确保第一批任务能同时启动。
- 启动时先填满
优化建议
- 页面复用:如果你的爬取逻辑比较固定,可以维护一个页面池(比如用数组存储闲置页面),不用每次都新建/关闭页面,能大幅提升性能。
- 错误重试:可以在
catch块里把失败的链接重新放回队列,设置重试次数上限,避免因网络波动导致的任务失败。 - 动态添加任务:如果爬取过程中发现新的链接,可以直接
push到taskQueue里,实现增量爬取。
内容的提问来源于stack exchange,提问作者Vega
相关产品推荐
相关产品推荐

