前のいくつかのレッスンでは、Scrapyクローラーフレームワークの使用方法について学びました。ただし、これらのフレームワークはすべて同じホスト上で実行されており、クロールの効率は比較的低くなります。複数のホストを共同でクロールできる場合、クロールの効率は必然的に指数関数的に増加します。これが分散クローラーの利点です。
次に、分散クローラーの基本原則と、分散クローラーを実装するためのScrapyのプロセスを見てみましょう。
Scrapyの基本的なクローラー機能を以前に実装しました。クローラーは非同期でマルチスレッド化されていますが、1つのホストでしか実行できないため、クロールの効率はまだ制限されていますが、分散クローラーには複数のクローラーがあります。ホストが組み合わされてクロールタスクが一緒に完了するため、クロールの効率が大幅に向上します。
1.分散クローラーアーキテクチャ
分散クローラーのアーキテクチャを理解する前に、図に示すように、まずScrapyのアーキテクチャを確認します。
SqueyスタンドアロンクローラーにはローカルクロールキューQueueがあり、これはdequeモジュールを使用して実装されます。新しいリクエストが生成されると、キューに入れられ、スケジューラによってリクエストがスケジュールされます。その後、リクエストはダウンローダーに渡されてクロールが実行され、シンプルなスケジューリングアーキテクチャが図に示されています。
2つのスケジューラが同時にキューからリクエストを取得する場合、各スケジューラは対応するダウンローダーを持ちます。帯域幅が十分であり、通常のクロールであり、キューアクセスの負荷が考慮されていない場合、クロールの効率はどうなりますか?そうです、クロールの効率は2倍になります。
このようにして、スケジューラは複数拡張でき、ダウンローダーも複数拡張できます。クロールキューキューは常に1でなければなりません。これは、いわゆる共有クロールキューです。この方法でのみ、スケジューラがキューからリクエストをスケジュールした後、他のスケジューラがリクエストを繰り返しスケジュールせず、複数のスケジューラを同時にクロールできることが保証されます。これは分散クローラーの基本的なプロトタイプであり、単純なスケジューリングアーキテクチャが図に示されています。
必要なのは、協調クロールのために複数のホストでクローラータスクを同時に実行することです。協調クロールの前提は、クロールキューを共有することです。このように、各ホストは独自のクロールキューを維持する必要はありませんが、共有クロールキューからのリクエストにアクセスします。ただし、各ホストには独自のスケジューラとダウンローダーがあるため、スケジューリング機能とダウンロード機能は別々に完了します。キューアクセスのパフォーマンス消費を考慮しない場合でも、クロール効率は2倍になります。
2.クロールキューを維持する
では、このキューをどのように維持するのですか?最初に検討する必要があるのはパフォーマンスです。データベースのアクセス効率はどの程度高いのでしょうか。現時点では、メモリストレージに基づいてRedisを当然考えていましたが、Redisはリストリスト、セットセット、ソート済みセットなどのさまざまなデータ構造もサポートしています。アクセス操作も非常に簡単なので、ここではRedisを使用します。クロールキューを維持します。
これらのタイプのデータ構造ストレージには実際に独自のメリットがあり、分析は次のとおりです。
-
リストのデータ構造にはlpush、lpop、rpush、rpopメソッドがあるため、これを使用して、先入れ先出しのクロールキューまたは先入れ先出しのスタッククロールキューを実装できます。
-
セットの要素は順序付けされておらず、繰り返されないため、ランダムに並べ替えられた繰り返しのないクロールキューを簡単に実装できます。
-
順序付きセットにはスコアがあり、Scrapy's Requestにも優先制御があるため、順序付きセットを使用して、優先順位スケジューリングを備えたキューを実装できます。
特定のクローラーのニーズに応じて、これらの異なるキューを柔軟に選択する必要があります。
3.重量を取り除く方法
Scrapyには自動重複排除機能があり、その重複排除はPythonのコレクションを使用します。このコレクションは、各リクエストのフィンガープリントをScrapyに記録します。これは、実際にはリクエストのハッシュ値です。Scrapyのソースコードは次のように見ることができます。
import hashlib
def request_fingerprint(request, include_headers=None):
if include_headers:
include_headers = tuple(to_bytes(h.lower())
for h in sorted(include_headers))
cache = _fingerprint_cache.setdefault(request, {
})
if include_headers not in cache:
fp = hashlib.sha1()
fp.update(to_bytes(request.method))
fp.update(to_bytes(canonicalize_url(request.url)))
fp.update(request.body or b'')
if include_headers:
for hdr in include_headers:
if hdr in request.headers:
fp.update(hdr)
for v in request.headers.getlist(hdr):
fp.update(v)
cache[include_headers] = fp.hexdigest()
return cache[include_headers]
request_fingerprintはRequestのフィンガープリントを計算するメソッドであり、そのメソッドは内部でhashlibのsha1メソッドを使用します。計算されるフィールドには、リクエストのメソッド、URL、本文、ヘッダーが含まれます。ここに違いがある限り、計算結果は異なります。計算の結果は暗号化された文字列であり、これは指紋でもあります。各リクエストには一意のフィンガープリントがあります。フィンガープリントは文字列です。リクエストオブジェクトが繰り返されるかどうかを判断するよりも、文字列が繰り返されるかどうかを判断する方がはるかに簡単です。したがって、フィンガープリントは、リクエストが繰り返されるかどうかを判断する基準として使用できます。
それで、それが重複しているかどうかをどのように判断しますか?Scrapyは次のように実装されています。
def __init__(self):
self.fingerprints = set()
def request_seen(self, request):
fp = self.request_fingerprint(request)
if fp in self.fingerprints:
return True
self.fingerprints.add(fp)
重複排除クラスRFPDupeFilterには、request_seenメソッドがあり、このメソッドにはパラメーターリクエストがあります。その役割は、Requestオブジェクトが重複しているかどうかを検出することです。このメソッドはrequest_fingerprintを呼び出してリクエストのフィンガープリントを取得し、フィンガープリントがフィンガープリント変数に存在するかどうかを検出します。フィンガープリントはコレクションであり、コレクションの要素は繰り返されません。フィンガープリントが存在する場合はTrueを返し、リクエストが重複していることを示します。それ以外の場合は、フィンガープリントがセットに追加されます。次回同じリクエストが渡され、フィンガープリントが同じである場合、フィンガープリントはすでにコレクションに存在しており、リクエストオブジェクトは直接重複と判断されます。このようにして、重複排除の目的が達成されます。
Scrapyの重複除外プロセスでは、コレクション要素の非反復的な特性を使用して、リクエストの重複除外を実現します。
分散クローラーの場合、重複を削除するために各クローラーの独自のコレクションを使用することはできません。このように、各ホストは独自のコレクションを個別に保持し、共有することはできません。複数のホストが同じリクエストを生成する場合、それらは各ホストの重複排除のみが可能であり、各ホストの重複排除はできません。
複数のホストの重複排除を実現するには、このフィンガープリントコレクションも共有する必要があります。Redisにはストレージデータ構造のコレクションがあります。Redisコレクションをフィンガープリントコレクションとして使用できるため、重複排除コレクションも共有されます。各ホストは新しいリクエストを生成した後、リクエストのフィンガープリントをセットと比較します。フィンガープリントがすでに存在する場合は、リクエストが重複していることを意味します。それ以外の場合は、リクエストのフィンガープリントをこのセットに追加します。同じ原理と異なるストレージ構造を使用して、分散型リクエストの重複除外も実現できます。
4.中断を防ぐ
Scrapyでは、クローラーランタイムのリクエストキューがメモリに配置されます。クローラーが中断されると、このキューのスペースが解放され、キューが破棄されます。したがって、クローラーが中断されると、クローラーを再度実行することは、新しいクロールプロセスに相当します。
中断後もクロールを続行するには、リクエストをキューに保存し、保存したデータを次のクロールで読み取って、最後のクロールのキューを取得します。Scrapyでクロールキューのストレージパスを指定できます。このパスは、JOB_DIR変数によって識別されます。次のコマンドを使用して、次のことを実行できます。
scrapy crawl spider -s JOBDIR=crawls/spider
詳細な使用方法については、公式ドキュメントを参照してください。リンクはhttps://doc.scrapy.org/en/latest/topics/jobs.htmlです。
Scrapyでは、実際にクロールキューをローカルに保存し、2回目のクロールでキューを直接読み取って復元できます。それで、私たちはまだ分散型アーキテクチャでこの問題を心配する必要がありますか?必要なし。クロールキュー自体はデータベースに格納されているため、クローラーが中断された場合でも、データベース内の要求は存在し続け、次に中断された場所からクロールが続行されます。
したがって、Redisキューが空の場合、クローラーは再度クロールし、Redisキューが空でない場合、クローラーは前回中断された場所から引き続きクロールします。
アーキテクチャの実装
次に、このアーキテクチャをプログラムに実装する必要があります。まず、共有クロールキューと重複排除機能を実装する必要があります。さらに、スケジューラが共有クロールキューからリクエストにアクセスできるように、スケジューラの実装を書き換える必要があります。
幸いにも、誰かがこれらのロジックとアーキテクチャを実装し、Scrapy-Redisと呼ばれるPythonパッケージとしてリリースしました。
次のセクションでは、Scrapy-Redisのソースコード実装とその詳細な動作原理について説明します。