You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 14:03:28