# 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 行）を読むことで、デバッグに必要なすべての情報が得られます。

- 起動時に `urllib.request.getproxies()` を呼び出すため、`http_proxy` / `https_proxy` 環境変数がコードなしでスパイダーを設定します。`no_proxy` は http と https スキームのみに対応しています。
- リクエストごとに、`request.meta['proxy']` は常に環境よりも優先されます。それを `None` に設定すると、そのリクエストに対して直接接続を強制します。
- プロキシ URL に埋め込まれた認証情報 (`http://user:pass@host:port`) は削除され、Base64 エンコードされて `Proxy-Authorization: Basic` ヘッダーとして送信されます。エンコードはデフォルトで latin-1 であり、`HTTPPROXY_AUTH_ENCODING` を介して設定可能です。
- ミドルウェアはエンコード前に認証情報をアンコードするため、パスワード内の特殊文字 `@` などを URL エンコードしてください。それにより正しく通過します。
- 再試行でプロキシが変更された場合、ミドルウェアは古い認証ヘッダーを削除し、新しいプロキシに漏れないようにします。これは現代の Scrapy における認証情報の衛生対策であり、古いバージョンを固定しない理由です。

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

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

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

```python
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@residentialpremium.quantumproxies.io:8080"},
        )
```

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

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

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

```python
# middlewares.py
class RotatingGatewayMiddleware:
    PROXY = "http://USER:PASS@residentialpremium.quantumproxies.io:8080"

    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のリクエストがダウンローダーミドルウェアのチェーンを通る図：スパイダーがリクエストをyieldし、優先度350の自作ミドルウェアがmeta'proxy'にゲートウェイを設定し、優先度750の組み込みHttpProxyMiddlewareが認証情報をProxy-Authorizationヘッダーに変換し、ダウンローダーがローテーションゲートウェイ経由で送信する。再試行には新しいIPが割り当てられる。](https://res.cloudinary.com/dmqliab0t/image/upload/f_auto,q_auto,w_1200/v1785335142/blog/generated/qp-b9-002-diag1.png)
*ミドルウェアは meta['proxy'] を設定するだけでよく、優先順位 750 の組み込みが認証を処理し、再試行は新しい IP でチェーンに再入します。*

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

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

```python
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 バン回避チェックリスト](https://quantumproxies.io/blog/avoid-ip-ban-web-scraping)にあります。

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

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

```python
# 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
```

- **RETRY_HTTP_CODES** — 429 と 403 を追加して、ソフトバンが失敗したアイテムの代わりに再試行（および回転による新しい IP）をトリガーするようにします。
- **RETRY_TIMES** — 質の高いプールには 3-5 が適しています。10 が必要な場合は、プール自体が不健康である兆候です。
- **CONCURRENT_REQUESTS_PER_DOMAIN** — 実際の礼儀のノブ。回転は負荷を IP に分散しますが、ターゲットは総量をまだ見ています。
- **DOWNLOAD_TIMEOUT** — デフォルトの 180 秒では、1 つの遅い出口が 3 分間同時実行スロットを占有します。30 で十分です。
- **AUTOTHROTTLE** — 観測されたレイテンシに合わせてペーシングを調整します。レート制限されたターゲットでの回転と相性が良いです。まだ 429 に溺れていますか？私たちの[レート制限ガイド](https://quantumproxies.io/blog/fix-429-too-many-requests-scraping)は、同時実行予算を詳しく説明しています。

![Scrapyの2つのローテーション構成の比較：BANされたIPが除外されリクエストが再キューされるにつれて縮んでいくDIYのプロキシリストと、サーバー側でローテーションする1つのエンドポイントであるローテーションゲートウェイ。カードの要点：リストにはヘルスチェックと除外が必要で、20個の静的IPは数分で消えることもあり、ゲートウェイならすべての再試行が新しいIPで行われる。](https://res.cloudinary.com/dmqliab0t/image/upload/f_auto,q_auto,w_1200/v1785335142/blog/generated/qp-b9-002-diag2.png)
*DIY リストはヘルスチェック、削除ロジック、古い IP を意味します。ゲートウェイはサーバー側で回転する 1 つのエンドポイントです — 再試行はデフォルトで新しい IP に着地します。*

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

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

```python
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 リクエストのコスト](https://quantumproxies.io/blog/headless-browser-vs-http-requests-cost)を比較検討してから選択してください — ほとんどの Scrapy プロジェクトはブラウザではなく、より良い IP を必要としています。

[Scrapy プロジェクトに回転プロキシを組み込む](https://quantumproxies.io/rotating-proxies)
