httpx、aiohttp、プロキシを使った非同期Pythonスクレイピング
非同期は遅いスクレイパーを速くし、速いスクレイパーをブロックされるものに変える — プロキシ、セマフォ、タイムアウトを正しく設定しない限り。ここでは、httpxとaiohttpの全パターンをコードと共に紹介します。
スクレイパーがネットワーク待機に多くの時間を費やす場合、非同期は利用可能な最大の速度向上です — そしてプロキシはその速度がブロックされないようにするものです。このガイドでは、httpxとaiohttpを使った非同期Pythonスクレイピングをプロキシと共にエンドツーエンドでカバーします: 各ライブラリがプロキシを設定する方法、セマフォで同時実行数を制限する方法、実際に発火するタイムアウトを設定する方法、セッションを壊さずにIPをローテーションする方法。すべて実行可能なコードとあなた自身の資格情報に置き換えるプレースホルダー付きです。
なぜ非同期なのか、そしてプロキシはどこに適合するのか
標準のrequestsはブロッキングです: 各呼び出しは次の開始前に応答を待ちます。500ページを取得するには、500回の往復を連続して支払います。非同期はそれらを1つのイベントループで同時に取得するため、合計時間は最も遅い単一のリクエストに向かって収束します。問題は: 1つのIPからの同時リクエストのバーストは、アンチボットシステムが監視するシグネチャそのものです。解決策は、速度を落とすことではなく、ローテーションプロキシプールにトラフィックを分散し、セマフォで意図的にスロットルすることです。
httpxでプロキシを設定する(非同期)
httpxは実用的なデフォルトです。1つのクライアントモデルが同期と非同期の両方を行い、HTTP/2をサポートします。モダンなAPIに注意してください: クライアント上の単一のproxy=引数であり、古いproxies=辞書ではありません — アップグレード後に「なぜプロキシが無視されるのか」という混乱の一般的な原因です。資格情報はプロキシURLに直接入れます。
import asyncio, httpx
PROXY = "http://USER:PASS@gate.quantumproxies.io:8000"
async def fetch(client, url):
r = await client.get(url, timeout=httpx.Timeout(20.0))
return url, r.status_code, r.text
async def main(urls):
async with httpx.AsyncClient(proxy=PROXY, http2=True) as client:
tasks = [fetch(client, u) for u in urls]
return await asyncio.gather(*tasks)
urls = ["https://httpbin.org/ip"] * 5
print(asyncio.run(main(urls)))
すべてのリクエストに再利用される1つのクライアントがポイントです — それは接続プールを温かく保ち、プロキシを通じて繰り返されるTLSハンドシェイクをスキップします。リクエストごとに新しいクライアントを作成することは、最も一般的な非同期パフォーマンスのバグです: それは接続の再利用とクッキーステートを毎回捨てます。
aiohttpでプロキシを設定する(リクエストごと)
aiohttpはasyncioネイティブで、最も細かい同時実行制御を提供しますが、そのプロキシの慣例は異なります: プロキシはsession.get()でリクエストごとに渡され、セッションではありません。それは実際にはローテーションに便利です。ここで人々を悩ませる2つのことがあります — デフォルトのUser-Agentは文字通りPython/3.x aiohttp/3.xであり、必ず上書きする必要があります。また、接続プールのサイズを明示的に設定する必要があります。
import aiohttp, asyncio
PROXY = "http://USER:PASS@gate.quantumproxies.io:8000"
HEADERS = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36"}
async def fetch(session, url):
timeout = aiohttp.ClientTimeout(total=30, connect=10, sock_read=20)
async with session.get(url, proxy=PROXY, timeout=timeout) as resp:
return url, resp.status, await resp.text()
async def main(urls):
conn = aiohttp.TCPConnector(limit=20, limit_per_host=8)
async with aiohttp.ClientSession(connector=conn, headers=HEADERS) as session:
return await asyncio.gather(*(fetch(session, u) for u in urls))
print(asyncio.run(main(["https://httpbin.org/ip"] * 5)))
TCPConnector(limit=20, limit_per_host=8)は、開いている接続の総数と単一ホストへの接続を制限します — 1つのターゲットがプール全体を吸収するのを防ぐ礼儀の第一線です。

セマフォで同時実行数を制限する
無制限の同時リクエストを発火することは、プロキシプールを燃やし、レート制限を引っ掛ける最速の方法です。asyncio.Semaphoreはスロットルです: それは一度に飛行中のリクエストの数を制限します、キューに入れたタスクの数に関係なく。グローバルな制限を設定し、攻撃的なジョブにはドメインごとの制限も設定します。
sem = asyncio.Semaphore(15) # never more than 15 requests in flight
async def guarded_fetch(client, url):
async with sem:
return await fetch(client, url)
async def main(urls):
async with httpx.AsyncClient(proxy=PROXY) as client:
return await asyncio.gather(*(guarded_fetch(client, u) for u in urls))
適切な数はターゲットの許容度とプールのサイズに依存し、マシンの速度には依存しません。保守的に始め(10–20)、ブロック率を監視し、成功が高いままである限りのみそれを上げます。1つのスティッキーIPでは、1桁に保ちます。
タイムアウト: 各フェーズをモデル化する
プロキシされたリクエストは、直接のものよりも多くの場所で失敗します — DNS解決、プロキシへの接続、トンネル設定、ターゲットへの接続、ヘッダーの待機、ボディの読み取りはすべて異なる停止です。単一の包括的なタイムアウトは、どのフェーズがハングしたかを隠します。aiohttpのClientTimeout(total=, connect=, sock_read=)とhttpxのTimeout()は、それらを個別に制限することができます。例外のない唯一のルール: タイムアウトなしでリクエストを発行しないこと、さもなければ1つのデッドエグジットがコルーチンを永遠にハングさせ、イベントループを静かに飢えさせます。
セッションを壊さずにプロキシをローテーションする
ローテーション戦略は2つあり、間違ったものを選ぶとデータが破損します。リクエストごとのランダムローテーションは、ステートレスなページ取得に最適です。しかし、それはクッキー、ログイン、ローカリゼーションに依存するフローを破壊します。なぜなら、リクエスト2はリクエスト1とは異なるIPに着地するからです。クリーンな分割: 論理ユニットの境界でローテーションする — 1つのIPをクロールセグメントまたはアカウントごとに使用し、回転ゲートウェイを使用して自動的に新しい出口を提供します。これにより、コードがリストを管理することはありません。
# rotating gateway: one endpoint, new exit IP per request
ROT = "http://USER:PASS@rotating.quantumproxies.io:8000"
# sticky session: same IP for a multi-step flow, tag the session id
STICKY = "http://USER-session-a1b2:PASS@gate.quantumproxies.io:8000"
async def crawl_segment(urls):
async with httpx.AsyncClient(proxy=ROT) as client: # rotates per call
return await asyncio.gather(*(fetch(client, u) for u in urls))
高い同時実行数のスクレイピングには、回転する住宅用ゲートウェイが実用的なデフォルトです: 200以上の国で9000万以上のIPにわたるリクエストごとのローテーション、カートやログインが数分間同じ出口を必要とする場合のスティッキーセッション。ジョブに住宅用、ISP、データセンターのどれを使用するかを検討している場合、どのプロキシタイプを使用するべきかについてのガイドがトレードオフを説明しています。
リトライ、バックオフ、ジッター
デッドエグジットと一時的なブロックは、スケールでは通常のことであり、例外ではありません。しかし、単純なリトライは事態を悪化させます: 50の非同期タスクが同時に失敗し、すべてが即座にリトライすると、ターゲットを元の実行よりも強く叩く同期バーストを発火します。リトライを分散させるために指数バックオフとランダム化されたジッターを追加し、試行回数を制限し、失敗時には焼けたIPを再利用するのではなく回転させます。
import random
from httpx import HTTPError
async def robust_fetch(client, url, tries=3):
for attempt in range(tries):
try:
r = await client.get(url, timeout=httpx.Timeout(20.0))
if r.status_code < 400 and looks_real(r.text):
return r
except HTTPError:
pass
# exponential backoff + jitter before the next attempt
await asyncio.sleep((2 ** attempt) + random.uniform(0, 1))
return None

200は成功ではない
最も微妙な非同期スクレイピングのバグは、HTTP 200を完了と見なすことです。アンチボットシステムは、CAPTCHAページ、アクセス拒否通知、空の結果セット、またはJSチャレンジを伴う200を返します — したがって、ステータスコードだけで「成功」をスコアするプロキシは、静かにブロックされたページを提供しています。コンテンツを検証します: 応答を信頼する前に、既知の要素、最小長、またはチャレンジマーカーの不在を確認します。それが上記のリトライループでのlooks_real()ゲートです。
スタックを手作業で組み立てるのをやめるとき
上記のパターン — 非同期クライアント、セマフォ、タイムアウト、ローテーション、コンテンツ検証 — はほとんどのターゲットをクリーンに処理します。しかし、一度サイトがCloudflare、TLSフィンガープリンティング、または重いクライアントサイドレンダリングを重ねると、プロキシに関係なく生の非同期HTTPは失敗し始めます。なぜなら、PythonのTLSハンドシェイクはChromeのものとは全く異なるからです。そのラインでは、Scraper APIが実際のブラウザフィンガープリントを持ち、IPをローテーションし、JavaScriptをオンデマンドでレンダリングするのが、すべてを手作業で維持するよりも少ないコードで高い成功率をもたらします。ヘッドレス対HTTPコストに関する私たちの投稿は、そのエスカレーションがどこで報われるかをカバーしています。
よくある質問
aiohttpでプロキシをどのように使用しますか?
リクエストごとにプロキシURLを渡します: session.get(url, proxy="http://user:pass@host:port")。requestsとは異なり、aiohttpはセッションにプロキシ辞書を受け取りません。デフォルトのUser-Agentを常に上書きし(Python/3.x aiohttp/3.xは明らかなボットシグナルです)、ClientTimeoutを設定して、デッドエグジットがコルーチンをハングさせないようにします。
非同期スクレイピングにはhttpxとaiohttpのどちらが良いですか?
デフォルトとしてhttpxを選びます: 1つのクライアントが同期と非同期の両方で動作し、HTTP/2が組み込まれており、プロキシは単一の引数です。最大の同時実行制御が必要な場合はaiohttpを選びます — 明示的な接続プールの制限とリクエストごとのプロキシは、大規模で高速なクロールに適しています。どちらも良いですが、ライブラリよりもプロキシ戦略が重要です。
どれだけの同時リクエストを実行すべきですか?
マシンが許容する数ではなく、ターゲットとプールが許容する数です。飛行中のセマフォを10〜20で開始し、ブロックとエラー率を監視し、成功が高いままである限りのみそれを上げます。1つのスティッキーIPでは、1桁に保ちます。広範なIPローテーションにより、より高い総同時実行数を安全に実行できます。
なぜ非同期スクレイパーが同期のものよりもブロックされるのですか?
なぜなら、同時実行がシグナルを集中させるからです: 1つのIPからの多くの同時リクエストは、典型的なボットパターンです。ローテーションプールに負荷を分散し、セマフォでスロットルし、リトライにジッター付きバックオフを追加し、応答コンテンツを検証します — 200でもチャレンジページである可能性があります。ローテーションなしの速度がブロックされた原因です。
これが完全な非同期パターンです: クライアントを選び、それに適した方法でプロキシを設定し、セマフォで同時実行数を制限し、すべてのタイムアウトフェーズを制限し、論理境界でローテーションし、裸の200を決して信頼しない。プロキシレイヤーを最初に正しく設定すれば、ほとんどのブロックリストはそれに到達する前に消えます。