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

Headless模式下Puppeteer爬虫.stdImg选择器返回空问题求助

问题描述

我正在开发一款基于Puppeteer的网页爬虫,用于提取商品详情(图片、标题、价格)。该爬虫在非无头模式(non-headless mode)下运行正常,但切换至无头模式(headless mode)时,用于提取图片的.stdImg选择器始终返回null。

观察到的行为

  • Headless: true + 设置User Agent:标题和价格提取正常,但.stdImg返回null
  • Headless: true + 空User Agent:标题、价格提取失败,.stdImg也返回null
  • Headless: false + 空User Agent:所有内容提取正常

由于该爬虫最终将作为Chrome扩展的一部分运行,必须使用无头模式,避免弹出浏览器窗口。

已尝试的解决方法

  • 通过移除navigator.webdriver、设置真实User Agent模拟非无头模式
  • 滚动页面处理懒加载图片
  • 通过截图调试两种模式下的DOM结构

爬虫代码

const { connect } = require("puppeteer-real-browser");

let browser = null;
let page = null;

//Configs
const connectOptions = {
    headless: false,
    devtools: true,
    args: [
        "--disable-features=site-per-process",
        "--no-sandbox",
        "--disable-setuid-sandbox",
        "--disable-gpu",
        "--hide-scrollbars",
        "--disable-extensions",
        "--disable-blink-features=AutomationControlled",
    ],
    customConfig: {},
    turnstile: true,
    connectOption: {},
    disableXvfb: false,
    ignoreAllFlags: false,
};

const viewPortOptions = { width: 1024, height: 768 };
const ExtraHTTPHeadersOptions = { "accept-language": "tr-TR,tr;q=0.9,en-US,en;q=0.8" };
const userAgent =
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";

//Load page
async function safeGoto(page, url, retries = 3) {
    for (let attempt = 1; attempt <= retries; attempt++) {
        try {
            await page.goto(url, { waitUntil: "domcontentloaded", timeout: 60000 });
            return;
        } catch (error) {
            console.error(`Attempt ${attempt} failed:`, error.message);
            if (attempt === retries) throw error;
        }
    }
}

//Extract data
async function safeEvaluate(page, selector, evaluateFn, errorMessage) {
    try {
        const element = await page.waitForSelector(selector, { timeout: 10000 });
        return await element.evaluate(evaluateFn);
    } catch (error) {
        console.error(errorMessage, error.message);
        return null;
    }
}

async function scrapeWebsite(url, selectors) {
    try {
        const { browser: connectedBrowser, page: connectedPage } = await connect(connectOptions);
        browser = connectedBrowser;

        const [page] = await browser.pages();
        await page.setUserAgent("");
        await page.setViewport(viewPortOptions);
        await page.setExtraHTTPHeaders(ExtraHTTPHeadersOptions);

        await page.waitForSelector("body", { timeout: 10000 });
        await safeGoto(page, url);
        await page.evaluate(() => {
            const imgs = document.querySelectorAll("img");
            imgs.forEach((img) => img.scrollIntoView({ behavior: "smooth", block: "center" }));
        });

        // Extract data
        const src = await safeEvaluate(
            page,
            selectors.image,
            (el) => el.src,
            `Failed to extract image from selector: ${selectors.image}`
        );

        const title = await safeEvaluate(
            page,
            selectors.title,
            (el) => el.textContent.trim().toLowerCase(),
            `Failed to extract title from selector: ${selectors.title}`
        );

        const value = await safeEvaluate(
            page,
            selectors.price,
            (el) => el.textContent.trim(),
            `Failed to extract price from selector: ${selectors.price}`
        );

        // Return extracted data
        const color = selectors.color;
        return { src, value, title, color, url };
    } catch (error) {
        console.error("Scraping failed:", error.message);
        throw error;
    } finally {
        if (page && !page.isClosed()) {
            try {
                await page.close();
            } catch (closeError) {
                console.error("Failed to close page:", closeError.message);
            }
        }
        if (browser && browser.isConnected()) {
            try {
                await browser.close();
            } catch (closeError) {
                console.error("Failed to close browser:", closeError.message);
            }
        }
        if (browser && browser.process() != null) browser.process().kill("SIGINT");
    }
}

const getFromDomain = (url) =>
    scrapeWebsite(url, {
        image: ".stdImg",
        title: ".classifiedDetailTitle h1",
        price: ".classified-price-wrapper",
        color: "#FFE800",
    });

选择器说明

  • .stdImg:图片选择器
  • .classifiedDetailTitle h1:标题选择器
  • .classified-price-wrapper:价格选择器

疑问

  1. 为何.stdImg选择器在无头模式下失效,但在空User Agent的非无头模式下正常?
  2. 如何修复该问题,实现无头模式下可靠提取图片?

解决方案

问题1的原因分析

无头模式下的Chrome与非无头模式存在核心差异,导致图片选择器失效:

  1. 浏览器特征检测:网站可能通过navigator.webdriver、窗口尺寸、渲染特性等识别无头浏览器,返回差异化DOM结构——比如延迟加载图片,甚至不渲染图片元素。
  2. 资源加载策略:无头模式默认会限制非关键资源(如图片)的加载优先级,.stdImg对应的图片可能被延迟或跳过;非无头模式下浏览器会自动加载所有可见资源,因此能正常获取。
  3. UA标识泄露:即使设置了真实User Agent,无头模式下Chrome的UA会隐含HeadlessChrome标识,网站识别后会调整资源加载逻辑。

问题2的修复方案

针对上述原因,通过以下步骤调整代码,确保无头模式下正常提取图片:

1. 完善无头模式配置

修改connectOptions,启用新版无头模式并补充参数消除自动化特征:

const connectOptions = {
    headless: "new", // 使用新版无头模式,行为更接近真实浏览器
    devtools: false, // 无头模式无需开启开发者工具
    args: [
        "--disable-features=site-per-process",
        "--no-sandbox",
        "--disable-setuid-sandbox",
        "--disable-gpu",
        "--hide-scrollbars",
        "--disable-extensions",
        "--disable-blink-features=AutomationControlled",
        "--start-maximized", // 最大化窗口,避免尺寸检测
        "--disable-web-security", // 规避部分网站的跨域限制
        "--allow-running-insecure-content",
    ],
    customConfig: {},
    turnstile: true,
    connectOption: {},
    disableXvfb: false,
    ignoreAllFlags: false,
};

2. 正确设置User Agent并隐藏自动化特征

使用真实UA,同时在页面加载前清除自动化标识:

async function scrapeWebsite(url, selectors) {
    try {
        const { browser: connectedBrowser, page: connectedPage } = await connect(connectOptions);
        browser = connectedBrowser;

        const [page] = await browser.pages();
        await page.setUserAgent(userAgent); // 使用定义好的真实UA,不要设为空
        await page.setViewport(viewPortOptions);
        await page.setExtraHTTPHeaders(ExtraHTTPHeadersOptions);

        // 隐藏自动化特征,避免被网站检测
        await page.evaluateOnNewDocument(() => {
            delete navigator.__proto__.webdriver;
            Object.defineProperty(navigator, 'maxTouchPoints', { get: () => 1 });
        });

        // 等待页面完全加载,包括所有资源
        await safeGoto(page, url, 3);
        await page.waitForLoadState("networkidle2"); // 等待网络空闲,确保资源加载完成

        // 针对懒加载图片,等待元素出现并加载完成
        await page.waitForSelector(selectors.image, { timeout: 15000 });
        await page.evaluate((selector) => {
            const img = document.querySelector(selector);
            if (img) {
                img.scrollIntoView({ behavior: "instant", block: "center" });
                // 等待图片加载完成
                return new Promise(resolve => {
                    if (img.complete) resolve();
                    else img.onload = resolve;
                });
            }
        }, selectors.image);

        // 提取数据,兼容懒加载的data-src属性
        const src = await safeEvaluate(
            page,
            selectors.image,
            (el) => el.src || el.dataset.src || el.getAttribute('src'),
            `Failed to extract image from selector: ${selectors.image}`
        );

        // 其余提取逻辑保持不变...

3. 优化资源等待策略

  • 将waitUntil从domcontentloaded改为networkidle2,确保页面所有资源加载完成;
  • 针对图片元素额外等待load事件触发,确认图片已渲染到DOM中;
  • 兼容懒加载图片的data-src等属性,避免只读取src导致为空。

4. 调试验证

如果问题仍存在,可在无头模式下生成页面截图,对比非无头模式的DOM结构:

await page.screenshot({ path: 'headless-screenshot.png', fullPage: true });

根据截图调整选择器或等待逻辑。


内容的提问来源于stack exchange,提问作者mr.sun

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 22:14:55