Puppeteer定时调用十余次waitForSelector报30秒超时问题
问题原因说明
为什么设置了2分钟导航超时,实际报30秒超时
- Puppeteer的超时配置是分类型生效的:
setDefaultNavigationTimeout仅对goto、reload、页面跳转类的导航操作生效,你报错的waitForSelector属于元素等待类API,它的默认超时是独立的30000ms,没有单独配置的话就会触发30秒超时,和导航超时设置无关。
为什么运行10-15次后必现超时,重启服务才恢复
- 核心原因是资源泄漏:你当前的代码每次调用爬虫函数都会启动一个全新的Chrome浏览器实例,但逻辑全程没有调用
browser.close()、page.close()释放资源。运行十几次后,服务器后台会堆积十多个无头Chrome进程,吃光CPU、内存资源,后续新启动的Chrome根本无法在超时时间内完成页面渲染,自然等不到目标选择器。此时只有重启服务杀掉所有残留进程,释放资源后才能恢复正常。 - 额外的隐藏问题:
- 启动参数缺少服务端运行必备的
--disable-dev-shm-usage,多数Linux服务端的/dev/shm共享内存分区默认大小只有64M,Chrome默认往这个分区写临时数据,跑几次就会写满导致页面加载卡住。 - catch块没有接收error参数,真出异常也打不出完整错误栈,会误导排查方向。
- 每次调用都新建浏览器实例的开销极大,本身就会加速资源耗尽。
- 启动参数缺少服务端运行必备的
修复方案
- 统一超时配置:调用
page.setDefaultTimeout(120000)设置全局默认超时,所有操作(包括元素等待、导航)都会继承这个配置,也可以给waitForSelector单独传timeout参数覆盖。 - 补全资源回收逻辑:在
finally块里无论爬取成功还是失败,都关闭当前打开的页面,避免标签页、浏览器进程残留。 - 复用浏览器实例:不要每次调用爬虫都启动新Chrome,维护一个长驻的浏览器实例,每次调用只新建标签页即可,能降低90%以上的资源开销;可以加个逻辑每运行20次左右主动重启一次浏览器,规避Chrome长时间运行的内存泄漏问题。
- 补全Chrome启动参数,加上
--disable-dev-shm-usage适配服务端运行环境。 - 修正catch块写法,正确接收并打印异常信息。
修复后参考代码
const puppeteer = require('puppeteer'); // 全局维护浏览器实例,避免重复启动 let browser = null; async function initBrowser() { if (browser?.connected) return browser; browser = await puppeteer.launch({ headless: true, args: [ "--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage", // 解决服务端共享内存不足问题 ], }); // 浏览器意外退出时清空实例引用,下次调用自动重建 browser.on('disconnected', () => { browser = null; }); return browser; } async function runCrawler(baseUrl) { const browserIns = await initBrowser(); const page = await browserIns.newPage(); // 全局设置所有操作默认超时为2分钟 page.setDefaultTimeout(120000); try { // waitUntil用domcontentloaded比load更快,不需要等所有图片、静态资源加载完 await page.goto(baseUrl, { waitUntil: "domcontentloaded" }); await page.waitForSelector( "body > div.page-window.market-watch.compact > div > div.b > div.page-block > div > table > tbody > tr:nth-child(1) > td.symbol" ); // 此处编写你的数据爬取逻辑 // const result = await page.evaluate(() => {}) // return result } catch (error) { console.error('爬取执行异常:', error); throw error; } finally { // 无论成功失败,都关闭当前标签页,避免标签页堆积 if (!page.isClosed()) { await page.close().catch(() => {}); } } } // 每5分钟调用一次即可 // setInterval(() => runCrawler('你的目标地址'), 5 * 60 * 1000)
内容的提问来源于stack exchange,提问作者Ebuka
相关产品推荐
相关产品推荐

