Node.js中Puppeteer导航超时错误求助及代码排查
Puppeteer爬取导航超时问题解决
问题分析
出现TimeoutError: Navigation timeout of 30000 ms exceeded通常是因为页面加载耗时超过默认30秒限制,可能是目标网站资源加载量大、存在反爬限制,或是代码中的导航/等待逻辑不够健壮导致。
解决方案
1. 调整导航超时时间
在page.goto时显式设置更长的超时时间,或临时禁用超时(仅应急使用):
// 设置60秒超时 await page.goto(url, { timeout: 60000 }); // 禁用超时(不推荐长期使用) await page.goto(url, { timeout: 0 });
2. 拦截非必要资源加载
禁止加载图片、样式、字体等非核心资源,大幅减少页面加载时间:
await page.setRequestInterception(true); page.on('request', (req) => { const blockedTypes = ['image', 'stylesheet', 'font']; if (blockedTypes.includes(req.resourceType())) { req.abort(); } else { req.continue(); } });
3. 优化等待逻辑
- 给
waitForSelector设置合理超时,避免无限等待 - 导航时指定
waitUntil: 'domcontentloaded',确保核心DOM加载完成即可:
// 等待目标元素,设置30秒超时 await page.waitForSelector('div#model-brief-specifications', { timeout: 30000 }); // 导航时等待DOM加载完成 await page.goto(deviceLink, { waitUntil: 'domcontentloaded' });
4. 增加导航重试机制
对超时的页面进行重试,避免单次失败导致程序中断:
async function gotoWithRetry(page, url, maxRetries = 3) { let retries = 0; while (retries < maxRetries) { try { await page.goto(url, { timeout: 60000, waitUntil: 'domcontentloaded' }); return; } catch (err) { retries++; console.log(`导航失败,重试第${retries}次: ${url}`); if (retries === maxRetries) throw err; await new Promise(resolve => setTimeout(resolve, 2000)); // 重试前等待2秒 } } }
5. 修复CSV写入的异步问题
原代码中csvWriter.writeRecords是异步操作但未等待,可能导致数据写入混乱,需用await确保完成:
// 无需手动写表头,csv-writer会自动处理 await csvWriter.writeRecords([deviceDetails]); console.log('数据已追加到CSV');
注意:初始化
createCsvWriter时,append: false会自动写入表头;后续运行改为append: true可避免覆盖已有数据。
6. 优化元素提取逻辑
原代码多次调用page.evaluate遍历DOM,效率低下,改为一次evaluate提取所有数据:
const deviceDetails = await page.evaluate((keysToExtract) => { const details = {}; const specItems = document.querySelectorAll('div#model-brief-specifications b'); specItems.forEach(item => { const key = item.textContent.trim(); if (keysToExtract.includes(key)) { details[key] = item.nextSibling?.textContent.trim() || ''; } }); // 给未找到的字段设置空字符串 keysToExtract.forEach(key => { if (!details.hasOwnProperty(key)) { details[key] = ''; } }); return details; }, keysToExtract);
修正后的完整代码
const puppeteer = require('puppeteer'); const createCsvWriter = require('csv-writer').createObjectCsvWriter; (async () => { const keysToExtract = [ 'Dimensions', 'Weight', 'SoC', 'CPU', 'Cores', 'GPU', 'RAM', 'Storage', 'Memory cards', 'Display', 'Battery', 'OS', 'Camera', 'SIM card', 'Wi-Fi', 'USB', 'Bluetooth', 'Positioning' ]; const browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox'] }); const page = await browser.newPage(); // 使用普通浏览器UA,避免被反爬识别 const userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36'; await page.setUserAgent(userAgent); // 拦截非必要资源 await page.setRequestInterception(true); page.on('request', (req) => { const blockedTypes = ['image', 'stylesheet', 'font']; if (blockedTypes.includes(req.resourceType())) { req.abort(); } else { req.continue(); } }); // 带重试的导航函数 async function gotoWithRetry(page, url, maxRetries = 3) { let retries = 0; while (retries < maxRetries) { try { await page.goto(url, { timeout: 60000, waitUntil: 'domcontentloaded' }); console.log('成功导航到:', url); return; } catch (err) { retries++; console.log(`导航超时,重试第${retries}次: ${url}`); if (retries === maxRetries) { console.log(`多次导航失败,跳过: ${url}`); throw err; } await new Promise(resolve => setTimeout(resolve, 2000)); } } } // 初始化CSV写入器 const csvWriter = createCsvWriter({ path: 'device_details.csv', header: keysToExtract.map(key => ({ id: key, title: key })), append: false // 首次运行自动写表头,后续改为true避免覆盖 }); try { // 导航到品牌列表页 await gotoWithRetry(page, 'https://www.devicespecifications.com/en/brand-more'); const brandLinks = await page.$$eval('.brand-listing-container-news a', links => links.map(a => a.href)); for (const brandLink of brandLinks) { try { await gotoWithRetry(page, brandLink); const deviceLinks = await page.$$eval('div.model-listing-container-80 h3 a', links => links.map(a => a.href)); for (const deviceLink of deviceLinks) { try { await gotoWithRetry(page, deviceLink); // 等待规格容器加载 await page.waitForSelector('div#model-brief-specifications', { timeout: 30000 }); // 一次性提取所有设备信息 const deviceDetails = await page.evaluate((keys) => { const details = {}; const specItems = document.querySelectorAll('div#model-brief-specifications b'); specItems.forEach(item => { const key = item.textContent.trim(); if (keys.includes(key)) { details[key] = item.nextSibling?.textContent.trim() || ''; } }); keys.forEach(key => { if (!details[key]) details[key] = ''; }); return details; }, keysToExtract); console.log('提取到设备信息:', deviceDetails); // 写入CSV await csvWriter.writeRecords([deviceDetails]); console.log('数据已写入CSV'); // 增加请求间隔,避免触发反爬 await new Promise(resolve => setTimeout(resolve, 1000)); } catch (err) { console.log('处理设备页面失败:', deviceLink, err.message); continue; } } } catch (err) { console.log('处理品牌页面失败:', brandLink, err.message); continue; } } } catch (err) { console.log('主流程出错:', err.message); } finally { await browser.close(); console.log('浏览器已关闭'); } })();
额外建议
- 避免使用Googlebot的User-Agent,容易被反爬系统识别
- 控制请求频率,增加页面跳转间隔,降低被封禁风险
- 可尝试使用多页面实例并行处理,但需注意并发量不要过高
内容的提问来源于stack exchange,提问作者Lak
相关产品推荐
相关产品推荐

