本文へスキップ

フレームワークと Playground

特定のフレームワークをホスト型ブラウザにつなぐ際の注意点と、ライブビューの横でスクリプトを試せるダッシュボードの Playground を紹介します。

フレームワーク

CDP 経由で Chrome に接続するものであれば、何でも connectUrl で動作します。URL は 1 回限りなので、自動で再接続するフレームワークでは接続ごとに新しいセッションが必要です。ブラウザは接続時に起動し、通常は数秒以内に立ち上がります(上記のタイムアウトについての注記を参照)。事前にステータスをポーリングする必要はありません。

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

エンジンはコンソールイベントをクライアントに一切送信しません。自動化クライアントの有無を調べるページが計測しているのは、まさにその転送だからです。次の 2 つのドライバー呼び出しは、これらのイベントに依存しています。

  • Playwright の page.setContent() は届くことのないコンソールイベントを待ち続けるため、タイムアウトします(Puppeteer では動作します)。代わりに HTML を data: URL として開くか、下の例のように自分でページに書き込んでください。
  • 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)

たたき台にできるヘルパー

上記のリトライ付きの作成、接続、確実な停止を 1 つの関数にまとめたものです。

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"> や “browse” ボタン、ドラッグ&ドロップ領域など)には、ローカルのブラウザと同じ方法で、ホスト型ブラウザでもファイルを渡せます。ファイルはお使いのマシン上で読み込まれ、そのバイト列が接続を通じて送られます。そのため、ブラウザがディスク上のパスを見ることはなく、スクリプトが動作している場所にしか存在しないファイルもアップロードできます。上限は 1 ファイルあたり 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 がない場合は、代わりに file chooser を受け取ります。

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 では、これらのブラウザの 1 つでスクリプトを実行し、その横にライブビュー、コンソール、スクリーンショットを表示できます。その下の「Use in your code」パネルには、上記のコードと同じセッションオプションが表示されます。Playground の page は Playwright ではなく、CDP を直接扱う小さなヘルパーです。そのため、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 に含めたリンクをコピーするため、当社側には何も保存されません。リンクを開いた人は、自分の残高でそのスクリプトを実行します。