キューとブラウザレンダリングを使用してウェブクローラーを構築する
キューとブラウザレンダリングを使用してウェブクローラーを動かす方法の例。
このチュートリアルでは、キュー、ブラウザレンダリング、および Puppeteerを使用してウェブクローラーを構築し、デプロイする方法を説明します。
Puppeteerは、Chrome/Chromiumブラウザとのインタラクションを自動化するために使用される高レベルのライブラリです。送信された各ページで、クローラーはcloudflare.comへのリンクの数を見つけ、サイトのスクリーンショットを撮り、結果をWorkers KVに保存します。
Puppeteerを使用して、ページ上のすべての画像をリクエストしたり、サイトで使用されている色を保存したりすることができます。
- Sign up for a Cloudflare account ↗.
- Install
npm↗. - Install
Node.js↗.
Node.js version manager
Use a Node version manager like Volta ↗ or nvm ↗ to avoid permission issues and change Node.js versions. Wrangler, discussed later in this guide, requires a Node version of 16.17.0 or later.
さらに、キューへのアクセスが必要です。
Queues is currently in Public Beta ↗ and is included in the monthly subscription cost of your Workers Paid plan, and charges based on operations against your queues. Refer to Pricing for more details.
Before you can use Queues, you must enable it via the Cloudflare dashboard ↗. You need a Workers Paid plan to enable Queues.
To enable Queues:
- Log in to the Cloudflare dashboard ↗.
- Go to Workers & Pages > Queues.
- Select Enable Queues Beta.
始めるには、create-cloudflare CLI ↗を使用してWorkerアプリケーションを作成します。ターミナルウィンドウを開き、次のコマンドを実行します。
npm create cloudflare@latest -- queues-web-crawleryarn create cloudflare@latest queues-web-crawlerpnpm create cloudflare@latest queues-web-crawlerFor setup, select the following options:
- For What would you like to start with?, choose
Hello Worldの例. - For Which template would you like to use?, choose
Hello World Worker. - For Which language do you want to use?, choose
TypeScript. - For Do you want to use git for version control?, choose
Yes. - For Do you want to deploy your application?, choose
No(we will be making some changes before deploying).
次に、新しく作成したディレクトリに移動します:
cd queues-web-crawlerKVストアを作成する必要があります。これはCloudflareダッシュボードまたはWrangler CLIを通じて行うことができます。このチュートリアルでは、Wrangler CLIを使用します。
npx wrangler kv namespace create crawler_linksnpx wrangler kv namespace create crawler_screenshots🌀 タイトル "web-crawler-crawler-links" の名前空間を作成中✨ 成功!次の内容をkv_namespaces配列の設定ファイルに追加してください:[[kv_namespaces]]binding = "crawler_links"id = "<GENERATED_NAMESPACE_ID>"
🌀 タイトル "web-crawler-crawler-screenshots" の名前空間を作成中✨ 成功!次の内容をkv_namespaces配列の設定ファイルに追加してください:[[kv_namespaces]]binding = "crawler_screenshots"id = "<GENERATED_NAMESPACE_ID>"次に、wrangler.tomlファイルに、ターミナルで生成された値を使用して次の内容を追加します:
kv_namespaces = [ { binding = "CRAWLER_SCREENSHOTS_KV", id = "<GENERATED_NAMESPACE_ID>" }, { binding = "CRAWLER_LINKS_KV", id = "<GENERATED_NAMESPACE_ID>" }]次に、Workerをブラウザレンダリング用に設定する必要があります。
現在のディレクトリで、Cloudflareのpuppeteerのフォークとrobots-parser ↗をインストールします:
npm install @cloudflare/puppeteer --save-devnpm install robots-parser次に、ブラウザレンダリングバインディングを追加します。ブラウザレンダリングバインディングを追加すると、WorkerはPuppeteerで制御するヘッドレスChromiumインスタンスにアクセスできます。
browser = { binding = "CRAWLER_BROWSER" }次に、キューを設定する必要があります。
npx wrangler queues create queues-web-crawlerキュー queues-web-crawler を作成中。キュー queues-web-crawler を作成しました。次に、wrangler.tomlファイルに次の内容を追加します:
[[queues.consumers]]queue = "queues-web-crawler"max_batch_timeout = 60
[[queues.producers]]queue = "queues-web-crawler"binding = "CRAWLER_QUEUE"消費者キューに60秒のmax_batch_timeoutを追加することは重要です。なぜなら、ブラウザレンダリングにはアカウントごとに1分あたり2つの新しいブラウザの制限があるからです。このタイムアウトは、キューのメッセージをバッチにまとめる前に最大1分待機します。これにより、Workerはこのブラウザ呼び出し制限を超えないようになります。
最終的なwrangler.tomlファイルは以下のようになります。
#:schema node_modules/wrangler/config-schema.jsonname = "web-crawler"main = "src/index.ts"compatibility_date = "2024-07-25"compatibility_flags = ["nodejs_compat"]
kv_namespaces = [ { binding = "CRAWLER_SCREENSHOTS_KV", id = "<GENERATED_NAMESPACE_ID>" }, { binding = "CRAWLER_LINKS_KV", id = "<GENERATED_NAMESPACE_ID>" }]
browser = { binding = "CRAWLER_BROWSER" }
[[queues.consumers]]queue = "queues-web-crawler"max_batch_timeout = 60
[[queues.producers]]queue = "queues-web-crawler"binding = "CRAWLER_QUEUE"src/index.tsの環境インターフェースにバインディングを追加して、TypeScriptがバインディングを正しく型付けできるようにします。キューをQueue<any>として型付けします。次のステップでは、この型を変更する方法を示します。
import { BrowserWorker } from "@cloudflare/puppeteer";
export interface Env { CRAWLER_QUEUE: Queue<any>; CRAWLER_SCREENSHOTS_KV: KVNamespace; CRAWLER_LINKS_KV: KVNamespace; CRAWLER_BROWSER: BrowserWorker;}クローリングするリンクを送信するために、Workerにfetch()ハンドラーを追加します。
type Message = { url: string;};
export interface Env { CRAWLER_QUEUE: Queue<Message>; // ... その他}
export default { async fetch(req, env): Promise<Response> { await env.CRAWLER_QUEUE.send({ url: await req.text() }); return new Response("成功!"); },} satisfies ExportedHandler<Env>;これにより、任意のサブパスへのリクエストを受け入れ、リクエストのボディをクローリングされるURLとして転送します。リクエストがPOSTリクエストであり、ボディに適切に形成されたURLが含まれていることを確認する必要がありますが、これは簡潔さのために省略されています。
送信したリンクを処理するために、Workerにqueue()ハンドラーを追加します。
import puppeteer from "@cloudflare/puppeteer";import robotsParser from "robots-parser";
async queue(batch: MessageBatch<Message>, env: Env): Promise<void> { let browser: puppeteer.Browser | null = null; try { browser = await puppeteer.launch(env.CRAWLER_BROWSER); } catch { batch.retryAll(); return; }
for (const message of batch.messages) { const { url } = message.body;
let isAllowed = true; try { const robotsTextPath = new URL(url).origin + "/robots.txt"; const response = await fetch(robotsTextPath);
const robots = robotsParser(robotsTextPath, await response.text()); isAllowed = robots.isAllowed(url) ?? true; // robots.txtを尊重! } catch {}
if (!isAllowed) { message.ack(); continue; }
// TODO: クロールする! message.ack(); }
await browser.close();},これはクローラーの骨組みです。Puppeteerブラウザを起動し、キューから受信したメッセージを反復処理します。サイトのrobots.txtを取得し、robots-parserを使用してこのサイトがクローリングを許可しているかどうかを確認します。クローリングが許可されていない場合、メッセージはackされ、キューから削除されます。クローリングが許可されている場合は、サイトのクローリングを続けることができます。
puppeteer.launch()はtry...catchでラップされており、ブラウザの起動に失敗した場合はバッチ全体を再試行できるようにしています。ブラウザの起動は、アカウントごとのブラウザ数の制限を超えた場合に失敗する可能性があります。
type Result = { numCloudflareLinks: number; screenshot: ArrayBuffer;};
const crawlPage = async (url: string): Promise<Result> => { const page = await (browser as puppeteer.Browser).newPage();
await page.goto(url, { waitUntil: "load", });
const numCloudflareLinks = await page.$$eval("a", (links) => { links = links.filter((link) => { try { return new URL(link.href).hostname.includes("cloudflare.com"); } catch { return false; } }); return links.length; });
await page.setViewport({ width: 1920, height: 1080, deviceScaleFactor: 1, });
return { numCloudflareLinks, screenshot: ((await page.screenshot({ fullPage: true })) as Buffer).buffer, };};このヘルパー関数は、Puppeteerで新しいページを開き、提供されたURLに移動します。numCloudflareLinksはPuppeteerの$$eval(document.querySelectorAllに相当)を使用して、cloudflare.comページへのリンクの数を見つけます。リンクのhrefがcloudflare.comページへのものであるかどうかを確認する処理は、hrefがURLでない場合を処理するためにtry...catchでラップされています。
次に、関数はブラウザのビューポートサイズを設定し、ページ全体のスクリーンショットを撮ります。スクリーンショットはBufferとして返され、ArrayBufferに変換されてKVに書き込まれます。
リンクを再帰的にクロールできるようにするには、Cloudflareリンクの数を確認した後に、キューの消費者からキュー自体にメッセージを再帰的に送信するスニペットを追加します。クローリングで可能なように、再帰が深すぎると、Durable ObjectのSubrequest depth limit exceeded.エラーが発生します。これが発生した場合はキャッチされますが、リンクは再試行されません。
// const numCloudflareLinks = await page.$$eval("a", (links) => { ...
await page.$$eval("a", async (links) => { const urls: MessageSendRequest<Message>[] = links.map((link) => { return { body: { url: link.href, }, }; }); try { await env.CRAWLER_QUEUE.sendBatch(urls); } catch {} // 何もしない、サブリクエスト制限に達した可能性});
// await page.setViewport({ ...次に、queueハンドラーでURLに対してcrawlPageを呼び出します。
// in the `queue` handler:// ...if (!isAllowed) { message.ack(); continue;}
try { const { numCloudflareLinks, screenshot } = await crawlPage(url); const timestamp = new Date().getTime(); const resultKey = `${encodeURIComponent(url)}-${timestamp}`; await env.CRAWLER_LINKS_KV.put(resultKey, numCloudflareLinks.toString(), { metadata: { date: timestamp }, }); await env.CRAWLER_SCREENSHOTS_KV.put(resultKey, screenshot, { metadata: { date: timestamp }, }); message.ack();} catch { message.retry();}
// ...このスニペットは、crawlPageからの結果を適切なKV名前空間に保存します。予期しないエラーが発生した場合、URLは再試行され、再度キューに送信されます。
KVにクロールのタイムスタンプを保存することで、あまり頻繁にクロールしないようにすることができます。
KVで、最後の1時間内にクロールが行われたかどうかを確認するために、robots.txtを確認する前にスニペットを追加します。これにより、同じURLで始まるすべてのKVキーがリストされ(同じページのクロール)、最後の1時間内にクロールが行われたかどうかを確認します。最後の1時間内にクロールが行われた場合、メッセージはackされ、再試行されません。
type KeyMetadata = { date: number;};
// in the `queue` handler:// ...for (const message of batch.messages) { const sameUrlCrawls = await env.CRAWLER_LINKS_KV.list({ prefix: `${encodeURIComponent(url)}`, });
let shouldSkip = false; for (const key of sameUrlCrawls.keys) { if (timestamp - (key.metadata as KeyMetadata)?.date < 60 * 60 * 1000) { // 最後の1時間内にクロールされた場合、スキップ message.ack(); shouldSkip = true; break; } } if (shouldSkip) { continue; }
let isAllowed = true; // ...最終的なスクリプトは以下に含まれています。
import puppeteer, { BrowserWorker } from "@cloudflare/puppeteer";import robotsParser from "robots-parser";
type Message = { url: string;};
export interface Env { CRAWLER_QUEUE: Queue<Message>; CRAWLER_SCREENSHOTS_KV: KVNamespace; CRAWLER_LINKS_KV: KVNamespace; CRAWLER_BROWSER: BrowserWorker;}
type Result = { numCloudflareLinks: number; screenshot: ArrayBuffer;};
type KeyMetadata = { date: number;};
export default { async fetch(req: Request, env: Env): Promise<Response> { // テスト目的のユーティリティエンドポイント await env.CRAWLER_QUEUE.send({ url: await req.text() }); return new Response("成功!"); }, async queue(batch: MessageBatch<Message>, env: Env): Promise<void> { const crawlPage = async (url: string): Promise<Result> => { const page = await (browser as puppeteer.Browser).newPage();
await page.goto(url, { waitUntil: "load", });
const numCloudflareLinks = await page.$$eval("a", (links) => { links = links.filter((link) => { try { return new URL(link.href).hostname.includes("cloudflare.com"); } catch { return false; } }); return links.length; });
// 再帰的にクロールするために - これをコメント解除してください! /*await page.$$eval("a", async (links) => { const urls: MessageSendRequest<Message>[] = links.map((link) => { return { body: { url: link.href, }, }; }); try { await env.CRAWLER_QUEUE.sendBatch(urls); } catch {} // 何もしない、サブリクエスト制限に達した可能性 });*/
await page.setViewport({ width: 1920, height: 1080, deviceScaleFactor: 1, });
return { numCloudflareLinks, screenshot: ((await page.screenshot({ fullPage: true })) as Buffer) .buffer, }; };
let browser: puppeteer.Browser | null = null; try { browser = await puppeteer.launch(env.CRAWLER_BROWSER); } catch { batch.retryAll(); return; }
for (const message of batch.messages) { const { url } = message.body; const timestamp = new Date().getTime(); const resultKey = `${encodeURIComponent(url)}-${timestamp}`;
const sameUrlCrawls = await env.CRAWLER_LINKS_KV.list({ prefix: `${encodeURIComponent(url)}`, });
let shouldSkip = false; for (const key of sameUrlCrawls.keys) { if (timestamp - (key.metadata as KeyMetadata)?.date < 60 * 60 * 1000) { // 最後の1時間以内にクロールされた場合、スキップ message.ack(); shouldSkip = true; break; } } if (shouldSkip) { continue; }
let isAllowed = true; try { const robotsTextPath = new URL(url).origin + "/robots.txt"; const response = await fetch(robotsTextPath);
const robots = robotsParser(robotsTextPath, await response.text()); isAllowed = robots.isAllowed(url) ?? true; // robots.txtを尊重する! } catch {}
if (!isAllowed) { message.ack(); continue; }
try { const { numCloudflareLinks, screenshot } = await crawlPage(url); await env.CRAWLER_LINKS_KV.put( resultKey, numCloudflareLinks.toString(), { metadata: { date: timestamp } }, ); await env.CRAWLER_SCREENSHOTS_KV.put(resultKey, screenshot, { metadata: { date: timestamp }, }); message.ack(); } catch { message.retry(); } }
await browser.close(); },};Workerをデプロイするには、次のコマンドを実行します:
npx wrangler deployURLをキューに送信してクロールし、結果をWorkers KVに保存するWorkerを正常に作成しました。
Workerをテストするには、次のcURLリクエストを使用してこのドキュメントページのスクリーンショットを取得できます。
curl <YOUR_WORKER_URL> \ -H "Content-Type: application/json" \ -d 'https://developers.cloudflare.com/queues/tutorials/web-crawler-with-browser-rendering/'完全なチュートリアルのGitHubリポジトリ ↗を参照してください。ここには、URLを送信し、クロール結果を表示するためにPagesでデプロイされたフロントエンドが含まれています。