Scrapy プロキシ ミドルウェア: バンされずにプロキシを回転させる方法

Scrapy はバージョン 0.8 からプロキシサポートを提供していますが、ドキュメントには大規模にプロキシを回転させ、再試行し、バンを回避する方法が記載されていません。ここでは、ミドルウェアの構造、コード、クロールが完了するかどうかを決定する設定について説明します。

Scrapy はバージョン 0.8 からプロキシ ミドルウェアを提供しているため、「Scrapy はプロキシをサポートしているか」という疑問は 15 年前に解決されました。しかし、ドキュメントが完全に組み立てていないのは、実際の運用の様子です。組み込みの Scrapy プロキシ ミドルウェアが実際にどのように認証情報を処理するのか、リクエストごとにプロキシを設定するタイミングとグローバルに設定するタイミング、ターゲットがクロール中に出口をバンし始めたときにどのように回転し回復するのか。このガイドでは、組み込みの HttpProxyMiddleware、リクエストごとの meta、バン処理を備えたカスタム回転ミドルウェア、および 100k ページのクロールが完了するか午前 3 時に停止するかを決定する設定について説明します。

組み込みの Scrapy プロキシ ミドルウェアの動作

scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware は、DOWNLOADER_MIDDLEWARES_BASE でデフォルトで優先順位 750 で有効になっています。そのソースコード(Scrapy 2.17 では約 100 行)を読むことで、デバッグに必要なすべての情報が得られます。

実際の結果として、サードパーティのプロキシパッケージはほとんど必要ありません。優先順位 750 より前に request.meta['proxy'] に有効なプロキシ URL を設定するものは、認証とヘッダー処理を無料で受けられます。

メタを使用したリクエストごとのプロキシ

最も軽量な統合は、スパイダー内で meta を直接設定することです。これは、一部のリクエストのみがプロキシを必要とする場合や、異なるターゲットが異なる出口国を必要とする場合に便利です。

import scrapy


class PricesSpider(scrapy.Spider):
    name = "prices"

    def start_requests(self):
        yield scrapy.Request(
            "https://example.com/product/1",
            meta={"proxy": "http://USER:PASS@gate.quantumproxies.io:PORT"},
        )

プロキシ選択自体がデータ駆動型の場合、リクエストごとのメタが輝きます。ドイツの製品ページをドイツ向けのユーザー名でルーティングし、画像のダウンロードを安価なデータセンターの出口で行い、HTML を住宅用に送信するか、2 回目の試行で粘着性のあるセッションに昇格させます。メタは再試行とリダイレクトを通じて生き残るため、リクエストを作成する際に行った決定は、ダウンロードサイクル全体を通じてそれに従います。しかし、数千のリクエストに手動でメタを設定することはスケールしません。それがダウンローダーミドルウェアの役割です。

最もシンプルな運用セットアップ: 回転ゲートウェイ

単一のゲートウェイエンドポイントの背後にある回転プロキシを使用すると、回転はサーバー側で行われます。同じ URL を通過するすべてのリクエストが、200 以上の国にまたがる 90M+ の住宅プールから異なる IP で終了します。ミドルウェアは 3 行に縮小され、ヘルスチェックやプルーニング、リフレッシュするリストはありません。

# middlewares.py
class RotatingGatewayMiddleware:
    PROXY = "http://USER:PASS@gate.quantumproxies.io:PORT"

    def process_request(self, request, spider):
        request.meta["proxy"] = self.PROXY


# settings.py
DOWNLOADER_MIDDLEWARES = {
    "myproject.middlewares.RotatingGatewayMiddleware": 350,
}

優先順位 350 が重要です: ミドルウェアは 750 の組み込みより前に実行される必要があり、HttpProxyMiddleware が認証情報を認証ヘッダーに変換できるようにします。このペアリング — ゲートウェイ回転と Scrapy の標準再試行機構 — は、コード行ごとの最高の信頼性を提供します。なぜなら、すべての再試行が自動的に新しい出口 IP を通過するからです。

Scrapy プロキシ ミドルウェア チェーンのフローダイアグラム: スパイダーリクエスト、カスタムミドルウェアがメタプロキシを設定、HttpProxyMiddleware が優先順位 750 で認証を追加、ダウンローダーが回転ゲートウェイを通じて終了
ミドルウェアは meta['proxy'] を設定するだけでよく、優先順位 750 の組み込みが認証を処理し、再試行は新しい IP でチェーンに再入します。

バン処理を備えたカスタム回転プロキシ ミドルウェア

独自のプロキシリストを実行する場合 — 静的 ISP アドレスや混合プール — 古典的なパターン(scrapy-proxies パッケージで普及したもので、無料リストプロキシが頻繁に失敗するため RETRY_TIMES = 10 を推奨)は次のとおりです: リクエストごとにランダムに選択し、バン信号で削除し、リクエストを再キューします。

import random


class ProxyListMiddleware:
    BAN_CODES = {403, 429}

    def __init__(self, proxies):
        self.proxies = list(proxies)

    @classmethod
    def from_crawler(cls, crawler):
        return cls(crawler.settings.getlist("PROXY_LIST"))

    def process_request(self, request, spider):
        if self.proxies and "proxy" not in request.meta:
            request.meta["proxy"] = random.choice(self.proxies)

    def process_response(self, request, response, spider):
        if response.status in self.BAN_CODES:
            bad = request.meta.get("proxy")
            if bad in self.proxies and len(self.proxies) > 1:
                self.proxies.remove(bad)
                spider.logger.warning("Evicted %s (%d left)", bad, len(self.proxies))
            return request.replace(dont_filter=True)  # re-queue on a new proxy
        return response

このミドルウェアがゲートウェイが無料で行うことをすべて行う必要があることに注意してください: プールの健康状態を追跡し、焼かれた IP を削除し、リクエストを再キューします。また、攻撃を受けると縮小します — 攻撃的なターゲットでは、20 の静的 IP のプールが数分で蒸発する可能性があります。より広範なバン回避プレイブック(ペーシング、ヘッダー、セッション規律)は、IP バン回避チェックリストにあります。

Scrapy の設定が結果を決定する

ミドルウェアはプロキシを配置しますが、設定はクロールがどのように動作するかを決定します。これらが最も重要です:

# settings.py — a sane baseline for proxied crawls
RETRY_TIMES = 5
RETRY_HTTP_CODES = [429, 403, 500, 502, 503, 504, 408]

CONCURRENT_REQUESTS = 32
CONCURRENT_REQUESTS_PER_DOMAIN = 8
DOWNLOAD_TIMEOUT = 30

AUTOTHROTTLE_ENABLED = True
AUTOTHROTTLE_TARGET_CONCURRENCY = 4.0
DIY Scrapy プロキシリスト ミドルウェアとプロキシ回転のための回転住宅ゲートウェイの比較
DIY リストはヘルスチェック、削除ロジック、古い IP を意味します。ゲートウェイはサーバー側で回転する 1 つのエンドポイントです — 再試行はデフォルトで新しい IP に着地します。

ステータスコードを超えたバン検出

洗練されたターゲットは正直な 403 を送信しません。彼らは CAPTCHA ページ、空の製品グリッド、または削除されたテンプレートを含む 200 を提供します。堅牢なスパイダーは、ステータスだけでなくコンテンツを検証します — 実際のページにのみ存在する要素を確認し、それが欠けている場合は再キューします。良いプロキシを通じてもページが構造的に空で戻ってくる場合、コンテンツはおそらくクライアント側でレンダリングされています。なぜスクレイパーが空のページを取得するのかを参照してください。そして、あるドメインが IP に関係なくプレーン HTTP を打ち負かす場合、そのドメインを JavaScript をレンダリングし、クリーンな HTML または markdown を返すScraper APIに渡してください。Scrapy はそれを他のレスポンスと同様に消費し、パイプラインを維持します。

def parse(self, response):
    if not response.css("div.product-grid"):
        # 200 OK but the real content is missing: soft ban or JS wall
        yield response.request.replace(dont_filter=True)
        return
    for product in response.css("div.product-grid article"):
        yield {"name": product.css("h2::text").get()}

よくある質問

Scrapy は標準でプロキシをサポートしていますか?

はい。HttpProxyMiddleware は優先順位 750 で有効になっています: http_proxy / https_proxy 環境変数を読み取り、リクエストごとに request.meta['proxy'] を尊重し、user:pass@ 認証情報を含み、それを Proxy-Authorization ヘッダーに変換します。各リクエストにどのプロキシを割り当てるかを決定するためのコードを書く必要があります。

Scrapy で単一のリクエストにプロキシを設定するにはどうすればよいですか?

リクエストのメタに渡します: scrapy.Request(url, meta={'proxy': 'http://user:pass@host:port'})。メタは常に環境レベルのプロキシを上書きし、値を None に設定すると、その 1 つのリクエストが直接行くことを強制します — 1 つのスパイダーでプロキシと非プロキシのトラフィックを混在させるのに便利です。

Scrapy でプロキシを回転させるにはどうすればよいですか?

リストからリクエストごとにプロキシを選択し、バンされたものを削除するダウンローダーミドルウェアを書くか、すべてのリクエストをサーバー側で新しい出口 IP を割り当てる回転ゲートウェイエンドポイントに向けます。ゲートウェイアプローチは 3 行のミドルウェアが必要で、ヘルスチェックは不要で、すべての Scrapy の再試行を無料で回転に変えます。

なぜ私の Scrapy プロキシは 407 を返すのですか?

プロキシが認証を拒否しました。認証情報がメタ内のプロキシ URL に含まれていること、特殊文字が URL エンコードされていること、プランが IP 認証を使用している場合はサーバー IP がホワイトリストに登録されていることを確認してください。認証情報に非 ASCII 文字が含まれている場合は、HTTPPROXY_AUTH_ENCODING をプロバイダーに合わせて設定してください。

覚えておくべきパターン: meta['proxy'] を早めに設定し、750 の組み込みミドルウェアに認証を任せ、再試行コードに 429 と 403 を追加し、リストの管理よりもサーバー側の回転を優先します。クロールの一部が JavaScript を必要とする場合は、ヘッドレスブラウザと HTTP リクエストのコストを比較検討してから選択してください — ほとんどの Scrapy プロジェクトはブラウザではなく、より良い IP を必要としています。

Scrapy プロジェクトに回転プロキシを組み込む