返回知识中心
入门

Playwright 抓取:浏览器与配置文件设置

配置经授权的 Playwright 列表到详情采集,区分浏览器配置、Cookie 与提取错误,并保存可诊断的 JSON 结果。

BotBrowser Team

文档中心

想直接进入 入门 文档吗?

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

用 Playwright 抓取数据,需要分别确认浏览器是否处于预期环境、提取结果是否符合业务含义。chromium.launch() 成功,既不能证明价格字段正确,也不代表获得采集权限。BotBrowser 支持通过 Playwright 的 executablePath 和 args 启动配置文件驱动的 Chromium;它不会提供目标站点的选择器、授予访问权限,也不保证服务器返回页面。面对自有或获授权的应用,一次范围明确的“列表到详情”采集可以把浏览器设置和数据错误分开诊断。

要点

  • 明确设置 executablePath,把 --bot-profile 作为单个参数传入。BotBrowser 配置文件不同于 Playwright 管理的 Cookie 和本地存储。
  • 同一授权会话的列表与详情使用同一个 BrowserContext。新上下文隔离存储,但不是新的网络身份。
  • 接受记录前检查导航状态、最终源与必需选择器。HTTP 200 也可能只是登录页。
  • 数据记录与诊断分开保存。认证状态文件属于凭据,不是可以随意分享的调试附件。

目录

自有目录页的采集需要分别校验 HTTP 状态和行结构;Puppeteer 抓取选项比较将启动输入与页面观察分开,并明确新隔离上下文的存储策略。

采集前配置浏览器

Playwright BrowserType 文档用 executablePath 指定不同于随框架附带的浏览器可执行文件,也明确提醒自定义浏览器与安装的 Playwright 版本不保证兼容。用锁文件固定 playwright-core,记录真实浏览器版本,任一组件更新后重新检查兼容性。下载 Playwright 自带的 Chromium,不会更新配置中指向的自定义文件。

浏览器和配置文件都使用绝对路径。在容器中,这些路径必须存在于容器内部,而不是只存在于创建任务的宿主机。启动前分别检查二进制执行权限和配置文件读取权限。缺少环境变量应立即报配置错误,不应悄悄采用意外的默认设置。直接接入 Playwright 的启动说明涵盖安装;采集任务还需要保留足够的配置记录,才能解释后续数据异常。

BotBrowser 配置文件参考要求浏览器与配置包的大版本一致。CLI 选项覆盖配置中的值,并不是叠加另一套独立身份。不要因为通用采集示例包含某个 userAgent、时区、语言列表或设备描述,就把它与现有配置随意组合。应用确实要求指定 viewport 时,应记录这项覆盖;否则 viewport: null 可以退出 Playwright 的固定视口模拟,但不会证明某个屏幕尺寸,也不会让所有页面布局相同。

访问授权、兼容的执行环境和提取模式仍然是三项独立前提。示例只针对自有商品目录:一个列表页、最多两个详情页,而不是公共电商平台。这样的边界允许核对选择器契约,不会形成无限爬取任务,也不包含对任何第三方防护服务的兼容承诺。

在独立示例项目中安装 playwright-core,将程序保存为 collect.mjs,由任务配置提供 BOTBROWSER_EXEC_PATH、BOT_PROFILE_PATH 和 OWNED_CATALOG_ORIGIN。目标源必须使用 HTTP 或 HTTPS,并属于受控示例。代码基于公开 API 展示实现方法,不是已运行 BotBrowser 的实测记录。

受控示例保留 Playwright 默认的自动化仪表机制,不测试、也不声称任何自动化信号结果。

提取列表及其详情页

受控示例的 /catalog 页面包含 data-testid="item-link" 链接。每个详情页恰好包含一个 data-testid="item-name" 和一个 data-testid="item-price" 元素,后者带有 data-currency。例如列表链接指向 /items/a1,详情价格为 <span data-testid="item-price" data-currency="USD">12.50</span>。这些值是受控示例输入,不是观察输出;其他 HTML 结构需要不同的选择器契约。

import { chromium } from 'playwright-core';
import { access, writeFile } from 'node:fs/promises';
import { constants } from 'node:fs';
import { isAbsolute } from 'node:path';

const executablePath = process.env.BOTBROWSER_EXEC_PATH;
const profilePath = process.env.BOT_PROFILE_PATH;
const origin = new URL(process.env.OWNED_CATALOG_ORIGIN);
if (!['http:', 'https:'].includes(origin.protocol)) {
  throw new Error('Fixture origin must use HTTP or HTTPS');
}
if (origin.username || origin.password || origin.search || origin.hash || origin.pathname !== '/') {
  throw new Error('Supply an origin without credentials, path, query or fragment');
}
const safeUrl = value => {
  const url = new URL(value);
  return url.origin + url.pathname;
};
for (const value of [executablePath, profilePath]) {
  if (!value || !isAbsolute(value)) throw new Error('Absolute paths required');
}
await access(executablePath, constants.X_OK);
await access(profilePath, constants.R_OK);

const browser = await chromium.launch({
  executablePath,
  headless: true,
  args: [`--bot-profile=${profilePath}`],
});
const records = [];
const diagnostics = [];
try {
  const context = await browser.newContext({ viewport: null });
  context.setDefaultTimeout(10000);
  const page = await context.newPage();
  async function open(url) {
    const response = await page.goto(url, {
      waitUntil: 'domcontentloaded',
      timeout: 30000,
    });
    const status = response?.status() ?? null;
    const finalUrl = new URL(page.url());
    diagnostics.push({ url: safeUrl(url), finalUrl: safeUrl(finalUrl), status });
    if (status === null || status < 200 || status >= 300) {
      throw new Error(`Unaccepted navigation status: ${status}`);
    }
    if (finalUrl.origin !== origin.origin) {
      throw new Error('Navigation left the fixture origin');
    }
  }
  await open(new URL('/catalog', origin).href);
  const links = page.getByTestId('item-link');
  await page.waitForFunction(() => document.querySelectorAll('[data-testid="item-link"]').length === 2);
  const hrefs = await links.evaluateAll(nodes => nodes.map(node => node.getAttribute('href')));
  const urls = [...new Set(hrefs.filter(Boolean).map(href => new URL(href, page.url()).href))].filter(
    url => new URL(url).origin === origin.origin
  );
  if (urls.length !== 2) throw new Error('Fixture requires two same-origin detail links');
  for (const url of urls) {
    await open(url);
    const name = page.getByTestId('item-name');
    const price = page.getByTestId('item-price');
    await name.waitFor({ state: 'visible' });
    await price.waitFor({ state: 'visible' });
    if ((await name.count()) !== 1 || (await price.count()) !== 1) {
      throw new Error('Detail selector is not unique');
    }
    const label = (await name.innerText()).trim();
    const amount = (await price.innerText()).trim();
    const currency = await price.getAttribute('data-currency');
    if (!label || !/^\d+\.\d{2}$/.test(amount) || !/^[A-Z]{3}$/.test(currency ?? '')) {
      throw new Error('Detail record does not match the fixture schema');
    }
    if (new URL(page.url()).origin !== origin.origin) {
      throw new Error('Detail origin changed during extraction');
    }
    records.push({ url: safeUrl(page.url()), name: label, amount, currency });
  }
  await writeFile(
    'catalog-result.json',
    JSON.stringify(
      {
        browserVersion: browser.version(),
        collectedAt: new Date().toISOString(),
        records,
        diagnostics,
      },
      null,
      2
    ),
    { flag: 'wx', mode: 0o600 }
  );
  await context.close();
} finally {
  await browser.close();
}

Playwright 定位器文档区分定位器与已经保存的 DOM 元素引用。定位器在使用时重新解析元素,有利于应对节点替换,但不知道哪条业务记录才正确。代码因此等待 受控示例 元素、要求唯一、检查文本格式,之后才追加记录。domcontentloaded 只是导航事件,不表示异步商品目录已经加载完成。

受控示例 明确要求两个链接,程序等待该数量,并拒绝少于两个不同同源详情 URL 的结果。真实应用如果逐步追加列表,应由应用团队提供完成标记或稳定的 API 响应。不能假设 evaluateAll() 会等到匹配元素的最终数量;否则程序可能从最终含有二十条记录的列表中,仅取走一条合法记录,又没有超时提示遗漏。

同源过滤限制从列表选中的链接。导航后的检查拒绝重定向后的外源页面,接受记录前的检查再拒绝读取期间发生的源变化,但相应请求已经发出。这不是网络防火墙:页面仍能加载其他主机的图片、脚本和 frame。需要严格出口限制时,还应由基础设施实施,应用代码无法事后阻止已经发出的请求。

金额保留为字符串,因为 受控示例 规定了十进制文本格式。真实价格可能包含小数逗号、千位分隔符、税费说明、订阅周期或多种币种共用的符号。parseFloat() 不能代替业务模式。按来源定义允许的格式,拒绝歧义记录,不要把开头的一段数字静默解释成完整价格。

决定下次运行保留什么

浏览器配置、浏览器存储与任务结果解决不同问题。Playwright 上下文生命周期与存储状态详细说明上下文如何在同一进程中隔离 Cookie 等状态。它不会分配新的公网 IP、创建新账户,也不能证明服务器无法关联请求。两个上下文可以具有相同的配置值,同时拥有独立 Cookie 容器。

组件承载内容不能证明的事情
--bot-profile配置包提供的浏览器设置站点已登录会话
BrowserContext生命周期内隔离的浏览状态新代理地址或数据采集授权
storageState支持保存的 Cookie 与源存储运行中标签页的全部临时状态
userDataDir持久化浏览器数据目录多进程同时写入同一目录是安全的
结果 JSON提取记录与选定诊断信息重现会话所需的凭据

三条不同路径:配置文件设置浏览器,BrowserContext 承载站点存储,提取程序写入 catalog-result.json。复用其中一条路径不会自动恢复另外两类状态。

单次列表到详情采集复用一个上下文与一个页面,让 Cookie 在导航间保留,也便于检查任务范围。每个详情都创建新上下文,可能丢失列表页刚建立的合法会话。反过来,让不同授权账户共用上下文又可能混合状态。应先确定账户和会话边界,再决定页面数量。

跨任务运行时,默认非持久化上下文提供干净的存储边界,关闭后其浏览数据被丢弃。需要恢复授权登录,可以保存并加载 storageState。Playwright 认证文档提醒,状态文件可能含有可冒用账户的凭据。不要提交到版本库,也不要混入常规结果导出;应限定访问权限、保留期限和账户归属。

并非所有存储都默认保存。当前文档通过 indexedDB: true 选择捕获 IndexedDB,需要检查固定版本是否支持。会话存储 需要针对应用单独处理,不能默认包含在普通快照里。恢复的 Cookie 也可能过期或被服务器撤销。状态文件成功解析,不等于认证会话仍然有效。

如果工作负载确实需要完整浏览器目录,launchPersistentContext() 返回一个使用 userDataDir 的上下文。该目录与 --bot-profile 所指的 .enc 文件不同。不要把多个浏览器实例指向同一个可写目录。并行运行的独立使用者应有独立目录,遵循正常锁定机制,不应因锁错误而强行共用。

在结果中区分数据与失败

只有所有选定详情都通过校验,示例才写结果。因此读完第一条后失败,不会伪装成完整的双条目批次。flag: 'wx' 也会拒绝覆盖已有文件。真实调度器应使用唯一运行目录和任务编号,在清单中区分完整成功、部分结果与失败尝试,而不是所有任务共用一个文件名。

时间戳表示采集时间,不是商品最近变更时间。browserVersion 是所连接浏览器返回的版本,不能证明宿主机上可执行文件的身份。网页 JavaScript 可能看到受配置控制的值,单独读取 navigator.userAgent 不能确认启动了哪个文件。可将解析后的可执行路径、锁文件版本和配置摘要存入受限任务元数据,与公开商品目录输出分离。

同时记录请求 URL 与最终 URL,有助于解释重定向。商品链接可能到达同意页、登录页或下架页,并仍然返回 HTTP 200。因此选择器和记录校验本来就是接收条件,不只是防御编程。后续重试流程应区分 HTTP 成功和业务记录有效这两个状态。

网页采集中的浏览器层关注目标可观察的环境,并不决定选择器是否仍然指向价格。版式改动、缺少权限、启动错误都可能产生空结果,但改配置无法修复前两项。将提取错误与浏览器环境证据分字段记录,才能选择对应处理方法。

共享证据默认不应包含完整请求头、Cookie 或未脱敏认证 URL。代码拒绝配置源中的用户名与密码,保存 URL 时去除查询串与片段。但路径仍可能含有秘密,因此 受控示例 只适用于公开商品路径,分享前还要检查字段。脱敏共享副本的同时,可以保留经批准、足以辨认授权任务的受限证据,而不是删除全部原始上下文。

诊断实际失败的层级

观察结果首先检查对应处理
浏览器进程未启动绝对路径、执行权限、依赖修复安装或任务挂载
启动后创建上下文失败固定的框架与浏览器兼容性按有文档依据的版本组合复现
导航返回 401 或 403访问授权和服务器响应停止采集,与所有者解决访问
导航返回 429服务限额及存在时的 Retry-After按服务规则暂停
HTTP 200 但无目标元素最终 URL、登录状态、完成标记先分类页面,再修改选择器
价格不符合格式来源模式及本地化格式拒绝记录,不猜转换方式
下次运行已退出登录上下文策略与状态过期恢复批准的状态或正常登录

故障表只有在任务保留了区分原因的观察时才有用。导航错误需保留 URL 类别、可取得的响应状态和异常类型;选择器错误需记录失败字段与是否到达预期应用路由。捕获所有异常后写空数组,会把被拒绝的请求和真正空目录混成同一结果。

不要盲目重跑整个脚本。超时可能是授权应用较慢,拒绝访问却需要权限决策;错误的小数价格也不会经过十次相同导航就变正确。将可重试传输故障、访问决定与确定性模式错误分开,遵循站点公布的限额及双方采集约定。BotBrowser 不决定某个目标是否适合重试。

示例刻意不包含代理轮换、并发爬取或挑战处理分支。这些功能会改变会话与流量契约,对确认配置浏览器能否提取自有目录并非必要。扩大范围前,应先定义账户归属、允许请求量、完整性和恢复行为。首次得到两条正确记录只是模式校验,不是大规模任务安全可靠的实证。

常见问题

只安装 playwright-core 就能选择 BotBrowser 吗?

只有显式传入目标 executablePath 才行。包提供自动化 API,不会寻找 BotBrowser 安装位置,也不会自动选择 .enc。把配置参数传给其他浏览器,也不会使其变成 BotBrowser。缺少路径应在启动前失败,调查兼容性时保留包版本。

新上下文会使两次采集互不相关吗?

不会。它隔离存储,服务器仍可以观察网络出口、账户、请求模式和浏览器属性。这是应用隔离工具,不是匿名保证。应用它来防止状态混用,而不是把它视为超出站点采集限额的许可。

应等待 networkidle 而不是选择器吗?

网络安静不是所需的业务状态。数据可能已经就绪而统计或轮询仍在继续,也可能网络安静但显示错误页。应使用应用自己的完成信号,再检查必需字段。受限 受控示例 要求链接进入 DOM、详情元素可见且唯一;其他应用需要自己的完成契约。

来源

实践结论

用 Playwright 抓取时,先选择明确的可执行文件与兼容配置,再让授权会话在列表和详情之间共用一个上下文。导航、源和记录模式都验证成功后,才接受结果。Cookie 保存、浏览器配置和商品记录导出是不同的安全与保留决策。

从自有目录、两个详情链接和文档中的选择器模式开始。增加流量前先适配模式,保留明确失败,不要返回看似成功的空数据集。

#Playwright#网页抓取#浏览器上下文#浏览器配置文件#Session Storage

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

在 BotBrowser 里跑一遍同样的检查

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