跳到正文

框架与 Playground

把特定框架连接到托管浏览器的注意事项,以及仪表盘中的 Playground:可以一边看实时画面,一边试跑脚本。

框架

任何通过 CDP 连接 Chrome 的工具都可以使用 connectUrl。它是一次性的,因此会自行重连的框架每次连接都需要一个新会话。浏览器在你连接时启动,通常只需几秒(见上文关于超时的说明);无需事先轮询任何状态。

javascript
// Patchright (optional; a Playwright fork): npm i patchright
import { chromium } from "patchright";
const browser = await chromium.connectOverCDP(connectUrl, { timeout: 120_000 });
const page = browser.contexts()[0].pages()[0];
python
# Browser Use
from browser_use import Agent, Browser
agent = Agent(task="Find the cheapest flight to Lisbon next Friday", llm=llm, browser=Browser(cdp_url=connect_url))
await agent.run()

# Crawl4AI
from crawl4ai import AsyncWebCrawler, BrowserConfig
config = BrowserConfig(browser_mode="custom", cdp_url=connect_url, use_managed_browser=True)
async with AsyncWebCrawler(config=config) as crawler:
    result = await crawler.arun("https://example.com")
javascript
// Stagehand v4
import { localBrowser, Stagehand } from "@browserbasehq/stagehand";
const stagehand = await Stagehand.create({ browser: await localBrowser.connect({ cdpUrl: connectUrl }) });

setContent 和 exposeFunction

引擎从不向客户端发送控制台事件,因为查找自动化客户端的页面测量的正是这种转发。有两个驱动调用依赖这些事件:

  • Playwright 的 page.setContent() 会等待一个始终不会到来的控制台事件,因此会超时(Puppeteer 的可以正常使用)。请改为以 data: URL 的形式打开 HTML,或者像下面的示例那样自己把它写入页面。
  • 在 Playwright 和 Puppeteer 中,exposeFunction() 和 exposeBinding() 在调用时已打开的页面中生效,该页面一旦发生导航就会失效。请在最后一次导航之后再调用它们,或者改为通过 evaluate 把结果传回。
javascript
// Instead of await page.setContent(html):

// Short HTML: a data: URL (the page gets no origin of its own, so no cookies or storage)
await page.goto("data:text/html," + encodeURIComponent(html));

// Any HTML: write it into a page you have navigated to; it keeps that page's origin
await page.evaluate((html) => {
  document.open();
  document.write(html);
  document.close();
}, html);
python
# Instead of page.set_content(html):
from urllib.parse import quote

page.goto("data:text/html," + quote(html))
# or
page.evaluate("html => { document.open(); document.write(html); document.close(); }", html)

一个可以直接上手的辅助函数

一个函数搞定:按上文的重试规则创建会话、连接,并始终停止会话:

typescript
// clearcote-hosted.ts
import { chromium, type Browser } from "playwright"; // or "patchright"

const API = "https://www.clearcotelabs.com/api/v1/browsers";
const AUTH = { authorization: "Bearer " + process.env.CLEARCOTE_API_KEY };
const RETRY_CODES = new Set(["NO_CAPACITY", "NO_WORKER"]);

export async function createSession(options: Record<string, unknown> = {}, attempts = 5) {
  for (let i = 0; ; i++) {
    const res = await fetch(API, {
      method: "POST",
      headers: { ...AUTH, "content-type": "application/json" },
      body: JSON.stringify(options),
    });
    const body = await res.json().catch(() => ({}));
    if (res.ok) return body as { id: string; connectUrl: string; worker: string };
    const retry = RETRY_CODES.has(body.code) || (res.status === 429 && !body.code) || [500, 502, 504].includes(res.status);
    if (!retry || i + 1 >= attempts) throw new Error([res.status, body.code, body.error].filter(Boolean).join(" "));
    await new Promise((r) => setTimeout(r, Math.min(15_000, 1000 * 2 ** i) * (0.5 + Math.random())));
  }
}

export async function withBrowser<T>(options: Record<string, unknown>, work: (browser: Browser) => Promise<T>) {
  const session = await createSession(options);
  try {
    const browser = await chromium.connectOverCDP(session.connectUrl, { timeout: 120_000 });
    try {
      return await work(browser);
    } finally {
      await browser.close().catch(() => {});
    }
  } finally {
    // Ends the session if closing the browser did not (a no-op otherwise).
    await fetch(API + "/" + session.id, { method: "DELETE", headers: AUTH }).catch(() => {});
  }
}

// await withBrowser({ profile: { name: "shop", persist: true }, country: "de" }, async (browser) => {
//   const page = browser.contexts()[0].pages()[0];
//   await page.goto("https://example.com");
// });

上传文件

需要文件的页面——<input type="file">、“浏览”按钮、拖放区域——在托管浏览器中接收文件的方式与在本地浏览器中完全相同。文件在你自己的机器上读取,其字节通过连接传输,因此浏览器看不到你磁盘上的路径,你也可以上传一个只存在于脚本运行所在机器上的文件。每个文件的上限为 50 MB。

Playwright 最为简单。setInputFiles 无需点击,直接把文件放进 input,即使 input 被隐藏也有效——许多网站会把真正的 input 藏在一个带样式的按钮后面,所以请直接针对 input 本身,而不是去点击按钮。

javascript
// Node.js + Playwright
await page.setInputFiles("input[type=file]", "C:/Users/me/invoice.pdf");

// or build the file in your script, with nothing on disk:
await page.setInputFiles("input[type=file]", {
  name: "invoice.pdf",
  mimeType: "application/pdf",
  buffer: Buffer.from("%PDF-1.4 …"),
});
python
# Python + Playwright
page.set_input_files("input[type=file]", "/home/me/invoice.pdf")

当某个按钮会打开系统文件对话框,而又没有可供你定位的 input 时,请改为捕获文件选择器:

javascript
const [chooser] = await Promise.all([
  page.waitForEvent("filechooser"),
  page.click("text=Browse"),           // the page's own button
]);
await chooser.setFiles("C:/Users/me/invoice.pdf");

Puppeteer。elementHandle.uploadFile() 和 fileChooser.accept() 在托管浏览器上不可用:它们指定的是一个供服务器读取的路径,而该路径可能指向服务器上的任意位置,因此网关会拒绝它们。请在页面内用你传入的字节构建文件,再把它交给 input:

javascript
// Puppeteer: construct the file in the page
const b64 = fs.readFileSync("invoice.pdf").toString("base64");
await page.evaluate(({ b64, name, type }) => {
  const data = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
  const dt = new DataTransfer();
  dt.items.add(new File([data], name, { type }));
  const input = document.querySelector("input[type=file]");
  input.files = dt.files;
  input.dispatchEvent(new Event("change", { bubbles: true }));
}, { b64, name: "invoice.pdf", type: "application/pdf" });

同一个 DataTransfer 也能驱动拖放区域:在其上派发带有该对象的 dragenter、dragover 和 drop 事件。使用本地路径的真实 CDP 拖动(即带有文件的 Input.dispatchDragEvent)会因与 uploadFile() 相同的原因被拒绝。

Playground

仪表盘中的 Playground 会在一个这样的托管浏览器上运行脚本,旁边同时显示实时画面、控制台和截图;下方的“Use in your code”面板则给出与上文代码相同的会话选项。它的 page 是一个直接通过 CDP 通信的小型辅助对象,并不是 Playwright,因此 Playground 脚本只是一份需要你转写的草稿,而不是可以直接粘贴使用的文件:

辅助函数作用
page.goto(url, { timeout? })导航并等待 load 事件。
page.click(sel) · page.type(sel, text) · page.press(key)真实的鼠标和键盘输入,会先将元素滚动到可见区域。
page.evaluate(fn, ...args)在页面中运行一个函数,并取回其 JSON 结果。
page.waitForSelector(sel, { timeout? }) · page.waitForNavigation()等待某个元素出现,或等待下一次页面加载。
page.scroll(px) · page.screenshot({ fullPage? })用鼠标滚轮滚动;截取一张 JPEG,显示在“Screenshots”标签页中。
page.title() · page.url() · page.content()文档标题、地址和 HTML。
log(...values) · sleep(ms)输出到控制台(对象会格式化打印);暂停。
cdp(method, params)向浏览器发送一条原始 CDP 命令(Target.*、Browser.*、Storage.*)。
page.cdp(method, params)向页面发送一条原始 CDP 命令(Page.*、Runtime.*、DOM.*、Network.*)。

报错时会指出出错的脚本行。“Share”会复制一个链接,脚本及其会话选项都放在链接的 URL 中,因此我们这边不存储任何内容;打开链接的人会用自己的余额运行它。