框架与 Playground
把特定框架连接到托管浏览器的注意事项,以及仪表盘中的 Playground:可以一边看实时画面,一边试跑脚本。
框架
任何通过 CDP 连接 Chrome 的工具都可以使用 connectUrl。它是一次性的,因此会自行重连的框架每次连接都需要一个新会话。浏览器在你连接时启动,通常只需几秒(见上文关于超时的说明);无需事先轮询任何状态。
// 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];# 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")// 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把结果传回。
// 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);# 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)一个可以直接上手的辅助函数
一个函数搞定:按上文的重试规则创建会话、连接,并始终停止会话:
// 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 本身,而不是去点击按钮。
// 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 + Playwright
page.set_input_files("input[type=file]", "/home/me/invoice.pdf")当某个按钮会打开系统文件对话框,而又没有可供你定位的 input 时,请改为捕获文件选择器:
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:
// 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 中,因此我们这边不存储任何内容;打开链接的人会用自己的余额运行它。