← Cloudflare Queues / queues / tutorials
Создание веб-краулера с Queues и Browser Run
В этом руководстве описано, как создать и развернуть веб-краулер с помощью Queues, Browser Run, а также Puppeteer.
Puppeteer представляет собой высокоуровневую библиотеку для автоматизации взаимодействия с браузерами Chrome и Chromium. На каждой переданной странице краулер подсчитывает количество ссылок на cloudflare.com и сделать снимок экрана сайта, сохранив результаты в Workers KV.
С помощью Puppeteer можно запрашивать все изображения на странице, сохранять цвета, используемые на сайте, и многое другое.
Предварительные требования
- Зарегистрируйтесь для получения Аккаунт Cloudflare ↗.
- Установка
Node.js↗.
менеджер версий Node.js
Используйте менеджер версий Node, например Volta ↗ или nvm ↗ чтобы избежать проблем с правами доступа и переключать версии Node.js. Wrangler, о котором пойдёт речь далее в этом руководстве, требует версию Node 16.17.0 или более поздней версии.
1. Создание нового приложения Workers
Для начала создайте приложение Worker с помощью create-cloudflare CLI ↗. Откройте окно терминала и выполните следующую команду:
npm create cloudflare@latest -- queues-web-crawlerДля настройки выберите следующие параметры:
- Для С чего вы хотите начать?, выберите
Hello World example. - Для Какой шаблон вы хотите использовать?, выберите
Worker only. - Для Какой язык вы хотите использовать?, выберите
TypeScript. - Для Хотите использовать git для контроля версий?, выберите
Yes. - Для Хотите развернуть приложение?, выберите
No(мы внесём некоторые изменения перед развёртыванием).
Затем перейдите в только что созданный каталог:
cd queues-web-crawler2. Создание пространства имён KV
Нам нужно создать хранилище KV. Сделать это можно через дашборд Cloudflare или Wrangler CLI. В этом руководстве мы будем использовать Wrangler CLI.
npx wrangler kv namespace create crawler_linksnpx wrangler kv namespace create crawler_screenshots🌀 Creating namespace with title "web-crawler-crawler-links"
✨ Success!
Add the following to your configuration file in your kv_namespaces array:
[[kv_namespaces]]
binding = "crawler_links"
id = "<GENERATED_NAMESPACE_ID>"
🌀 Creating namespace with title "web-crawler-crawler-screenshots"
✨ Success!
Add the following to your configuration file in your kv_namespaces array:
[[kv_namespaces]]
binding = "crawler_screenshots"
id = "<GENERATED_NAMESPACE_ID>"Добавление привязок KV в конфигурационный файл Wrangler
Затем добавьте в файл Wrangler следующее, используя значения, полученные в терминале:
{
"kv_namespaces": [
{
"binding": "CRAWLER_SCREENSHOTS_KV",
"id": "<GENERATED_NAMESPACE_ID>",
},
{
"binding": "CRAWLER_LINKS_KV",
"id": "<GENERATED_NAMESPACE_ID>",
},
],
}[[kv_namespaces]]
binding = "CRAWLER_SCREENSHOTS_KV"
id = "<GENERATED_NAMESPACE_ID>"
[[kv_namespaces]]
binding = "CRAWLER_LINKS_KV"
id = "<GENERATED_NAMESPACE_ID>"3. Настройка Browser Run
Теперь необходимо настроить Worker для Browser Run.
В текущем каталоге установите форк Puppeteer а также robots-parser ↗:
npm i -D @cloudflare/puppeteernpm i robots-parserЗатем добавьте привязку Browser Run. Привязка Browser Run предоставляет Worker доступ к безголовому экземпляру Chromium, которым вы будете управлять с помощью Puppeteer.
{
"browser": {
"binding": "CRAWLER_BROWSER",
},
}[browser]
binding = "CRAWLER_BROWSER"4. Настройка очереди
Теперь нужно настроить очередь.
npx wrangler queues create queues-web-crawlerCreating queue queues-web-crawler.
Created queue queues-web-crawler.Добавление привязок Queue в конфигурацию Wrangler
Затем добавьте в файл Wrangler следующее:
{
"queues": {
"consumers": [
{
"queue": "queues-web-crawler",
"max_batch_timeout": 60,
},
],
"producers": [
{
"queue": "queues-web-crawler",
"binding": "CRAWLER_QUEUE",
},
],
},
}[[queues.consumers]]
queue = "queues-web-crawler"
max_batch_timeout = 60
[[queues.producers]]
queue = "queues-web-crawler"
binding = "CRAWLER_QUEUE"Добавление max_batch_timeout в 60 секунд для очереди потребителя важен, поскольку позволяет очереди собирать сообщения в пакет за более длительный период. Это помогает управлять Browser Run ограничения скорости запросов и может повысить эффективность за счёт обработки нескольких URL-адресов в одном пакете с использованием одного экземпляра браузера.
Итоговый файл Wrangler должен выглядеть примерно так, как показано ниже.
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "web-crawler",
"main": "src/index.ts",
// Set this to today's date
"compatibility_date": "2026-08-28",
"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,
},
],
"producers": [
{
"queue": "queues-web-crawler",
"binding": "CRAWLER_QUEUE",
},
],
},
}"$schema" = "./node_modules/wrangler/config-schema.json"
name = "web-crawler"
main = "src/index.ts"
# Set this to today's date
compatibility_date = "2026-08-28"
compatibility_flags = [ "nodejs_compat" ]
[[kv_namespaces]]
binding = "CRAWLER_SCREENSHOTS_KV"
id = "<GENERATED_NAMESPACE_ID>"
[[kv_namespaces]]
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"5. Добавление привязок в окружение
Добавление привязок в интерфейс окружения, в src/index.ts, чтобы TypeScript корректно типизировал привязки. Тип очереди задаётся как Queue<Message>, где Message определяется на следующем шаге.
import type { BrowserWorker } from "@cloudflare/puppeteer";
export interface Env {
CRAWLER_QUEUE: Queue<Message>;
CRAWLER_SCREENSHOTS_KV: KVNamespace;
CRAWLER_LINKS_KV: KVNamespace;
CRAWLER_BROWSER: BrowserWorker;
}6. Отправка ссылок для сканирования
Добавьте fetch() обработчик в Worker для отправки ссылок на обход.
type Message = {
url: string;
};
export interface Env {
CRAWLER_QUEUE: Queue<Message>;
// ... etc.
}
export default {
async fetch(req, env, ctx): Promise<Response> {
await env.CRAWLER_QUEUE.send({ url: await req.text() });
return new Response("Success!");
},
} satisfies ExportedHandler<Env>;Это будет принимать запросы на любой подпуть и передавать тело запроса на обработку краулером. Предполагается, что тело запроса содержит только URL. В продакшене следует проверять, что запрос был POST и содержит корректно сформированный URL в теле запроса. Для простоты это опущено.
7. Сканирование с помощью Puppeteer
Добавьте queue() обработчик в Worker для обработки отправляемых вами ссылок.
import puppeteer from "@cloudflare/puppeteer";
import robotsParser from "robots-parser";
async queue(batch, env, ctx): 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; // respect robots.txt!
} catch {}
if (!isAllowed) {
message.ack();
continue;
}
// TODO: crawl!
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 обёрнута в try...catch для обработки случаев, когда hrefs не могут быть URL-адресами.
Затем функция задаёт размер области просмотра браузера и делает скриншот всей страницы. Скриншот возвращается в виде 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 {} // do nothing, likely hit subrequest limit
});
// await page.setViewport({ ...Затем в queue обработчик, вызовите crawlPage для URL.
// 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 помогает избежать слишком частого обхода.
Добавление фрагмента кода перед проверкой robots.txt , чтобы проверить в KV наличие обхода за последний час. При этом выводятся все ключи KV, начинающиеся с того же URL (обходы одной и той же страницы), и проверяется, выполнялся ли обход за последний час. Если обход уже выполнялся за последний час, сообщение 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) {
// if crawled in last hour, skip
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, env, ctx): Promise<Response> {
// util endpoint for testing purposes
await env.CRAWLER_QUEUE.send({ url: await req.text() });
return new Response("Success!");
},
async queue(batch, env, ctx): 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;
});
// to crawl recursively - uncomment this!
/*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 {} // do nothing, might've hit subrequest limit
});*/
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) {
// if crawled in last hour, skip
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; // respect 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();
},
} satisfies ExportedHandler<Env, Message>;8. Развёртывание Worker
Чтобы развернуть Worker, выполните следующую команду:
npx wrangler deployВы успешно создали Worker, который может отправлять URL-адреса в очередь для сканирования и сохранять результаты в Workers KV.
Чтобы протестировать Worker, можно использовать следующий запрос cURL и сделать снимок этой страницы документации.
curl <YOUR_WORKER_URL> \
-H "Content-Type: application/json" \
-d 'https://developers.cloudflare.com/queues/tutorials/web-crawler-with-browser-run/'См. Репозиторий GitHub с полным руководством ↗, включая интерфейс, развёрнутый с помощью Pages, для отправки URL-адресов и просмотра результатов сканирования.