← ブログに戻る

Pythonのhttpxにおける非同期プロキシ:ローテーション、HTTP/2、初心者の5つの間違い

httpxでプロキシのローテーションを正しく設定し、HTTP/2を有効にし、90%の初心者パーサー開発者が直面する落とし穴を避ける方法を解説します。

📅2026年10月17日

httpx ライブラリは、Python における非同期 HTTP リクエストの標準となりましたが、プロキシを使用する際には、プロダクションでパーサーを壊すニュアンスがあります。非同期プロキシローテーションの設定方法、HTTP/2 の有効化方法、なぜスクリプトが 50 ~ 100 リクエストごとにクラッシュするのかを解説します。

なぜ httpx なのか、requests や aiohttp ではなく

requests は同期型で、HTTP/2 を標準でサポートしていません。aiohttp は非同期ですが、プロキシの設定があまり親切ではなく、HTTP/2 のサポートもありません。httpx は両方の問題を解決します: 同期および非同期クライアントを統一された API で提供し、h2 拡張を通じてのネイティブな HTTP/2 サポート、そしてトランスポートレベルでのプロキシとの柔軟な連携です。

マーケットプレイス(Wildberries、Ozon、Avito)のパーシング、ソーシャルメディア API からのデータ収集、競合の価格監視などのタスクにおいて、非同期性は重要です — これは、逐次リクエストに比べて速度の大幅な向上をもたらします。しかし、適切なプロキシローテーションと接続管理がなければ、非同期性は新たな問題の原因となります: 接続プールの枯渇、ハングしたコルーチン、ファイルディスクリプタのリーク。

もう一つの重要な点は、HTTP/2 が一つの TCP 接続で複数のリクエストを多重化できるため、ターゲットサーバーへの負荷を軽減し、単一の IP からの TCP ハンドシェイクの数を監視する単純なアンチボットシステムを回避するのに役立つことです。

httpx におけるプロキシの基本設定

httpx 0.26 以降、プロキシの設定構文が変更されました — 辞書 proxies の代わりに、proxy パラメータが使用されます。これは、古いチュートリアルを参照している場合に混乱の原因となることがよくあります。

import httpx
import asyncio

PROXY_URL = "http://user:[email protected]:8000"

async def fetch(url: str):
    async with httpx.AsyncClient(proxy=PROXY_URL, timeout=15.0) as client:
        response = await client.get(url)
        return response.status_code, response.text[:200]

asyncio.run(fetch("https://httpbin.org/ip"))

HTTP と HTTPS トラフィックのためにプロキシを分ける必要がある場合や、異なるドメインに対して異なるプロキシを設定する場合は、httpx.Client(mounts=...) を使用してください — トランスポートは URL パターンに基づいてマウントされます:

mounts = {
    "http://": httpx.AsyncHTTPTransport(proxy="http://user:[email protected]:8000"),
    "https://": httpx.AsyncHTTPTransport(proxy="http://user:[email protected]:8000"),
}

async with httpx.AsyncClient(mounts=mounts) as client:
    response = await client.get("https://example.com")

ユーザー名とパスワードによる認証のための形式 http://user:pass@host:port は、HTTP プロキシに対して安定して機能します。SOCKS5 を使用する場合は、httpx[socks] を追加でインストールする必要があります。そうしないと、ランタイムで ImportError が発生し、コードを書く段階ではなく実行時にエラーが発生します — これは多くの人が陥る罠です。

非同期プロキシローテーション: 動作するコード

プロキシローテーションにおける主な誤りは、各リクエストごとに新しい AsyncClient を作成することです。これはパフォーマンスを損ないます。なぜなら、クライアントは接続プールを保持しており、その再作成は keep-alive 接続のすべての利点を無にします。正しいアプローチは、あらかじめプロキシごとにクライアントプールを作成し、それらの間でタスクを分配することです。

import httpx
import asyncio
import itertools
import random

PROXIES = [
    "http://user:[email protected]:8000",
    "http://user:[email protected]:8000",
    "http://user:[email protected]:8000",
]

class ProxyPool:
    def __init__(self, proxies: list[str]):
        self.clients = [
            httpx.AsyncClient(proxy=p, timeout=httpx.Timeout(15.0, connect=5.0))
            for p in proxies
        ]
        self._cycle = itertools.cycle(self.clients)

    def get_client(self) -> httpx.AsyncClient:
        return next(self._cycle)  # ラウンドロビン
        # ランダム選択: random.choice(self.clients)

    async def close_all(self):
        for client in self.clients:
            await client.aclose()

async def fetch_with_retry(pool: ProxyPool, url: str, retries: int = 3):
    for attempt in range(retries):
        client = pool.get_client()
        try:
            response = await client.get(url)
            if response.status_code == 200:
                return response.text
        except (httpx.ConnectTimeout, httpx.ProxyError, httpx.ReadTimeout):
            await asyncio.sleep(0.5 * (attempt + 1))
            continue
    return None

async def main():
    pool = ProxyPool(PROXIES)
    urls = [f"https://example.com/page/{i}" for i in range(100)]

    sem = asyncio.Semaphore(20)  # 並列制限

    async def bound_fetch(url):
        async with sem:
            return await fetch_with_retry(pool, url)

    results = await asyncio.gather(*(bound_fetch(u) for u in urls))
    await pool.close_all()
    return results

asyncio.run(main())

このパターンは、3つの問題を同時に解決します: keep-alive を通じて TCP 接続を再利用し、ラウンドロビン方式でプロキシに負荷を分散し、セマフォを通じて並列性を制限してターゲットサーバーをダウンさせず、レート制限による禁止を引き起こさないようにします。

Wildberries、Ozon、Instagram API で動作するプロダクションパーサーにとって、プロキシプールが十分に大きく、地理的に多様であることが重要です — さもなければ、同じサブネット内でのローテーションではブロックを回避できません。ここでは、レジデンシャルプロキシ が適しています。なぜなら、実際のユーザーの IP を持ち、ASN やアドレス範囲の評判を分析するアンチボットシステムのフィルターに引っかかりにくいからです。

httpx における HTTP/2: 理由と有効化方法

デフォルトでは httpx は HTTP/1.1 を使用します。HTTP/2 を有効にするには、拡張機能をインストールし、フラグを明示的に渡す必要があります:

# pip install httpx[http2]

async with httpx.AsyncClient(http2=True, proxy=PROXY_URL) as client:
    response = await client.get("https://example.com")
    print(response.http_version)  # "HTTP/2" を期待します

重要な注意点: すべてのプロキシが HTTP/2 のトンネリングを正しくサポートしているわけではありません。一部の安価なデータセンターのプロキシは、最終サーバーが HTTP/2 をサポートしていても、HTTP/1.1 プロキシとしてのみ機能します — この場合、httpx は CONNECT トンネルを通じて静かに HTTP/1.1 にロールバックし、response.http_version を明示的に確認しない限り、これに気付かないことがあります。

HTTP/2 は、次の 3 つのシナリオで実際の利点を提供します: 一つのドメインに対して多くのリクエストを行う場合(1つの接続での多重化が遅延を減少させる)、アンチボット保護のためにリクエストの目立ちにくさを減らすことが重要な場合(単一の IP からの TCP ハンドシェイクが少ない方が自然に見える)、ターゲットサイトが HTTP/2 を要求し、HTTP/1.1 のクライアントにエラーや制限されたコンテンツを返す場合 — これは Cloudflare 上のサイトで時々見られます。

プロキシを通じての HTTP/2 接続の速度と安定性が重要なタスクには、専用の データセンターのプロキシ が適しています — これらはレジデンシャルプロキシよりも速く、通常は最新のプロトコルのトンネリングを正しくサポートします。

httpx におけるプロキシの 5 つの一般的なエラー

1. 各リクエストごとにクライアントを再作成する

async with httpx.AsyncClient(...) as client: await client.get(url) のようなコードをループ内で使用するのはアンチパターンです。クライアントの作成は新しい TCP 接続、新しい TLS ハンドシェイクを意味し、接続プールを完全に無視します。1000 リクエストで、正しいアプローチと間違ったアプローチの速度の違いは 5 ~ 10 倍に達することがあります。

2. タイムアウトがないか、デフォルトのタイムアウトが大きすぎる

デフォルトでは httpx はすべてに対して 5 秒のタイムアウトを設定します — 接続、読み取り、書き込み、プール。安定しないプロキシを使用する場合、接続にはこれが少なすぎますが、プロキシが単に「ハング」している場合、読み取りには多すぎます。タイムアウトを明示的に分けてください:

timeout = httpx.Timeout(
    connect=5.0,
    read=10.0,
    write=10.0,
    pool=5.0
)
client = httpx.AsyncClient(proxy=PROXY_URL, timeout=timeout)

3. 接続プールの制限を無視する

大量の並列タスク(セマフォなしで 500 以上のコルーチンを gather する場合)を実行すると、httpx は PoolTimeout をスローし始めます。これは、プール内の接続の制限(デフォルトは 100)が枯渇したためです。制限は httpx.Limits(max_connections=200, max_keepalive_connections=50) を通じて明示的に設定し、常に並列性を制御するためにセマフォを使用してください。

4. 理解せずに verify を無効にする

多くの人が Stack Overflow から verify=False をコピーして、MITM 証明書を使用してプロキシを介して作業する際の SSL エラーを回避しようとします。これは「中間者攻撃」を許可し、妥協されたプロキシを通じてデータ漏洩を引き起こす可能性があります。プロキシが特別な証明書を要求する場合は、verify="/path/to/cert.pem" を通じて明示的に追加し、検証を完全に無効にしないでください。

5. クライアントを閉じず、ファイルディスクリプタが漏れる

クライアントプールを手動で作成する場合(上記の例のように)、作業が終了したら各クライアントに対して await client.aclose() を呼び出すことを忘れないでください。明示的に接続を閉じない長時間実行されるスクリプトは、オペレーティングシステムのオープンファイルディスクリプタの制限(通常、Linux ではデフォルトで 1024)に達し、パーサーが数時間の作業の後に謎のエラー Too many open files でクラッシュします。

パーシングに適したプロキシの種類

プロキシの選択は、上記のリストからどれだけのエラーを実際に目にするかに直接影響します — 不安定なプロキシはタイムアウトや接続の切断を引き起こします。たとえコードが完璧に書かれていても。

プロキシの種類 速度 ブロックのリスク 使用するタイミング
データセンター 高い 保護されたサイトで高い オープン API、弱いアンチボット保護
レジデンシャル 中程度 低い Wildberries、Ozon、ソーシャルメディア
モバイル 中程度から低い 最小限 Instagram、TikTok API、厳しい保護

中程度の保護を持つ API への高速リクエストの非同期パーシングには、データセンターのプロキシが経済的に有利な選択肢です — 重要なのは、上記のコードのようにプール全体に負荷を均等に分散させることです。

プロダクション前のチェックリスト

  • httpx クライアントは一度作成され、各リクエストごとではなく再利用されます
  • タイムアウトは connect/read/write/pool に分けられ、デフォルトは使用されません
  • 並列性は asyncio.Semaphore を通じて制限されています
  • 接続プールの制限は実際の負荷に合わせて設定されています
  • ProxyError、ConnectTimeout、ReadTimeout の処理を含むリトライロジックがあります
  • すべてのクライアントは終了時に aclose() を通じて明示的に閉じられます
  • verify=False は極力使用されません
  • HTTP/2 は意識的に有効化され、response.http_version を通じて確認されます

結論

httpx はプロキシと HTTP/2 に対する非同期操作の強力なツールを提供しますが、クライアントプール、タイムアウト、接続制限の慎重な設定が必要です。プロダクションでのパーサーのクラッシュの多くは、ライブラリではなくアーキテクチャ上の誤りに起因しています: クライアントの再作成、並列性の制限の欠如、プロキシの例外処理の不適切さです。

マーケットプレイスやソーシャルメディアの API 用のパーサーを構築する場合、疑わしい活動を積極的に監視している場合、プロキシのインフラストラクチャの安定性はコードと同じくらい重要です。このようなタスクには、レジデンシャルプロキシ を検討する価値があります — これにより、安価なデータセンターのプールと比較して IP アドレスの評判が高いため、ProxyError や ConnectTimeout のエラーが減少します。