Cloudflare Browser Rendering で AI ベースの Web スクレイピング

はじめに

Cloudflare が提供する Browser Rendering API を試してみました。

この API は、Cloudflare のグローバルネットワーク上でヘッドレス Chrome を実行できるサービスです。特に興味深いのが /json エンドポイントで、AI を使って Web ページから構造化データを JSON 形式で抽出できます。

この記事では、実際に API を使ってスクレイピングを実装した過程と、遭遇した課題について紹介します。

Cloudflare Browser Rendering とは

Cloudflare Browser Rendering は、Cloudflare のグローバルネットワーク上でヘッドレス Chrome を実行できるサービスです。

主な特徴は以下の通りです。

数千のブラウザへの即時アクセスが可能で、エッジネットワーク配置により低遅延を実現しています。コールドスタート時間が短く、素早くブラウザを起動できます。

実装方式として REST API と Workers Bindings の 2 つが用意されており、シンプルで状態を持たないタスクには REST API が適しています。

利用可能なエンドポイントには、JSON エンドポイント(Web ページデータを JSON 形式に解析・構造化)、スクリーンショット、PDF 生成、HTML コンテンツ抽出などがあります。

特に /json エンドポイントが便利で、AI を使って Web ページから構造化データを抽出できます。プロンプトと JSON スキーマを指定するだけで、ページから必要な情報を取得できます。CSS セレクタを書く必要がありません。

実装方法

Browser Rendering API の実装は、思ったよりシンプルでした。

API クライアントの実装

まず、Browser Rendering API を呼び出すクライアントを実装します。

export async function extractWithAI<T>(
  url: string,
  prompt: string,
  schema: Record<string, unknown>,
  env: Env
): Promise<T> {
  const endpoint = `https://api.cloudflare.com/client/v4/accounts/${env.CLOUDFLARE_ACCOUNT_ID}/browser-rendering/json`;

  const payload = {
    url,
    prompt,
    response_format: {
      type: 'json_schema',
      schema,
    },
  };

  const response = await fetch(endpoint, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${env.CLOUDFLARE_API_TOKEN}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(payload),
  });

  const data = await response.json();
  return data.result;
}

Account ID と API トークンは環境変数で管理します。

汎用パーサーの実装

次に、どんなサイトでも使える汎用パーサーを実装しました。

export async function parseWithAI(
  url: string,
  name: string,
  sinceDate: string,
  env: Env
): Promise<Article[]> {
  const prompt = `
You are extracting articles from a blog or news website.

Instructions:
1. Find all articles, blog posts, or news items on this page
2. For each article, extract:
   - Title: The main heading or title of the article
   - URL: The link to the full article
   - Date: The publication date in YYYY-MM-DD format

3. Date handling (CRITICAL):
   - If the date is visible on the list page, use it
   - If NOT visible, navigate to the article detail page and extract the date
   - Convert all dates to YYYY-MM-DD format

4. Filtering:
   - Only include articles published on or after ${sinceDate}

Return the results as a structured JSON array.
`;

  const schema = {
    type: 'object',
    properties: {
      articles: {
        type: 'array',
        items: {
          type: 'object',
          properties: {
            title: { type: 'string' },
            url: { type: 'string' },
            date: { type: 'string' },
          },
          required: ['title', 'url', 'date'],
        },
      },
    },
    required: ['articles'],
  };

  const result = await extractWithAI<{ articles: Article[] }>(
    url,
    prompt,
    schema,
    env
  );

  return result.articles.map(article => ({
    ...article,
    source: name,
  }));
}

ポイントは、プロンプトで日付の取得方法を明確に指示していることです。一覧ページに日付が表示されていない場合は、AI が自動的に記事詳細ページに遷移して日付を取得してくれます。

動作確認

API が正しく動作するか確認するため、テストスクリプトを作成しました。

#!/bin/bash

CLOUDFLARE_ACCOUNT_ID="your-account-id"
CLOUDFLARE_API_TOKEN="your-api-token"
TEST_URL="https://kusumoto.app/blog"

ENDPOINT="https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/browser-rendering/json"

REQUEST_BODY=$(cat <<EOF
{
  "url": "$TEST_URL",
  "prompt": "このページから最新の記事を抽出してください。各記事のタイトル、URL、日付を取得してください。",
  "response_format": {
    "type": "json_schema",
    "schema": {
      "type": "object",
      "properties": {
        "articles": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "title": { "type": "string" },
              "url": { "type": "string" },
              "date": { "type": "string" }
            },
            "required": ["title", "url"]
          }
        }
      },
      "required": ["articles"]
    }
  }
}
EOF
)

curl -X POST "$ENDPOINT" \
  -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d "$REQUEST_BODY" | jq .

テストを実行したところ、見事に 9 件の記事が抽出されました。

{
  "success": true,
  "result": {
    "articles": [
      {
        "title": "iOS アプリ開発 〜 Mac のカメラを使ってシミュレータでカメラの動作確認",
        "url": "https://kusumoto.app/blog/rocketsim-simulator-camera",
        "date": "2025-11-07"
      },
      ...
    ]
  }
}

API 単体では問題なく動作しています。

遭遇した課題

実装自体はスムーズでしたが、実際に運用環境で動かすと問題が発生しました。

Worker リソース制限

Cloudflare Workers には CPU 時間とメモリの制限があります。Browser Rendering はヘッドレスブラウザを起動するため、かなりのリソースを消費します。

複数のサイトを並列処理する場合、この並列処理が Worker のリソース制限を超えてしまう可能性があります。実際に試したところ、Browser Rendering の処理中に Worker がリソース枯渇で強制終了されるケースがありました。

Browser Rendering の利点

課題はありましたが、Browser Rendering API 自体は非常に優れています。

サイト構造に依存しない柔軟性があります。HTML の変更に強く、プロンプトの調整で対応できます。

新しいサイトの追加が簡単です。CSS セレクタを調べる必要がなく、URL とプロンプトを設定するだけです。

JSON スキーマによる型安全性も魅力です。レスポンスの形式を厳密に指定できます。

エッジ実行による高速性もメリットです。コールドスタートがなく、グローバルに分散配置されています。

まとめ

Cloudflare Browser Rendering API を使った AI ベースのスクレイピングを試してみました。API 自体は非常に魅力的で、単体テストでは 9 件の記事を正確に抽出できました。

プロンプトと JSON スキーマだけで構造化データを取得できる柔軟性は大きな利点です。サイト構造に依存せず、プロンプトの調整だけで対応できる点も便利でした。

ただし、Cloudflare Workers のリソース制限により、複数サイトの並列処理では課題が残りました。単一サイトや少数サイトの処理であれば、十分に実用的だと感じています。

参考リンク