返回知识中心
入门

Puppeteer 抓取:启动选项与页面可见差异

区分 Puppeteer 的启动、视口、持久存储和无头选项,在自有目录页记录页面可见值与数据校验结果。

BotBrowser Team

Puppeteer 抓取:启动选项与页面可见差异
文档中心

想直接进入 入门 文档吗?

这篇文章属于知识中心的指南库。若你要步骤化配置、参考说明和持续更新,请直接进入对应 docs 分区。

BotBrowser 文档说明通过 puppeteer-core、executablePath、defaultViewport 和 --bot-profile 启动使用配置文件的 Puppeteer 浏览器;它不保证第三方响应或检测结果。

要点

Puppeteer 抓取设置具有不同作用域。executablePath、headless 和 userDataDir 影响浏览器进程;defaultViewport 影响页面模拟;BrowserContext 管理进程中的会话存储。页面可读取视口尺寸或 navigator.webdriver 等结果,但这些值不是检测结论、身份凭证或授权决定。

使用自己拥有的目录页,固定二进制和配置文件,每次只改一个选项,保存最终 URL、视口值、navigator.webdriver、通过校验的记录数和诊断。这样能够比较可证伪的差异,而不声称某种模式更受接受或更保护隐私。

目录

进程启动选项

Puppeteer 启动集成说明包安装和初次配置连接;下面的选项比较假设该集成已经配置完成。

executablePath 选择启动的可执行文件。路径存在不能证明加载了预期构建、配置文件已被接受或目标站点会响应。在运行记录中保存路径、包版本、可执行文件版本和参数,以便识别发生变化的进程输入。

headless 有三种文档定义:true 使用 Chrome 当前的无头模式,'shell' 使用独立的 chrome-headless-shell,false 使用有界面浏览器。它们可能具有不同的进程行为、渲染和界面能力。这些是执行选择,不代表信任、接受或访问权。自有页面报告输出,不能把输出变成服务器决定。

userDataDir 是持久浏览器数据的进程级目录。它不是 BotBrowser 的 .enc 配置文件,也不是 BrowserContext。并发进程必须使用不同的可写目录。复用目录可能保留 Cookie 和本地存储,也会让两次运行共享状态,因此必须明确记录复用。

BotBrowser 的 Puppeteer 设置文档 说明 puppeteer-core、Chrome 模式、executablePath、defaultViewport: null 和 --bot-profile。配置参考 说明配置值;两份文档均不承诺第三方判断或目标响应。

Puppeteer 从进程启动到页面观察和差异记录的选项作用域

页面与上下文作用域

Puppeteer 通过页面选项应用默认 800 × 600 视口。defaultViewport: null 取消这一模拟默认值,并不证明操作系统屏幕大小、窗口管理器状态或物理显示器尺寸。应记录 window.innerWidth、window.innerHeight、screen.width 和 screen.height,而不是从选项名称推断。

BrowserContext 隔离同一进程内不同上下文的 Cookie 和存储等浏览器会话数据。它不选择新的可执行文件、不创造公网 IP、不授予采集权限,也不确立新身份。上下文标识与存储策略属于进程输入记录,不是结果中的访问证明。

只读布尔值 navigator.webdriver 是一个页面可见观察值。MDN 说明了受自动化控制的 Chrome 暴露它的条件。true 或 false 可以帮助解释自有页面差异。检测方还可使用其他信号,服务器也可使用页面拿不到的数据。Puppeteer 与 Playwright 补丁边界分析 区分 JavaScript 属性的变化与补丁无法控制的浏览器行为。

安装配置与运行选项必须分开。Puppeteer 配置文件控制下载和缓存目录;puppeteer-core 忽略这些文件。缓存变化可能改变安装的二进制,但不等于页面视口或存储观察。应记录解析后的可执行文件和包版本。

运行记录字段

启动前建立记录,保存运行标识、包版本、解析后的可执行文件、版本字符串、不含秘密内容的配置参数名称、无头值、视口输入、页面版本和自有来源。这些字段决定两份 JSON 是否可比较。不要把 Cookie、访问令牌、配置文件字节或含认证信息的查询字符串放进记录。

选项与观察值对照表

输入作用域应记录的观察与诊断不能证明什么
executablePath浏览器进程版本与支持的 API请求的二进制已加载或服务器接受
headless浏览器进程视口、渲染、选定属性信任、授权或检测结论
defaultViewport页面模拟innerWidth、innerHeight、屏幕值物理显示器尺寸
userDataDir持久状态运行可访问的 Cookie 与存储配置文件、公网 IP 或账户所有权
BrowserContext会话存储上下文本地 Cookie 与存储隔离新二进制、网络身份或权限
navigator.webdriver页面字段页面暴露的只读布尔值完整指纹或服务器决定

自有页面验收清单

先验证来源:必须是项目拥有的 HTTP 或 HTTPS 来源,以 / 结尾,无凭证、查询或片段。导航后的列表路由必须仍在该来源。每行必须恰好有一个名称元素和一个价格元素,价格符合约定的小数格式。拒绝重复行、跨来源链接、缺失币种和意外路径。

可复现性与清理

每次独立比较使用新的 RUN_USER_DATA_DIR,除非持久状态正是研究变量。保持配置输入不变,只复制允许的页面数据。限制导航超时,记录超时在响应之前还是之后发生。超时是传输或就绪状态观察,不证明目标拒绝浏览器。

输入清单

在记录旁保存清单:Puppeteer 包版本、锁文件摘要、可执行文件版本、启动模式、视口输入、上下文策略、页面版本和开始时间。路径不足以确定输入:可执行文件可能原地替换,目录可能保留旧状态。清单固定比较输入,不复制配置内容或账户数据。

选项保持类型:headless 为 true、false 或 'shell';defaultViewport 为 null 或包含数值宽高的对象;userDataDir 用脱敏运行标识代替私人绝对路径。环境变量输入需记录解析值与变量名。未记录 Puppeteer 版本前,不能把省略选项与显式默认值视作等价。

导航与响应记录

保存请求 URL、最终 URL、重定向次数、响应状态,以及等待 domcontentloaded 的耗时。它们回答不同问题:同来源最终页仍可能是应用错误页,200 仍可能不符合目录结构。首次响应前超时与脚本加载期间超时不同。分开记录,避免将传输、导航和数据失败压成同一个标签。

页面应暴露确定的就绪信号。固定两行目录比读取期间不断增长的列表更容易比较。若页面有加载标记,先等待标记,再验证行数。网络安静不证明应用已完成:分析请求、轮询和 Service Worker 可能在行已就绪后继续,错误页也可能立即安静。

数据校验记录

输出前逐行验证:恰好一个名称、一个价格和一个三字母币种。若调试需要原始文字,只保存在受控的本地诊断中;共享记录可保存脱敏摘要和校验结果。两条通过记录与零条记录不同,也都不能说明其他路由或服务会接受同一浏览器。

字段失败应明确规则,如 price-format、currency-code、duplicate-item 和 missing-name,而不是只写 schema=false。同时保存选择器版本和页面摘要,因为即使浏览器输入完全未变,标记结构变化仍可造成新失败。

差异处理

两份记录均存在后再生成归一化差异。按页面键排序条目标识,统一 URL 尾部斜杠,尺寸按数字比较而非字符串。未观察字段单列。以下是假设的记录形状,不是实测输出:

{
  "changedOption": "defaultViewport",
  "before": { "innerWidth": 800, "innerHeight": 600 },
  "after": { "innerWidth": 1280, "innerHeight": 720 },
  "same": ["origin", "itemCount", "currency"],
  "changed": ["innerWidth", "innerHeight"],
  "notObserved": ["screenX"],
  "interpretation": "The fixture read a different emulated viewport."
}

解释应只点名观察及其范围,不添加关于个人、检测方、配置所有者或目标政策的结论。可执行文件或页面版本不同时,先标记 not-comparable,再看页面字段。两个输入同时变化是新的实验,不是原比较的延续。

持久状态比较

研究持久状态时,只改变这个变量。A 使用新目录,关闭浏览器后 B 复用该目录,记录自有页面报告的 Cookie 或存储键。记录中排除 Cookie 值或令牌。第三次使用新目录可检查观察值是否依赖持久化,但仍不能证明账户所有权或授权。

上下文隔离是另一个比较。在同一进程创建两个上下文,加载同一自有来源,记录各自能否读取页面标记。随后关闭两个上下文;若研究进程复用,再用新进程重复。结果只说明页面存储行为,不说明二进制、网络路径或服务器决定发生改变。

版本与回退

比较期间固定 Puppeteer 包和可执行文件。任一版本变化,即使启动选项相同,也应开始新的一对记录。浏览器更新可能改变渲染、默认行为或暴露字段。保留之前的归一化差异,在清单中记录采用新版本的理由。

回退应恢复完整输入:包锁、可执行文件、配置引用、启动参数、视口设置、上下文策略和页面版本。仅恢复 headless 或 defaultViewport 不能重现不同二进制或存储目录下的记录。页面版本与包锁保存在版本控制中,配置引用与秘密内容分开。

有边界的单变量差异示例

以下页面必须属于自己。它在 /catalog 暴露两行 data-testid="item",每行有唯一 data-id、一个名称、一个价格和币种。两行完成后,目录才在 data-testid="catalog" 上设置 data-ready="true"。代码是示例,不是浏览器运行记录;响应、最终 URL、就绪标记和行结构均通过后才写记录。

import puppeteer from 'puppeteer-core';
import fs from 'node:fs/promises';

const origin = new URL(process.env.OWNED_CATALOG_ORIGIN ?? '');
const executablePath = process.env.BOTBROWSER_EXEC_PATH;
const profilePath = process.env.BOT_PROFILE_PATH;
const mode = process.env.HEADLESS_MODE ?? 'true';
const viewportMode = process.env.DEFAULT_VIEWPORT ?? '800x600';
if (
  !['http:', 'https:'].includes(origin.protocol) ||
  origin.username ||
  origin.password ||
  origin.pathname !== '/' ||
  origin.search ||
  origin.hash
)
  throw new Error('OWNED_CATALOG_ORIGIN must be a plain HTTP(S) origin');
if (!['true', 'false'].includes(mode)) throw new Error('HEADLESS_MODE must be true or false');
if (!['null', '800x600'].includes(viewportMode)) throw new Error('invalid DEFAULT_VIEWPORT');
if (!executablePath || !profilePath || !process.env.RUN_USER_DATA_DIR)
  throw new Error('browser inputs and a dedicated RUN_USER_DATA_DIR are required');
const viewport = viewportMode === 'null' ? null : { width: 800, height: 600 };
const headless = mode === 'true';
const browser = await puppeteer.launch({
  browser: 'chrome',
  executablePath,
  headless,
  defaultViewport: viewport,
  args: [`--bot-profile=${profilePath}`],
  userDataDir: process.env.RUN_USER_DATA_DIR,
});
const diagnostics = [];
try {
  const context = await browser.createBrowserContext();
  const page = await context.newPage();
  const assertRoute = () => {
    const url = new URL(page.url());
    if (url.origin !== origin.origin || url.pathname !== '/catalog' || url.search || url.hash)
      throw new Error('unexpected catalogue URL');
  };
  const started = Date.now();
  const response = await page.goto(new URL('catalog', origin).href, {
    waitUntil: 'domcontentloaded',
    timeout: 15000,
  });
  assertRoute();
  if (!response || !response.ok()) throw new Error(`HTTP response rejected: ${response?.status()}`);
  const navigationMs = Date.now() - started;
  await page.waitForSelector('[data-testid="catalog"][data-ready="true"]', { timeout: 5000 });
  const items = await page.$$eval('[data-testid="item"]', nodes =>
    nodes.map(node => {
      if (
        node.querySelectorAll('[data-testid="item-name"]').length !== 1 ||
        node.querySelectorAll('[data-testid="item-price"]').length !== 1
      )
        throw new Error('name or price cardinality rejected');
      return {
        id: node.getAttribute('data-id'),
        name: node.querySelector('[data-testid="item-name"]')?.textContent?.trim(),
        price: node.querySelector('[data-testid="item-price"]')?.textContent?.trim(),
        currency: node.querySelector('[data-testid="item-price"]')?.getAttribute('data-currency'),
      };
    })
  );
  if (
    items.length !== 2 ||
    new Set(items.map(item => item.id)).size !== 2 ||
    items.some(
      item =>
        !item.id || !item.name || !/^\d+\.\d{2}$/.test(item.price ?? '') || !/^[A-Z]{3}$/.test(item.currency ?? '')
    )
  )
    throw new Error('fixture row schema rejected');
  const observed = await page.evaluate(() => ({
    innerWidth: window.innerWidth,
    innerHeight: window.innerHeight,
    screenWidth: screen.width,
    screenHeight: screen.height,
    webdriver: navigator.webdriver,
  }));
  assertRoute();
  diagnostics.push({
    finalUrl: page.url(),
    status: response.status(),
    navigationMs,
    redirectCount: response.request().redirectChain().length,
    itemCount: items.length,
  });
  await fs.writeFile(
    'puppeteer-scrape-record.json',
    JSON.stringify(
      {
        inputs: { headless, defaultViewport: viewport },
        controls: { userDataPolicy: 'fresh-empty-per-run', contextPolicy: 'new-isolated-context' },
        observed,
        items,
        diagnostics,
      },
      null,
      2
    ),
    { flag: 'wx' }
  );
} finally {
  await browser.close();
}

A 与 B 保持自有来源、可执行文件、配置、页面、包锁和选择器版本不变。只改变研究选项 HEADLESS_MODE 或 DEFAULT_VIEWPORT,不能同时改两个。每个进程使用不同且初始为空的 RUN_USER_DATA_DIR。路径不同,但控制策略同为 fresh-empty-per-run,不代表所有原始输入相同。启动前验证该策略。核心示例比较无头或视口值,不比较持久化。

示例的 HEADLESS_MODE 仅接受 true 和 false;独立 headless-shell 也会改变二进制输入。每次从不同输出目录运行,因为 flag: 'wx' 拒绝覆盖已有记录。两份 inputs 给出研究选项值;确认仅一个值变化且两项控制策略一致后,才命名 changedOption。

脚本只写成功记录。每次另存前述输入清单,包、可执行文件、配置、页面和选择器标识不由此脚本推断。失败时另存进程退出码和错误输出,再依照下文分类失败阶段。JSON 缺失或旧运行留下的 JSON 均不证明当前运行成功。启动、导航、就绪等待或行校验失败必须保留为失败运行。

新上下文有意使用隔离存储。即使 userDataDir 指向持久目录,目录页也不会继承默认上下文的 Cookie。持久化比较应始终使用默认上下文,或在每个新上下文中明确建立同样的页面状态。上下文、页面与清理参考 说明上下文所有权;示例关闭浏览器时也关闭页面和上下文,包括创建页面或验证抛出错误的情况。

观察范围与局限

字段都有作用域:视口差异属于页面模拟,userDataDir 属于持久状态,headless 属于进程模式,navigator.webdriver 属于一个浏览器布尔值。它们都不是完整指纹,不能单独识别个人、设备、配置所有者或检测结论。

差异结果分为 same、changed 和 not-observed。缺失字段不能变成 false:API 可能不可用、权限可能拒绝、页面可能未暴露字段。HTTP 状态与通过的业务数据分开。200 可包含空目录或无效目录,有效记录也不证明其他路由会响应。

解释页面字段前,先比较可执行文件、Puppeteer 版本、参数、用户数据目录、上下文创建方式、最终 URL、响应状态、页面版本和选择器摘要,避免把二进制变化、旧 Cookie 或页面变化描述成视口效果。

失败边界

无效 executablePath 是进程启动失败;缺少配置文件是启动输入失败;共享 userDataDir 是状态隔离失败;最终 URL 离开自有来源是授权或导航失败;200 页面零条通过是目录结构失败。诊断保留分类,不对所有失败重复同一命令。

若 puppeteer-core 忽略预期配置文件,检查显式参数与解析后的二进制。若 defaultViewport: null 尺寸异常,记录四个尺寸及显示环境,不推断物理屏幕。navigator.webdriver 不同时记录模式与参数,不修改属性。

常见问题

无头模式改变什么?

它选择浏览器执行模式。页面可见部分后果,服务器还可使用其他信号。记录描述差异,不承诺检测或站点结果。

userDataDir 等于配置文件吗?

不等于。它是持久浏览器数据目录。配置启动输入与用户数据目录具有不同生命周期和所有权,两者均需明确记录。

新 BrowserContext 会创建新身份吗?

不会。它隔离同一浏览器中的会话数据,不授予授权、不改变公网 IP,也不确立会话归属。

为什么记录 navigator.webdriver?

它是能解释页面差异的具体可见字段,不是结论。诊断中不应删除或改写它。

来源

实践结论

Puppeteer 抓取比较应只改变一个启动或页面选项,固定自有页面、可执行文件、配置、上下文策略、包版本和选择器版本。保存准确观察和诊断,按作用域分类失败,未知值保持未知。差异说明页面读到什么,不证明模式无法检测、配置代表身份或目标必须接受请求。

延伸阅读:Puppeteer 上下文、页面与清理和 Puppeteer 与 Playwright 补丁边界分析。

#Puppeteer#网页抓取#Browser Automation#浏览器信号

本文属于指南: 浏览器自动化入门:首次运行地图

在 BotBrowser 里跑一遍同样的检查

加载 profile,让刚读到的这些信号在浏览器内部保持一致,并在正式上线前先在自己的机器上确认结果。