Geonode logo
Geonode Team

Geonode Team

更新日:2026年10月7日

公開日:2026年9月2日

クロールリストの作成方法:完全ガイド

クロールリストとは、クローラーがアクセスしようとするURLの集合であり、これを適切に管理できるかどうかが、処理を完了できるクローラーと、いつまでも実行し続けてしまうクローラーとの最大の違いとなります。 興味深い課題は、単にURLを取得することではありません。次にどのURLを取得すべきかを決定すること、2つのURLが同じページであることを認識すること、そしていつ停止すべきかを判断することです。 このガイドでは、シード、正規化、フロンティア、優先順位付け、および予算について解説します。また、正規化を「おおよその」ものではなく「正確な」ものにするための仕様詳細についても説明します。

当社の立場:当社はGeonodeであり、プロキシを販売しています。これらはクローリングの入力要素であり、リスト管理の目的ではありません。 率直に言えば、管理が不十分なクロールリストは、不適切なプロキシプランを選ぶことよりもはるかに大きなコストを招きます。 URLの正規化を行わないクローラーは、クエリパラメータの順序が異なるだけで同じページを何十回も訪問することになり、ギガバイト単位の課金体系では、そのたびに料金が発生します。 予算設定のないクローラーは、カレンダーに従って無限に動作し続け、その分が請求されます。リストの修正は無料ですが、請求書発行後の修正には費用がかかります。そのセクションは以下にありますが、まず最初に読む価値のある部分です。

クロールリストとは実際何なのか

よく混同されがちな3つの要素。

シードリスト — 開始点。数個のURL、あるいは完全なサイトマップ。

フロンティア — 発見され、キューに入れられたものの、まだフェッチされていないURL。これが実際のデータ構造であり、すべての設計上の判断が反映されている部分です。

訪問済みセット — すでにフェッチされたURL。重複してフェッチされないよう保持されます。

そのライフサイクルはループです:フロンティアからURLを取り出し、フェッチし、リンクを抽出して正規化し、訪問済みセットにすでに含まれているものやキューにすでに追加されているものを除外し、残りをフロンティアに追加し、そのURLを訪問済みとしてマークします。フロンティアが空になるか、リソースが尽きるまでこれを繰り返します。

概要はシンプルです。しかし、各ステップには、もし間違えれば1日分の時間を費やすことになるような細かい点があります。

シード:最初のURLの出典

作業量の少ない順に挙げます。

サイトマップ。 サイト自体の目録であり、変更内容を示すlastmodタイムスタンプが含まれているため、利用可能なシードの中で最も優れています。robots.txt内のSitemap:ディレクティブを介してサイトマップを見つけ、サイトマップのインデックスファイルを再帰的に追跡します。

パートナーのフィードやAPI。 これらが存在すれば、クロールは全く必要ないかもしれません。

Webアーカイブ。 インターネットアーカイブのCDX APIは、ドメインの過去のURLを返します。これには、もはやどこからもリンクされていないページも含まれます。対象サイトには一切費用がかからず、クロールでは見つけることのできない孤立ページを発見できます。

検索演算子。 site: のようなクエリは、インデックスに登録されたページだけでなく、さらに有用なことに、あなたが知らなかったサブドメインも明らかにします。

カテゴリページとインデックスページ。 ターゲットを絞ったクロールを行う場合、ホームページから始めて運任せにするよりも、関心のある特定のリストページからシードする方がはるかに効率的です。

ホームページ。 最後の手段であり、多くの人が最初に頼るデフォルトの手法です。1つのURLから始めてリンクを辿ってすべてを発見しようとするのは、カバレッジが最悪になる最も遅いルートです。

ウェブサイトの全ページを見つける方法では、ソース階層全体を網羅しました。要約すると、優れたシードリストがあれば、クロールはフェッチに変わります。

URLの正規化:誰もが見落としがちなステップ

クローラーにおいて最も価値の高いエンジニアリング手法であり、かつ最も頻繁に見落とされるものです。

これがなければ、訪問済みセットには5つの異なるURLが存在し、サーバー側には1つのページとして認識されます:

http://Example.com/Products
http://example.com/products
http://example.com/products/
http://example.com:80/products
http://example.com/products?utm_source=email

RFC 3986では、常に安全な正規化方法が定義されています。

大文字小文字の正規化。 「スキームとホストは大文字小文字を区別しないため、小文字に正規化すべきである。例えば、URI <HTTP://www.EXAMPLE.com/>

は <http://www.example.com/>."

と同等である。ただし、次の制限に注意すること:「スキームで特に別段の定義がない限り、その他の一般的な構文要素は大文字小文字を区別するとみなされる。」 パスは大文字と小文字を区別するため、小文字に変換しないでください。

また、「パーセントエンコーディングの3文字組に含まれる16進数(例:%3a

対 %3A

)は大文字と小文字を区別しないため、大文字を使用して正規化すべきである」。

パーセントエンコーディングによる正規化。 RFCでは、これを「それ以外では同一であるURI間の差異が生じやすい原因」と指摘しています。その理由は、「一部のURI生成者が、パーセントエンコーディングを必要としないオクテットをパーセントエンコーディングしてしまう」ためです。これらは、「予約されていない文字に対応するパーセントエンコーディングされたオクテットをすべてデコードすることで正規化されるべき」です。

パスセグメントの正規化。 「一部の実装では、参照がすでにURIである場合に参照の解決が不要であると誤って想定している」ため、remove_dot_segments

アルゴリズムを適用して、.

および..

のセグメントを削除する。

スキームに基づく正規化。 RFCでは、以下の4つが等価であるという典型的な例が挙げられている:

http://example.com
http://example.com/
http://example.com:/
http://example.com:80/

したがって、空のパスは「/

というパスに正規化されるべき」であり、デフォルトまたは空のポートは「スキームに基づく正規化によって削除されるべき」です。

仕様を超えて、以下の3つの正規化は厳密な安全性というよりは実用的なものであり、注意して適用する価値があります:

追跡パラメータの削除。 utm_*

、fbclid

、gclid

、 セッション識別子。これらはコンテンツを変更することはほぼなく、URLの数を膨大に増やしてしまいます。推測するのではなく、リストを作成しておきましょう。

残りのクエリパラメータを並べ替える。 ?a=1&b=2

と ?b=2&a=1

は通常、同じページを指します。通常は――ただし、順序に依存するアプリケーションもあるため、サンプルでテストしてください。

フラグメントを削除する。 #section

はクライアントサイドの要素であり、サーバーには決して到達しません。クロール目的では、これを削除しても問題ありません。

**そして、rel="canonical"

を尊重してください。** ページが正規 URL を宣言している場合、それはサイト側が複数のアドレスのうちどれが正しいものかを示していることになります。これを尊重することは、サイト自身の権威を背景にした、コストのかからない重複排除となります。

フロンティア:キューの設計

クローラーがスケーラブルかどうかは、3つの特性によって決まります。

重複排除のコストは低く抑えなければなりません。 URLを追加する前に、それがすでに登録されているかどうかを確認します。URLが100万件にもなると、線形スキャンは実用できません。 ハッシュセットはある程度までは有効ですが、それを超えると、ブルームフィルタを使用することで、わずかなメモリ使用量で定数時間のメンバーシップ判定が可能になります。ただし、偽陽性率が高くなるため、未訪問のURLを時折見落とすことになります。ほとんどのクローリングではこのトレードオフで問題ありませんが、網羅性が重要な場合は、フィルタの背後に正確なストアを用意してください。

順序は制御可能でなければならない。 単純なFIFOキューは幅優先探索(BFS)を行い、これは通常望ましい動作である――早期に広範囲をカバーし、探索の深さを浅く保つことができる。LIFOスタックは深さ優先探索(DFS)を行い、1つの分岐を深く掘り下げるが、サイトのクロールにはほとんど役に立たない。 優先度付きキューを使えば、後述するように、任意の基準で順序付けが可能です。

ホストごとの状態を追跡する必要があります。 実際には、フロンティアは単一のキューではなく、ホストごとのキューであり、それによって各ホストに個別に「礼儀的なレート制限」が適用されます。グローバルなレート制限を持つ単一のグローバルキューでは、1つの大規模サイトが他のすべてのサイトのリソースを独占してしまうことになります。

スケーラビリティを確保できる構造は、ホストごとのキューの集合と、次にフェッチの対象となるホストを選択するスケジューラで構成されます。ここで「対象となる」とは、そのホストへの前回のリクエストから十分な時間が経過していることを意味します。

優先順位の決定

すべてのURLを取得できない場合、次にどのURLを取得すべきか。

深さ順。 通常、深さが浅いページほど重要度が高い。シンプルで効果的なデフォルト設定です。

パスパターンによる優先順位付け。 商品ページを取得したい場合は、/product/ に一致するURLを優先します。これは、ターゲットを絞ったクロールにおいて最も高いリターンが期待できるヒューリスティックであり、計算コストも極めて低いです。

lastmod による優先順位付け。 サイトマップから、変更されたページを取得します。

変更履歴による優先順位付け。 定期的なクロールの場合、過去に頻繁に変更されたページは、今後も頻繁に変更される傾向があります。

被リンク数による優先順位付け。 多くの場所からリンクされているページは、通常、より重要です。クロール中の計算コストはかかりますが、大規模な作業ではその価値があります。

推定価値による優先順位付け。 実際の目的が何であれ。価格情報を得たい場合は、価格が含まれている可能性が高いページを優先します。

実用的な仕組みとしては、キューへの追加時にこれらのシグナルのうちいくつかから計算された小さな整数スコアを、優先度キューのキーとして使用します。複雑な仕組みは、その複雑さを正当化できることはめったにありません。深さとパスパターンのボーナスを組み合わせれば、ほとんどのニーズをカバーできます。

範囲設定:予算と落とし穴

制限がないと、一部のクロールは終了しません。これは例外的なケースではありません。

最大深さ。 リンクからリンク、さらにその先のリンク。深さを5~6に制限すれば、実際のサイト構造のほぼすべてをカバーできます。

ホストあたりの最大ページ数。 厳格な数値です。この上限に達した場合は、継続するのではなく、停止して報告してください。

総ページ数の最大値。 ジョブ全体について。

最大帯域幅。 特に従量課金制のプロキシ通信では、無制限のクロールは無限の請求額につながります。

パターンの除外。 カレンダーは典型的な無限空間です。next month

のようなリンクは、永遠にURLを生成し続けます。大規模なカタログにおけるファセットナビゲーションは、組み合わせの爆発を引き起こします。以下のパターンでこれらを除外してください:

/calendar/
/?filter=
/*?sort=

重複コンテンツの検出。 本文をハッシュ化してください。100個のURLが同一のコンテンツを返す場合、それは100ページではなく、生成された空間であると考えられます。

そして、発見率を注視してください。 最も有用な単一の警告指標:取得したページごとに、新たに発見されたURLの数を追跡します。 有限のサイトでは、この数値は着実にゼロに向かって低下します。横ばいまたは上昇し続ける場合は、URLを処理できる速度よりも速いペースで何かがURLを生成していることを意味します。その原因としては、トラップ、カレンダー、あるいはファセットによる爆発的な増加などが考えられます。意図的なバージョンについては、ハニーポット・トラップで解説しました。

再クロールスケジュールの設定

1回以上実行されるものについては、そのリストがスケジュールとなります。

変動の度合いに応じて優先順位を付けましょう。 1時間ごとに変更されるページは1時間ごとのチェックが必要ですが、1年に1回しか変更されないページにはその必要はありません。最も変動の激しい項目に必要な頻度で全てをクロールすることは、予算をオーバーする最も一般的な原因です。

条件付きリクエストを活用する。 If-Modified-Since や If-None-Match を使用すると、再クロールを一連の 304 Not Modified レスポンスに変換でき、各レスポンスのコストは数百バイト程度に抑えられます。ほとんどのページに変更がない再クロールでは、これにより請求額と対象サーバーの負荷を1桁分削減できます。

観測結果に基づいて調整する。 10回のチェックでページに変更がなかった場合は、チェック頻度を下げます。直近3回のチェックで変更があった場合は、チェック頻度を上げます。単純な乗法バックオフで十分です。

削除を明示的に検出する。 ページは消滅することがありますが、サイト側がそれを告知することはめったにありません。404エラーを監視するか、ポリシーを適用します。つまり、3回連続のクロールで検出されなかったURLは、非アクティブとしてマークします。これを怠ると、データセットはもはや存在しないエントリで埋まってしまい、エントリの欠落よりも早く信頼性を損なうことになります。

ストレージとスケーラビリティ

インメモリ方式が機能しなくなる場合についての補足。

URLがおよそ10万件程度までであれば、Pythonのセットやリストで十分です。不必要に複雑にしないようにしましょう。

数百万程度までは、URLのハッシュとフェッチステータスにインデックスを設定したローカルデータベース(SQLiteが適しています)が有効です。永続化により、クロールがクラッシュしても最初からやり直すのではなく再開できるため、これはパフォーマンス以上に重要です。

それを超える場合、適切なキューとキーバリューストアを使用します。フロンティアをホストごとに分割することで、ワーカーにホスト全体を割り当てることができ、調整なしでも「ポリテネス」が正しく保たれます。

あらゆるスケールで効果を発揮する2つの設計上の決定事項:

URLだけでなく、URLハッシュも保存する。 固定長ハッシュでの比較やインデックス作成はコストが低く、ストレージ容量も少なくて済みます。

解析結果だけでなく、生のレスポンスを保存する。 パーサーが故障した場合(必ず起こります)、手元にあるデータを再解析するコストはゼロですが、再取得には帯域幅とユーザーの信頼を損なうリスクが伴います。

最小限の実装

具体的な仕組みを把握しやすいよう、全体を約40行で記述しました。

import time
from collections import deque, defaultdict
from urllib.parse import urlsplit, urlunsplit, parse_qsl, urlencode

TRACKING = {"utm_source", "utm_medium", "utm_campaign", "fbclid", "gclid"}

def normalise(url):
    p = urlsplit(url)
    host = p.hostname or ""
    port = "" if p.port in (None, 80, 443) else f":{p.port}"
    query = urlencode(sorted(
        (k, v) for k, v in parse_qsl(p.query, keep_blank_values=True)
        if k.lower() not in TRACKING
    ))
    return urlunsplit((p.scheme.lower(), host + port, p.path or "/", query, ""))

class Frontier:
    def __init__(self, delay=1.5, max_per_host=5000):
        self.queues = defaultdict(deque)
        self.seen = set()
        self.next_ok = defaultdict(float)
        self.counts = defaultdict(int)
        self.delay, self.max_per_host = delay, max_per_host

    def add(self, url, depth=0):
        url = normalise(url)
        if url in self.seen or depth > 5:
            return False
        host = urlsplit(url).hostname
        if self.counts[host] >= self.max_per_host:
            return False
        self.seen.add(url)
        self.counts[host] += 1
        self.queues[host].append((url, depth))
        return True

    def next(self):
        now = time.monotonic()
        for host, q in self.queues.items():
            if q and self.next_ok[host] <= now:
                self.next_ok[host] = now + self.delay
                return q.popleft()
        return None

そこには5つの設計上の決定事項が見て取れ、それぞれが上記のセクションに対応しています。

正規化は add() で行われ、フェッチ時に行われるわけではありません。 重複排除が正しく行われるのは、正規化された形式が訪問済みセットに格納される場合のみです。したがって、後で正規化を行うと、その時点で既に重複データが保存されてしまいます。

訪問済みセットは、処理完了時ではなく、キューへの追加時に構築されます。 そうでなければ、20のページで発見されたURLは、最初のフェッチが完了する前に20回もキューに入れられてしまいます。

キューはホスト単位であり、優先順位もホスト単位です。 next_ok は、各ホストに次にアクセスできるタイミングを記録しているため、1つの大規模サイトが他のサイトのリソースを独占することはなく、遅延は適切な場所で適用されます。

予算制限はキューへの追加時点で適用されます。 探索深度およびホストごとの上限により、URLがメモリを消費する前に拒否されます。これが、制限されたクロールと、RAMを使い果たして初めて限界に気づくクロールとの違いです。

next() は、処理をブロックするのではなく、None を返します。 これにより、呼び出し元は待機するか、他の作業を行うか、あるいは処理を終了するかを自由に決定できます。データ構造内部でスリープするスケジューラは、計測を行うことができません。

意図的に省かれているのは永続性であり、実用的なシステムを構築する際には、これが最初に追加すべき機能です。シードリストから再起動しなければならないほどクラッシュしたクロールは、時間を失うだけでなく、すべてを再取得することになり、帯域幅とユーザーの信頼を損なうことになります。

リストの計測

何を測定すべきか。「取得したページ数」だけを報告するクロールでは、ほとんど何もわからないからだ。

経時的なフロンティアの規模。 ゼロに向かって減少するはずである。増加している場合は、発見が無制限に続いていることを意味する。

発見率。 上記と同様に、取得したページあたりの新規URL数。

ホストごとのステータス別取得結果。 集計値だけでは、1つのホストが完全に機能していないことが隠れてしまう。

重複率。 発見されたURLのうち、すでに知られていたものの割合。正規化後の重複率が高い場合は、正規化処理で何かが見落とされていることを意味する。

有用なページあたりのバイト数。 クロールと請求書を結びつける指標であり、不要な画像をメガバイト単位で取得しているヘッドレスブラウザを明らかにする指標でもある。

コンテンツのアサーション。 ページに期待通りのマーカーが含まれているかどうか。ソフトブロックされたページを返しながら100%の成功率を報告するクローラーは、コストのかかる失敗であり、これを検出できるのはコンテンツのアサーションだけです。

よくある質問

クロールリストとは何ですか?

クローラーがアクセスしようとするURLの集合で、通常はシードリスト、発見済みだがまだ取得されていないURLのフロンティア、および訪問済みセットで構成されます。フロンティアの管理(順序、重複排除、リソース配分)こそが、クロールが完了するかどうかを決定づける最大の要因となります。

クロールのためにURLを正規化するにはどうすればよいですか?

スキームとホストを小文字に変換しますが、パスは変換しません。パーセントエンコードされた16進数を大文字に変換し、不要なパーセントエンコードをデコードし、ドット区切りを削除し、デフォルトポートを削除し、空のパスを / に正規化し、フラグメントを削除し、既知のトラッキングパラメータを削除します。RFC 3986では、最後の2つを除くすべてが定義されています。

クロール・フロンティアとは何ですか?

発見されたものの、まだフェッチされていないURLのキューです。実際には、ホストごとのキューの集合と、礼儀正しさの制限の範囲内で次に処理可能なホストを選択するスケジューラで構成されています。これは、単一のグローバルキューでは、1つの大規模サイトが他のすべてのサイトの処理を妨げてしまうためです。

クローラーが永遠に実行され続けるのを防ぐにはどうすればよいですか?

厳格な制限を設定します:最大探索深度、ホストごとおよび全体での最大ページ数、帯域幅の上限です。カレンダーやファセットナビゲーション用のパターン除外を追加し、新規に発見されたURLと取得済みページの比率が低下しなくなった際にアラートを発するようにします。

同じページを2回クロールしないようにするにはどうすればよいですか?

同じページには多くの有効なアドレスが存在するため、重複排除の前にURLを正規化してください。その後、訪問済みセットへの所属を確認します。小規模な場合はハッシュセット、大規模な場合はブルームフィルタやデータベースを使用します。ページでrel="canonical"が宣言されている場合は、これを尊重してください。

再クロールはどのくらいの頻度で行うべきですか?

要件が許容する限り、できるだけ低い頻度で行い、各ページが実際に変更される頻度に応じて段階的に調整してください。サイトマップのlastmodや条件付きリクエストを活用し、変更のないページに対してはフルフェッチではなく304のみを実行するようにします。変動の激しいページの頻度に合わせてすべてをクロールすることは、最も一般的なリソースの無駄の原因となります。

ブルームフィルターとは何ですか?また、必要ですか?

ブルームフィルターは、ごくわずかなメモリ使用量で定数時間で要素の包含を判定できる確率的集合です。ただし、偽陽性の可能性がわずかにあります。つまり、実際には確認していない URL を時折見落とすことがあります。URL が数百万件を超える場合は導入する価値がありますが、それ以下の場合は不要です。その場合は、単純な集合の方がシンプルで正確です。

ページが削除されたことをどのように検知すればよいですか?

404エラーを監視し、単に表示されなくなったページに対してはポリシーを適用します。例えば、3回連続のクロールで検出されなかったURLを「非アクティブ」としてマークするなどです。これを怠ると、データセットにはもはや存在しないエントリが蓄積され、データに欠落がある場合よりも早く信頼性を損なうことになります。

まとめ

クローリングは、そのほとんどが管理作業です。データの取得自体はライブラリによって解決済みの問題ですが、何を取得すべきかを判断し、すでに取得済みのものを認識し、いつ停止すべきかを把握することこそが、エンジニアリングの真骨頂です。

難易度に見合わないほど注目すべき点が3つあります。正規化です。これを行わないと、同じページに十数もの異なるアドレスからアクセスすることになり、その都度コストが発生してしまいます。RFC 3986には、どの変換が安全かが正確に記されています。 予算。一部のURL空間は文字通り無限であり、厳格な制限のないクローラーは必ずその無限の領域に遭遇してしまうからです。そして発見率。これは、請求書が届くはるか以前に、落とし穴や予定表、あるいはファセット検索による爆発的な増加を明らかにする唯一の指標だからです。

そして、適切なシードを行うこと。サイトマップがあれば、クロールは変更点の取得に変わり、アーカイブクエリはリンクの辿りでは決して到達できないページを浮き彫りにします。どちらも対象側には一切コストがかかりません。ホームページから始めて「うまくいくことを願う」のは、最も不完全な結果に至る最も遅いルートですが、それでもなお、それがデフォルトの手法となっています。