Puppeteer批量爬取UPC关联网页时连续操作导致整台电脑系统冻结
问题根因
- 代码中
results.map(processRow)的写法会同步触发所有任务的Promise,没有任何等待逻辑、没有并发数限制,700个UPC对应的puppeteer.launch()会在极短时间内全部执行。每个独立的Chromium进程本身需要占用300-500MB内存,700个进程同时启动会直接把16GB内存全部占满,触发系统资源耗尽保护,导致整个系统无响应冻结。 - 出现的MaxListenersExceededWarning是并发失控的附带表现:每次调用
puppeteer.launch()时,Puppeteer内部会给Node进程的exit、SIGINT、SIGTERM、SIGHUP事件绑定资源清理的监听函数。正常浏览器退出后这些监听会被移除,但因为短时间内启动了上百个浏览器实例,绑定的监听数量超过了Node默认的10个上限,就会弹出对应警告。警告本身不会导致系统冻结,只是在提示进程的资源占用已经处于失控状态。
修复方案
- 取消“每个UPC启动一个独立浏览器实例”的逻辑,全局仅启动1个Puppeteer浏览器实例,所有爬取任务复用这个实例,通过新开标签页的方式执行爬取,内存占用会降到原来的几十分之一。
- 增加严格的并发数上限,杜绝一次性把所有700个任务全部触发的写法,根据机器内存调整同时运行的任务数,16GB内存的环境下同时跑3-5个爬取标签页完全不会造成内存压力。
- 替换原来无等待、无错误捕获的
results.map(processRow)调用,增加任务执行的完成等待、错误捕获机制,避免爬取任务异常退出后残留资源占用。 - 给每个页面加载流程增加超时限制,避免目标网站响应慢导致任务一直卡住占着资源不释放。
修改后的参考实现
import puppeteer from 'puppeteer'; import parse from 'csv-parse'; import { promises as fs } from "fs"; // 全局单例浏览器,仅启动一次 let browser: puppeteer.Browser; // 并发上限,16GB内存环境推荐设置为3 const MAX_CONCURRENT_TASKS = 3; const parseCsv = async (filename: string): Promise<parse.Parser> => { let contents: Buffer = await fs.readFile(filename); return parse(contents); } const processRow = async (row: string[]) => { // 复用全局浏览器实例,不再重复启动Chromium const page = await browser.newPage(); // 配置30秒页面加载超时,避免任务卡死 await page.goto(`https://www.example.com/search/?q=${row[5]}`, { timeout: 30000, waitUntil: 'domcontentloaded' }); // 爬取完成后立刻关闭标签页释放内存 await page.close(); } // 通用带并发限制的任务执行器 const runTasksWithConcurrency = async (taskList: (() => Promise<void>)[], limit: number) => { const activeTasks = new Set<Promise<void>>(); for (const task of taskList) { const currentTask = task().then(() => activeTasks.delete(currentTask)); activeTasks.add(currentTask); // 当正在执行的任务数达到上限时,等待任意一个任务完成再继续派发新任务 if (activeTasks.size >= limit) { await Promise.race(activeTasks); } } // 等待所有剩余任务执行完成 await Promise.all(activeTasks); } (async () => { console.log('Reading file'); const parser = await parseCsv(process.argv[process.argv.length - 1]); const validRows: string[][] = []; console.log('Extracting data'); for await (let row of parser) { if (/\d/.test(row[5])) { validRows.push(row); } } // 全流程仅启动一次浏览器实例 browser = await puppeteer.launch({ // 启用新版无头模式进一步降低内存占用 headless: 'new' }); try { console.log('Scraping'); // 构造带行参数的任务列表,传入并发限制执行 const taskList = validRows.map(row => () => processRow(row)); await runTasksWithConcurrency(taskList, MAX_CONCURRENT_TASKS); } finally { // 无论任务成功或失败,强制关闭浏览器释放所有残留资源 await browser.close(); } })();
内容的提问来源于stack exchange,提问作者Nailuj29
相关产品推荐
相关产品推荐

