INTEGRITY Документация

Создание веб-краулера с Queues и Browser Run

В этом руководстве описано, как создать и развернуть веб-краулер с помощью Queues, Browser Run, а также Puppeteer.

Puppeteer представляет собой высокоуровневую библиотеку для автоматизации взаимодействия с браузерами Chrome и Chromium. На каждой переданной странице краулер подсчитывает количество ссылок на cloudflare.com и сделать снимок экрана сайта, сохранив результаты в Workers KV.

С помощью Puppeteer можно запрашивать все изображения на странице, сохранять цвета, используемые на сайте, и многое другое.

Предварительные требования

  1. Зарегистрируйтесь для получения Аккаунт Cloudflare ↗.
  2. Установка 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

Для настройки выберите следующие параметры:

Затем перейдите в только что созданный каталог:

cd queues-web-crawler

2. Создание пространства имён KV

Нам нужно создать хранилище KV. Сделать это можно через дашборд Cloudflare или Wrangler CLI. В этом руководстве мы будем использовать Wrangler CLI.

npx wrangler kv namespace create crawler_links
npx 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/puppeteer
npm 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-crawler
Вывод
Creating 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;
}

Добавьте 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
curl <YOUR_WORKER_URL> \
  -H "Content-Type: application/json" \
  -d 'https://developers.cloudflare.com/queues/tutorials/web-crawler-with-browser-run/'

См. Репозиторий GitHub с полным руководством ↗, включая интерфейс, развёрнутый с помощью Pages, для отправки URL-адресов и просмотра результатов сканирования.