Claude Media
Claude CodeでCrawleeのクローラーを組む — 間隔と並列数を設定で固定

Claude CodeでCrawleeのクローラーを組む — 間隔と並列数を設定で固定

Crawleeの並列数・レート・リトライ・キューをClaude Codeに設計させ、アクセス間隔とUser-Agentを設定ファイルとテストで固定する手順です。

Crawleeは、Node.jsとPythonで使えるクローラー用ライブラリです。キューの管理、並列数の自動調整、リトライ、セッション管理を最初から備えています。この記事では、Claude Codeに設計と実装を任せつつ、相手サイトへの負荷に直結する値を人が決めた設定で固定する手順を説明します。

ここで扱うのは自前のクローラー側です。Apifyの実行基盤からActorを呼ぶ連携はClaudeとApifyを連携する方法にあります。PythonのScrapyで同じ問題を扱った例はClaude CodeでScrapyのスクレイパーを作るです。

礼儀に関わる設定は、どのオプションに書くのか

クローラーの礼儀は、アクセス間隔、同時接続数、1分あたりの件数、リトライ回数、robots.txtの扱い、User-Agentの6点でほぼ決まります。CrawleeのNode.js版では、このうち5点がクローラーのコンストラクターのオプションで指定できます。

守りたいことオプション既定値
同じドメインへの間隔オプションsameDomainDelaySecs既定値0(待たない)
同時リクエスト数の上限オプションmaxConcurrency既定値200
1分あたりの上限オプションmaxRequestsPerMinute既定値Infinity
失敗時の再試行オプションmaxRequestRetries既定値3
1回の実行で開くページ数オプションmaxRequestsPerCrawl既定値未設定
robots.txtの遵守オプションrespectRobotsTxtFile既定値未指定

既定値の読み方に注意が要ります。sameDomainDelaySecs は0、maxRequestsPerMinute は無制限です。何も指定しないクローラーは、同じドメインに対して待たずに、自動調整の範囲で並列数を増やしていきます。並列数の既定値は、1本から始めて最大200本まで増える動きです。

maxRequestsPerCrawl については、設定ミスによる無限ループを防ぐために常に指定するよう、リファレンスに書かれています。並列で動かすと、実際に開くページ数がこの値を少し超えることがある点も併記されています。

User-Agentだけは、上の表に対応するオプションがありません。respectRobotsTxtFile にオブジェクトを渡すと、robots.txtを判定するときのUser-Agentを userAgent で指定できます。省略すると * として判定されます。実際に送るリクエストのヘッダー側は、HTTPクローラーの preNavigationHooks で触ります。このフックは、ナビゲーションの直前にクローラーコンテキストと gotOptions を受け取る関数です。

最初に決めておく値と、Claudeに任せる範囲

設計をまるごとClaude Codeに任せると、並列数が大きめに選ばれたり、間隔が入らなかったりします。ライブラリの既定値が穏やかではないので、理由のない値は既定値に落ち着きやすいからです。そこで役割を分けます。

  • 人が決める: 対象ドメイン、間隔、並列数の上限、1分あたりの件数、取得ページ数の上限、User-Agentの文字列と連絡先
  • Claudeに任せる: ハンドラーの実装、リンクの絞り込み(globs)、データの保存形式、失敗時のログ、テスト

人が決めた値は、1つのファイルに集めます。ここでは src/politeness.ts とします。値はあくまで例で、実際のサイトの利用規約と負荷に合わせて決めてください。

// src/politeness.ts — 人が決めた値だけを置く
export const USER_AGENT =
  'MyCrawler/1.0 (+https://example.com/bot; contact@example.com)';
 
export const POLITENESS = {
  sameDomainDelaySecs: 2,
  maxConcurrency: 2,
  maxRequestsPerMinute: 30,
  maxRequestRetries: 3,
  maxRequestsPerCrawl: 200,
  requestHandlerTimeoutSecs: 60,
  respectRobotsTxtFile: { userAgent: 'MyCrawler' },
};

User-Agentには、連絡先を含めた名乗りを入れるのが一般的な作法です。ブラウザに偽装する値は、礼儀の設定としては逆向きになります。

クローラー本体をClaude Codeに書かせる

次に、POLITENESS を読み込むだけのクローラーを書かせます。依頼文では、値を埋め込ませない点を明示します。

src/crawler.ts に CheerioCrawler を使うクローラーを作って。
- 礼儀の設定は src/politeness.ts の POLITENESS を展開して渡すだけ。値を書き換えない
- User-Agent は USER_AGENT を preNavigationHooks で付ける
- enqueueLinks は globs で /docs/** に限定する
- failedRequestHandler で失敗URLを failed.jsonl に追記する

返ってくるコードは、たとえば次のような形になります。以下はこの依頼から想定される一例で、細部はセッションごとに変わります。

import { CheerioCrawler, Dataset } from 'crawlee';
import { POLITENESS, USER_AGENT } from './politeness.js';
 
const crawler = new CheerioCrawler({
  ...POLITENESS,
  preNavigationHooks: [
    async (_ctx, gotOptions) => {
      gotOptions.headers = { ...gotOptions.headers, 'user-agent': USER_AGENT };
    },
  ],
  async requestHandler({ request, $, enqueueLinks }) {
    await Dataset.pushData({ url: request.url, title: $('title').text() });
    await enqueueLinks({ globs: ['https://example.com/docs/**'] });
  },
  async failedRequestHandler({ request }) {
    // 3回の再試行でも失敗したURLが渡される
    console.error('failed', request.url);
  },
});
 
await crawler.run(['https://example.com/docs/']);

failedRequestHandler は、maxRequestRetries の回数を超えて失敗したリクエストに対して呼ばれます。リトライの途中でリクエストを書き換えたいときは、別に errorHandler があります。こちらは、再試行のたびにその直前に呼ばれます。

preNavigationHooks の中でヘッダーを足す書き方が、実際にUser-Agentとして送られるかどうかは、コードを読むだけでは断定できません。次の節のように、手元のサーバーに受けさせて確かめます。

守れているかを、ローカルサーバーで測るテスト

CLAUDE.mdに「間隔を守ること」と書くだけでは、Claudeが守るかどうかは毎回の判断になります。Claude Codeのベストプラクティスにも、合否が返ってくるチェックを渡すと、Claudeが自分で直して再実行するループに入れると書かれています。そこで、クローラーを手元のHTTPサーバーに向け、届いた時刻とUser-Agentを記録するテストを置きます。

// test/politeness.test.ts(Vitestの例)
import http from 'node:http';
import { expect, test } from 'vitest';
import { CheerioCrawler } from 'crawlee';
import { USER_AGENT } from '../src/politeness.js';
 
test('間隔とUser-Agentが守られる', async () => {
  const hits: { at: number; ua?: string }[] = [];
  const server = http.createServer((req, res) => {
    hits.push({ at: Date.now(), ua: req.headers['user-agent'] });
    res.setHeader('content-type', 'text/html');
    res.end('<title>t</title>');
  }).listen(0);
  const { port } = server.address() as { port: number };
 
  const delaySecs = 1;
  const crawler = new CheerioCrawler({
    sameDomainDelaySecs: delaySecs,
    maxConcurrency: 4, // 並列を許しても間隔が守られるか見る
    preNavigationHooks: [async (_c, o) => {
      o.headers = { ...o.headers, 'user-agent': USER_AGENT };
    }],
    requestHandler: async () => {},
  });
  await crawler.run([1, 2, 3].map((n) => `http://localhost:${port}/${n}`));
  server.close();
 
  expect(hits.every((h) => h.ua === USER_AGENT)).toBe(true);
  for (let i = 1; i < hits.length; i++) {
    expect(hits[i].at - hits[i - 1].at).toBeGreaterThanOrEqual(delaySecs * 900);
  }
});

このテストが見ているのは2点です。届いたリクエストのUser-Agentが指定どおりか。同じドメインへの到着間隔が、並列を許した状態でも設定値に近いか。許容幅を10%取っているのは、タイマーの揺れでテストが不安定になるのを避けるためです。

本番の POLITENESS の値そのものは、別のテストで読み取ります。値が人の決めた範囲に入っているかを検査するだけなので、数行で済みます。

import { POLITENESS } from '../src/politeness.js';
test('礼儀の値が上限内', () => {
  expect(POLITENESS.sameDomainDelaySecs).toBeGreaterThanOrEqual(2);
  expect(POLITENESS.maxConcurrency).toBeLessThanOrEqual(2);
  expect(POLITENESS.maxRequestsPerCrawl).toBeDefined();
});

CLAUDE.mdと設定ファイルで、変えさせない範囲を決める

CLAUDE.mdには、Claudeがコードから読み取れない事柄だけを短く書きます。ベストプラクティスの指針に沿って、検証コマンドと触ってはいけない場所に絞ります。

# Commands
- テスト: `npx vitest run`
- 礼儀テストのみ: `npx vitest run politeness`
 
# Gotchas
- src/politeness.ts は人が管理する。値の変更は提案だけして、編集しない
- 実サイトへのクロール (`npm run crawl`) は自分で実行しない

ただし、CLAUDE.mdは指針であって、Claudeが従わないと判断する余地が残ります。ベストプラクティスは、例外を許さないルールは設定側に移すよう勧めています。.claude/settings.json に、権限ルールとStopフックを置く例です。

{
  "permissions": {
    "ask": ["Bash(npm run crawl*)"],
    "deny": ["Edit(/src/politeness.ts)"]
  },
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "npx vitest run politeness 1>&2 || exit 2"
          }
        ]
      }
    ]
  }
}

権限ルールは、deny、ask、allowの順に評価され、最初に一致したものが結果になります。上の例では、実クロールを起動するコマンドは毎回確認が入り、politeness.ts へのEditは拒否されます。Stopフックは、Claudeが作業を終えようとするたびにテストを走らせ、失敗すれば終了コード2で作業へ戻します。|| exit 2 が必要なのは、2以外の失敗コードでは止まらずに進む仕様だからです。出力を 1>&2 で標準エラーに回すと、失敗の内容がClaudeに返ります。

この構成には穴もあります。Editの拒否ルールが止めるのはEditツールで、Claudeがシェルのコマンドでファイルを書き換える経路は別です。厳密に守らせたいときは、サンドボックスや、ファイルの変更を検査するフックを併用します。送信先のドメインを管理設定側で絞る方法は、Claude Codeのドメイン許可リストを管理設定で固定するにあります。設定ファイル全般の置き場所はsettings.json完全ガイドが一覧にしています。

並列数・レート・リトライの決め方

数値の決め方は、相手サイトが公開している基準に合わせるのが基本です。基準が分からないときは、控えめな値から始めます。Claudeに任せるなら、「いくつにすべきか」ではなく、選択肢と影響を並べさせると使いやすくなります。

値上げたとき下げたとき
maxConcurrency上げたとき取得は速くなるが、相手と自分の負荷が増える下げたとき遅くなるが穏やか
maxRequestsPerMinute上げたとき瞬間的な集中を抑えつつ総量を増やせる下げたとき総量が減る
sameDomainDelaySecs上げたとき1ドメインあたりの間隔が空く下げたとき連続アクセスになる
maxRequestRetries上げたとき一時的な失敗を拾えるが、落ちているサイトに再送が続く下げたとき取りこぼしが増える

maxRequestsPerMinute は、maxConcurrency の上限に一気に達して、その後しばらく待つ動きを避ける目的で使います。毎秒の件数を数えて、バーストを抑える仕組みだとガイドに書かれています。

minConcurrency を高くしすぎる設定には警告があります。実行環境の資源に対して高すぎると、極端に遅くなったり落ちたりします。ガイドは、既定値のままにして、資源に応じた自動調整に任せるよう勧めています。礼儀の面では minConcurrency を触らず、maxConcurrency を上限として固定するのが分かりやすい構成です。

自動調整の細かいオプションは autoscaledPoolOptions から渡せます。ガイドは、十分に検証できる場合を除いて既定値のままにするよう注意しています。クロールの礼儀を守る目的なら、ここまで触る必要はありません。

キューの中身は、ディスクに残る

Crawleeのリクエストキューは、メモリ上で動きつつ、ローカルのディスクにも書き出されます。保存先は環境変数 CRAWLEE_STORAGE_DIR で決まり、未設定なら現在のディレクトリの ./storage です。既定のキューは default という名前で、request_queues/default/entries.json に配列として入ります。名前を変えたいときは CRAWLEE_DEFAULT_REQUEST_QUEUE_ID を設定します。

実行のたびに別のキューにしたいか、前回の続きから走らせたいかは、設計の判断です。Claudeには次のように聞くと、判断材料が出てきます。

このクローラーを2回続けて実行したとき、キューと結果は
どうなるか。ストレージのパスと、リセットの手段を
コードとドキュメントの両方から調べて、表にして。

答えは実装とバージョンで変わりうるので、鵜呑みにせずに、小さなURLリストで2回実行して storage/ の中身を見せるよう頼むと確実です。実行結果を読む作業は、ベストプラクティスが勧める「証拠を出させる」使い方にあたります。

つまずきやすい点

間隔を指定したのに、速く感じる。間隔の単位はドメインごとです。複数のドメインを同時に巡回していると、全体では速く見えます。礼儀の観点では、ドメイン単位の間隔が守られていれば足ります。

retryOnBlocked を有効にしてよいか。このオプションは、ボット対策を検出して回避を試みるものです。ガイドにはCloudflareのBot ManagementとGoogle検索のレート制限が挙げられています。ブロックされるのは、相手がアクセスを望んでいないか、負荷が高すぎる合図である場合があります。回避を試みる設定は、礼儀を固定する目的とは逆向きなので、Claudeに任せず人が判断する項目に入れておきます。

ブラウザ型クローラーのフィンガープリント。PlaywrightCrawlerとPuppeteerCrawlerでは、ブラウザのフィンガープリントを生成して切り替える機能が、設定なしで有効です。ブロックを避けるための機能なので、User-Agentで名乗る方針と食い違わないか、実装前に決めておきます。ブラウザ操作のテストをClaude Codeで組む例は、PlaywrightのE2Eテストをhookで自動実行するが参考になります。

robots.txtの対象。 respectRobotsTxtFile を有効にすると、各ドメインのrobots.txtを取得し、許可されないURLを飛ばします。enqueueLinks で追加される許可外のURLも対象です。飛ばされたURLの記録は、onSkippedRequest のコールバックで受けられます。robots.txtの許可は、利用規約への同意の代わりにはなりません。

Python版を使うとき。Python版には、並列数を決める ConcurrencySettings があり、max_tasks_per_minute や max_concurrency を渡します。max_concurrency の既定値は100で、Node.js版の200とは違います。ここで説明した考え方は共通ですが、オプション名と既定値はバージョンごとに必ず確かめてください。

まとめ

Crawleeの既定値は、速く取得する側に寄っています。礼儀は、設計をClaude Codeに任せたうえで、値を人が決めたファイルに集め、ローカルサーバーを使ったテストとStopフックで毎回確かめる構成にすると、実装のたびに説明し直さずに済みます。実サイトへの初回の実行だけは、権限のaskで人が起動し、maxRequestsPerCrawl を小さくして挙動を見るところから始めるのが安全です。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/best-practices
  2. code.claude.com/docs/en/permissions
  3. crawlee.dev/js/docs/guides/scaling-crawlers.md
  4. crawlee.dev/js/api/basic-crawler/interface/BasicCrawlerOptions.md
  5. crawlee.dev/js/docs/guides/request-storage.md
この記事を共有:XはてブLinkedIn