使用Crawlee+Playwright爬取无限滚动页面时DOM元素异常问题
解决无限滚动爬取最后一页数据丢失及DOM元素异常问题
问题根源
playwrightUtils.infiniteScroll 工具函数在处理最后一页时,会尝试点击已被DOM移除的「加载更多」按钮,导致抛出 elementHandle.click: Element is not attached to the DOM 异常,中断爬取流程,进而无法捕获最后一页的API响应。
解决方案
替换默认的无限滚动工具,实现自定义滚动逻辑,增加对按钮状态的检查和异常捕获,确保流程能优雅结束并完整捕获所有API响应。
关键优化点
- 自定义滚动循环:每次迭代前检查按钮是否存在、可见且可交互
- 异常容错处理:捕获按钮不存在/已脱离DOM等常见错误,主动终止循环
- 响应捕获保障:确保所有API请求完成后再结束爬取流程
修改后的完整代码
import { PlaywrightCrawler, Dataset, playwrightUtils } from 'crawlee'; const productItemScrollPage = { domain: 'https://www.chedraui.com.mx', url: "https://www.chedraui.com.mx/mascotas/perros/alimento", jsonResponseProductListPropertyPath: "data.productSearch.products", jsonRequestProductPageUrl: "https://www.chedraui.com.mx/_v/segment/graphql/v1", urlAttributesToMatch: ["operationName=productSearchV3"], hasLoadMoreBtn: false, loadMoreBtnSelector: "" } const productItemScrollPageV2 = { domain: 'https://www.heb.com.mx', url: "https://www.heb.com.mx/mascotas/perros/alimento-seco", jsonResponseProductListPropertyPath: "data.productSearch.products", jsonRequestProductPageUrl: "https://www.heb.com.mx/_v/segment/graphql/v1", urlAttributesToMatch: ["operationName=productSearchV3"], hasLoadMoreBtn: true, loadMoreBtnSelector: ".vtex-search-result-3-x-buttonShowMore button" } // 优化URL匹配逻辑,更简洁高效 function areAllAttributesPresentInTheURLRequest(url, config) { return config.urlAttributesToMatch.every(attr => url.includes(attr)); } const crawler = new PlaywrightCrawler({ headless: false, maxRequestsPerCrawl: 20, maxRequestRetries: 10, retryOnBlocked: true, requestHandler: async ({ page, request }) => { const productResponses = []; // 监听API请求完成事件,捕获产品数据 page.on('requestfinished', async (req) => { if(req.url().startsWith(productItemScrollPageV2.jsonRequestProductPageUrl) && areAllAttributesPresentInTheURLRequest(req.url(), productItemScrollPageV2) ){ try { const res = await req.response(); if (res) { const jsonObj = await res.json(); productResponses.push(jsonObj); // 实时保存数据到Dataset await Dataset.pushData(jsonObj.data.productSearch.products); } } catch (err) { console.error('解析响应失败:', err); } } }); // 自定义无限滚动逻辑 const buttonSelector = productItemScrollPageV2.loadMoreBtnSelector; while (true) { try { // 等待按钮出现,超时5秒则认为已无更多内容 const button = await page.waitForSelector(buttonSelector, { timeout: 5000 }); if (!button) break; // 检查按钮是否可交互(可见且启用) const isVisible = await button.isVisible(); const isEnabled = await button.isEnabled(); if (!isVisible || !isEnabled) break; // 点击按钮并等待网络请求完成 await button.click(); await page.waitForLoadState('networkidle', { timeout: 30000 }); } catch (error) { // 捕获常见终止信号,优雅退出循环 const errorMessages = [ 'not attached to the DOM', 'Timeout', 'element not visible', 'element not enabled' ]; if (errorMessages.some(msg => error.message.includes(msg))) { console.log('已加载所有页面,退出滚动循环'); break; } // 其他错误打印日志后继续尝试 console.error('滚动过程中出现错误:', error); continue; } } // 额外等待1秒确保最后一批请求完成 await page.waitForTimeout(1000); console.log(`共捕获${productResponses.length}页产品数据`); }, }); // 启动爬虫 await crawler.run([productItemScrollPageV2.url]);
代码说明
- URL匹配优化:使用
every方法替代原filter+length判断,逻辑更简洁高效 - 自定义滚动循环:每次迭代前严格检查按钮状态,避免无效点击
- 异常处理:针对性捕获终止类错误,确保流程不被意外中断
- 响应捕获:实时保存数据到Dataset,同时保留内存中的响应数组用于后续处理
- 收尾等待:循环结束后额外等待1秒,确保最后一页的API请求完全完成并被捕获
内容的提问来源于stack exchange,提问作者user22647102
相关产品推荐
相关产品推荐

