Geonode logo
Geonode Team

Geonode Team

更新日:2026年10月16日

公開日:2026年10月16日

aiohttp プロキシの設定:セッション、認証、および接続制限

aiohttp は、スクレイパーで数百もの進行中のリクエストが必要であり、イベントループを完全に制御したい場合に選ぶクライアントです。 プロキシ経由でのルーティングはいくつかのキーワード引数で設定できますが、認証、接続制限、タイムアウトに関するデフォルト設定は `requests` や httpx とは異なり、多くの aiohttp プロキシ設定で問題が発生するのはこの点にあります。 このガイドを読み終える頃には、ローテーション方式の一般家庭向けゲートウェイに対して認証を行い、制限付きの並列リクエストを実行し、予測可能なタイムアウトを設定し、プロキシエラーを種類別に報告する `ClientSession` が完成しているはずです。 例は、Python 3.11 以降上の aiohttp 3.14 を対象としています。最近のリリースで動作が変更された場合は、そのバージョンを明記しています。

aiohttp は、スクレイパーで数百もの進行中のリクエストが必要であり、イベントループを完全に制御したい場合に選ぶクライアントです。 プロキシ経由でのルーティングはいくつかのキーワード引数で設定できますが、認証、接続制限、タイムアウトに関するデフォルト設定は requests や httpx とは異なっており、多くの aiohttp プロキシ設定で問題が発生するのはこの点です。 このガイドを読み終える頃には、ローテーション方式の一般家庭向けゲートウェイに対して認証を行い、制限付きの並列リクエストを実行し、予測可能なタイムアウトを設定し、プロキシエラーを種類別に報告する ClientSession が完成しているはずです。

例は、Python 3.11 以降上の aiohttp 3.14 を対象としています。最近のリリースで動作が変更された場合は、そのバージョンを明記しています。

セッション単位で aiohttp プロキシを設定する

proxy キーワードは、リクエスト単位とセッション単位の両方で指定可能です。リクエスト単位の設定は元の API の仕様であり、現在も機能しています。一方、スクレイパーを使用する場合は、セッション単位の設定が推奨されます。これにより、個々の呼び出しがプロキシを迂回してしまう可能性を排除できるからです。

import asyncio
import aiohttp

PROXY = "http://USERNAME:PASSWORD@premium-residential.geonode.com:9000"

async def main() -> None:
    async with aiohttp.ClientSession(proxy=PROXY) as session:
        async with session.get("https://api.ipify.org?format=json") as resp:
            print(resp.status, await resp.json())

asyncio.run(main())

ポート 9000 は Geonode の HTTP(S) ローテーションゲートウェイであるため、新しい接続ごとに異なる住宅用 IP アドレスから接続されます。 個々の呼び出しでは、独自の proxy= 引数を指定することでデフォルト設定を上書きできます。これは、例えば内部のメトリクスエンドポイントなど、特定のリクエストを直接送信する必要がある場合に便利です。

リクエストごとの形式

async with aiohttp.ClientSession() as session:
    async with session.get("https://api.ipify.org?format=json", proxy=PROXY) as resp:
        print(await resp.json())

プールマネージャーがタスクごとに異なるスティッキーセッションを割り当てる場合など、プロキシが呼び出しごとに異なる場合にこれを使用します。

認証:URL 認証情報または proxy_headers

プロキシの認証情報を送信するには、2 つのサポートされている方法と、古いチュートリアルではまだ見かけるが非推奨となっている方法が 1 つあります。

方法例ステータス
プロキシ URL 内の認証情報proxy="http://user:pass@host:9000"サポート対象
proxy_headers と encode_basic_authproxy_headers={"Proxy-Authorization": aiohttp.encode_basic_auth("user", "pass")}サポート対象、推奨
proxy_auth=aiohttp.BasicAuth("user", "pass")3.14 以降非推奨、4.0 で削除

URL形式が最も短いです。ヘッダー形式では、シークレットがURLに含まれないため、プロキシURLがログや例外メッセージに記録される場合に重要です。aiohttp.encode_basic_authは、あらかじめ作成されたBasic ...文字列を返します:

import asyncio
import os
import aiohttp

PROXY = "http://premium-residential.geonode.com:9000"

async def main() -> None:
    proxy_headers = {
        "Proxy-Authorization": aiohttp.encode_basic_auth(
            os.environ["GEONODE_USER"], os.environ["GEONODE_PASS"]
        )
    }
    async with aiohttp.ClientSession(proxy=PROXY, proxy_headers=proxy_headers) as session:
        async with session.get("https://api.ipify.org?format=json") as resp:
            print(await resp.json())

asyncio.run(main())

コードベースでまだ proxy_auth=aiohttp.BasicAuth(...) を使用している場合、3.14 では DeprecationWarning を使用することで動作します。今すぐ移行してください。このパラメータとクラスは aiohttp 4.0 で削除され、置き換えには 1 行の変更で済みます。

asyncio.gather と Semaphore を用いた並列リクエスト

aiohttp を使用する理由は並行処理のためですが、並行処理がプロキシを機能不全に陥らせる理由は、デフォルトではそれを制限する仕組みがないからです。 asyncio.gatherで1,000以上のコルーチンがあると、1,000の接続を開こうとします。コネクタはこれを100に制限するため、残りはaiohttp内でキューに蓄積されます。これは何もしないよりはましですが、ペース制御を行うことはできません。 Semaphore を使用すると、その制限が明示され、コネクタとは独立して調整できるようになります。

import asyncio
import aiohttp

PROXY = "http://USERNAME:PASSWORD@premium-residential.geonode.com:9000"
CONCURRENCY = 20

async def fetch(session: aiohttp.ClientSession, sem: asyncio.Semaphore, url: str) -> tuple[str, int]:
    async with sem:
        async with session.get(url) as resp:
            await resp.read()
            return url, resp.status

async def main() -> None:
    urls = [f"https://httpbin.org/get?i={i}" for i in range(100)]
    sem = asyncio.Semaphore(CONCURRENCY)
    connector = aiohttp.TCPConnector(limit=CONCURRENCY, ttl_dns_cache=300)
    timeout = aiohttp.ClientTimeout(total=30, connect=10, sock_read=20)
    async with aiohttp.ClientSession(
        proxy=PROXY, connector=connector, timeout=timeout
    ) as session:
        results = await asyncio.gather(
            *(fetch(session, sem, u) for u in urls), return_exceptions=True
        )
    ok = sum(1 for r in results if not isinstance(r, Exception))
    print(f"{ok}/{len(urls)} succeeded")

asyncio.run(main())

return_exceptions=Trueは、1つのリクエストが失敗しただけでバッチ全体がキャンセルされるのを防ぎます。セマフォをコネクタの上限に合わせて設定することで、どちらの側も、相手が処理しきれない作業を黙ってキューに溜め込むことがないようにします。

TCPConnector: limit および limit_per_host

このコネクタはソケットプールを管理しています。aiohttp プロキシトラフィックに関しては、2 つのパラメータが重要です。

limit は同時接続の総数で、デフォルトは 100 です。 limit_per_host は、1 つの (host, port, is_ssl) トリプルに対する接続数を制限するもので、デフォルトは 0(無制限)です。プロキシを経由する場合、すべてのターゲットへのリクエストはプロキシのホストとポートを共有するため、ホストごとの上限はゲートウェイに適用され、宛先サイトには適用されません。 純粋なプロキシワークロードの場合は、limit を目的の同時接続数に設定し、limit_per_host は 0 のままにします。limit_per_host は、同じセッションがオリジンにも直接通信を行い、それらを保護したい場合にのみ設定してください。

connector = aiohttp.TCPConnector(limit=30, limit_per_host=10, ttl_dns_cache=300)

ttl_dns_cache=300 を設定すると、ゲートウェイの DNS 応答がデフォルトの 10 秒ではなく 5 分間保持されるため、ほとんどの新規接続においてリゾルバーの往復通信が省略されます。セッションはコネクタの所有権を取得し、終了時にそれを閉じます。connector_owner=False は、意図的に 1 つのコネクタを複数のセッションで共有する場合にのみ指定してください。

デフォルトの5分ではなくClientTimeout

aiohttpのデフォルトの合計タイムアウトは300秒です。これは直接リクエストには十分な長さですが、プロキシ経由のトラフィックには長すぎます。プロキシ経由の場合、住宅用出口サーバーの応答が停滞した際は速やかに失敗と判定され、別のIPアドレスで再試行できるようにする必要があります。

timeout = aiohttp.ClientTimeout(total=30, connect=10, sock_read=20)

total は、リダイレクトやボディのダウンロードを含めたリクエスト全体に制限を設けます。connect は、プールからの接続取得に加え、TCP およびプロキシのハンドシェイクにかかる時間に制限を設けます。 sock_readは、連続するバイト間の間隔を制限するもので、接続を受け入れた後にデータを少しずつ送信してくるターゲットを捕捉します。このオブジェクトをセッションに渡すとデフォルト値が適用され、単一のリクエストに渡すとその値が上書きされます。有効期限が切れると、後述するasyncio.TimeoutErrorが発生します。

trust_env と環境プロキシ

requestsとは異なり、aiohttpは、trust_env=Trueで明示的に有効にしない限り、HTTP_PROXY、HTTPS_PROXY、およびNO_PROXYを無視します。 有効化されると、urllib.request.getproxies()を使用し、no_proxyを尊重し、また存在する場合、~/.netrcからプロキシの認証情報を読み取ります。

async with aiohttp.ClientSession(trust_env=True) as session:
    async with session.get("https://api.ipify.org?format=json") as resp:
        print(await resp.json())

常に特定のゲートウェイを使用する必要があるスクレイパーの場合は、trust_envを無効にしたまま、proxy=を明示的に設定してください。そうすることで、CIランナー上の企業プロキシがルーティングを乗っ取ることを防ぎ、no_proxyがターゲットを密かに除外することを防ぐことができます。

SOCKS5 と aiohttp-socks

aiohttp には SOCKS サポートが組み込まれていないため、SOCKS5 経由の aiohttp プロキシにはサードパーティ製のコネクタが必要です。 aiohttp-socks パッケージには、TCPConnector に代わる ProxyConnector が含まれており、SOCKS ハンドシェイクを処理します。 これを pip install aiohttp-socks とともにインストールしてください。Geonode の SOCKS5 によるローテーションポートは 11000 です:

import asyncio
import aiohttp
from aiohttp_socks import ProxyConnector

SOCKS_URL = "socks5://USERNAME:PASSWORD@premium-residential.geonode.com:11000"

async def main() -> None:
    connector = ProxyConnector.from_url(SOCKS_URL)
    async with aiohttp.ClientSession(connector=connector) as session:
        async with session.get("https://api.ipify.org?format=json") as resp:
            print(await resp.json())

asyncio.run(main())

セッション上で ProxyConnector と proxy= を組み合わせないでください。コネクタがすでにすべてをルーティングしています。ProxyConnector は、TCPConnector と同じ limit および limit_per_host キーワードを受け付けます。

TLS-in-TLS の制限

aiohttp は HTTP プロキシと通信し、CONNECT を使用して https:// ターゲットへの接続をプロキシ経由でトンネル化します。 ただし、プロキシ自体(つまり https:// プロキシ URL)への TLS 経由の接続は確実に実行されません。これは、TLS-in-TLS が asyncio トランスポートのサポートに依存しており、そのサポート状況が Python の各リリース間で異なっているためです。ドキュメントには、これを前提とすべきではないことが明記されています。 実際には、Geonode にとっては問題になりません。 ゲートウェイには9000/10000ポートのプレーンHTTP経由でアクセスされ、https://のトラフィックはCONNECTトンネル内で依然としてエンドツーエンドで暗号化されているため、プロキシURLスキーマはhttp://のままにしておいてください。

複数ステップのフローにおけるスティッキーセッション

ローテーションポートでは、接続ごとに新しいIPアドレスが割り当てられます。ログインフローやページネーションされた一覧表示では、数分間同じIPアドレスが必要となります。Geonodeでは、HTTP(S)スティッキーポート10000を介して、ユーザー名を通じて以下のIPアドレスを提供します:

import secrets
import aiohttp

def sticky_proxy(country: str = "us", lifetime: int = 600) -> str:
    session_id = secrets.token_hex(4)  # 8 alphanumeric characters
    user = f"USERNAME-country-{country}-session-{session_id}-lifetime-{lifetime}"
    return f"http://{user}:PASSWORD@premium-residential.geonode.com:10000"

session = aiohttp.ClientSession(proxy=sticky_proxy())

有効期間は180秒から86400秒の範囲です。州および都市のターゲティングを組み合わせることはできず、asnでは国の指定が必要です。新しいセッションIDを生成することで、オンデマンドでローテーションを行うことができます。

診断:各例外の意味

例外意味一般的な対処法
aiohttp.ClientProxyConnectionErrorプロキシホストへのTCP接続を開くことができませんでしたホスト名、ポート、ファイアウォールを確認してください。connectのタイムアウト値を引き上げてください
aiohttp.ClientHttpProxyError および .status == 407CONNECT 中にプロキシが認証情報を拒否したユーザー名またはパスワードを修正する;ユーザー名オプションの構文を確認する
aiohttp.ClientHttpProxyError(502/503 エラー)ゲートウェイがトンネルを確立できませんでした新しいセッション ID で再試行してください。ポートがプロトコルと一致しているか確認してください
asyncio.TimeoutErrorClientTimeoutフィールドの有効期限が切れた新しいIPアドレスで再試行してください;sock_readを調整してください
aiohttp.ClientConnectorError一般的な接続エラー(DNS、拒否).os_error を確認

ClientProxyConnectionError は ClientConnectorError のサブクラスであり、ClientHttpProxyError は ClientResponseError のサブクラスであるため、まず特定のものを捕捉してください。

import asyncio
import aiohttp

PROXY = "http://USERNAME:PASSWORD@premium-residential.geonode.com:9000"

async def fetch_ip(session: aiohttp.ClientSession) -> str | None:
    try:
        async with session.get("https://api.ipify.org?format=json") as resp:
            resp.raise_for_status()
            return (await resp.json())["ip"]
    except aiohttp.ClientHttpProxyError as e:
        if e.status == 407:
            print("proxy auth failed: check credentials")
        else:
            print("proxy returned", e.status)
    except aiohttp.ClientProxyConnectionError as e:
        print("cannot reach proxy:", e)
    except asyncio.TimeoutError:
        print("timed out through proxy")
    return None

async def main() -> None:
    timeout = aiohttp.ClientTimeout(total=30, connect=10, sock_read=20)
    async with aiohttp.ClientSession(proxy=PROXY, timeout=timeout) as session:
        print(await fetch_ip(session))
    await asyncio.sleep(0.25)  # let SSL transports close before the loop stops

asyncio.run(main())

なお、単純な http:// ターゲットの場合、プロキシからの 407 レスポンスは例外ではなく通常のレスポンスとして返されるため、その場合は resp.status も確認してください。

正常なシャットダウン

セッションが閉じられた際、asyncio はまだ基盤となる SSL トランスポートの解放処理を行っている可能性があります。イベントループが先に停止してしまうと、ResourceWarning: unclosed transport が発生します。 ドキュメントに記載されている対処法は、セッションの async with ブロックの後に短い一時停止を挿入することです。プレーン HTTP セッションの場合は await asyncio.sleep(0)、セッションが HTTPS リクエストを行った場合は await asyncio.sleep(0.25) となります。CONNECT トンネルを経由している場合、ほぼ常に HTTPS リクエストが行われています。上記の例にはこの処理が含まれています。

よくある間違い

リクエストごとにセッションを作成する

ClientSession は接続プールとクッキー・ジャーを保持しています。リクエストごとに新しいセッションを開くと、ゲートウェイへのキープアライブが機能しなくなり、プロキシとのハンドシェイク回数が増加し、呼び出しのたびにシャットダウン警告が発生します。ワーカーごとに 1 つのセッションを作成し、それを共有するようにしてください。

プロキシが唯一の「ホスト」であることを忘れている

limit_per_hostの値が小さい場合、プロキシ経由のすべてのリクエストがゲートウェイの単一の(host, port)エントリに対してカウントされるため、ホストごとの上限が5に設定されていると、スクレイパー全体の接続数が5に制限されてしまいます。 代わりに limit を設定するか、limit_per_host を 0 のままにしておいてください。

デフォルトのタイムアウトに依存している

5分という時間は十分に長いため、停止した住宅用出口サーバーが、この期間全体にわたってセマフォスロットを占有してしまいます。常に、connectおよびsock_readの値と共にClientTimeoutを指定してください。

依然として proxy_auth を渡している場合

現在は警告が表示されるものの動作しますが、バージョン 4.0 では動作しなくなります。これを proxy_headers={"Proxy-Authorization": aiohttp.encode_basic_auth(user, pw)} に置き換えるか、プロキシ URL に認証情報を指定してください。

次のステップ

上記のセッションでは、aiohttpプロキシトラフィックの認証、バインディング、および監視を行いました。あとは、そのトラフィックが通過するプロキシプールが残っています。Geonode 住宅用プロキシ は、ここで使用されているエンドポイントにサービスを提供しています: 250万のIPアドレスがオンラインで利用可能で、195カ国以上で毎月3,000万~5,000万のユニークIPアドレスがローテーションされます。ユーザー名による国、州、都市、ASNのターゲティングが可能で、HTTP、HTTPS、およびSOCKS5経由で最大24時間のスティッキーセッションが利用できます。 料金はボリュームディスカウント適用で1GBあたり0.27ドルから、エントリープランは10GBで月額7.92ドル、3日間のトライアルは5ドルで契約不要です。 ウェブスクレイピングおよび価格インテリジェンスのユースケースでは、チームがこのコネクタの設定をリトライおよびスケジューリングロジックと組み合わせて活用する方法を紹介しています。

よくある質問

セッション全体に対して aiohttp プロキシを設定するにはどうすればよいですか?

ClientSessionのコンストラクタ(aiohttp.ClientSession(proxy="http://user:pass@host:port"))にproxy=を渡してください。そのセッションからのすべてのリクエストで、呼び出し側がproxy=を別の値で上書きしない限り、このプロキシが使用されます。 セッションレベルのプロキシは aiohttp 3.11 以降で利用可能です。それ以前のリリースでは、代わりに各 get() または post() の呼び出しで proxy= を渡す必要があります。

aiohttp で proxy_auth が非推奨となった理由と、それに代わるものは何ですか?

aiohttp 3.14 以降、proxy_auth パラメータおよび aiohttp.BasicAuth クラスは非推奨となり、4.0 で削除される予定です。 これに代わるのは、単純なヘッダーである proxy_headers={"Proxy-Authorization": aiohttp.encode_basic_auth("user", "pass")} です。これはヘルパークラスを使用せずに同じ Base64 エンコードを行い、リクエスト単位およびセッションレベルの両方で機能します。

aiohttp は HTTPS プロキシや SOCKS5 をサポートしていますか?

aiohttpはネイティブにHTTPプロキシと通信し、CONNECTを使用してhttps://のターゲットをプロキシ経由でトンネル転送します。TLS経由でプロキシ自体に接続する(TLS-in-TLS)機能は、Pythonビルドにおけるasyncioのサポート状況に依存するため、これを当てにすべきではありません。 SOCKS4およびSOCKS5を利用するには、サードパーティ製のaiohttp-socksパッケージとそのProxyConnectorが必要です。

aiohttp プロキシを介して実行できる同時リクエスト数はいくつですか?

TCPConnector では、デフォルトで 100 の同時接続が可能であり、ホストごとの上限はありません。 プロキシ経由では、すべてのリクエストが同じホストとポートを共有するため、limitを明示的に設定し、直接呼び出しも行う場合はlimit_per_hostを追加し、キューに追加されたタスクがすべて同時にソケットを開かないように、リクエストのコルーチンをasyncio.Semaphoreでラップしてください。

プロキシ経由の aiohttp リクエストが失敗せずにハングしてしまうのはなぜですか?

ClientSession のデフォルトの合計タイムアウトは 5 分間であり、一般家庭向けのエグジットサーバーが応答を停止すると、ハングしているように見えます。 セッションまたはリクエストに aiohttp.ClientTimeout(total=30, connect=10, sock_read=20) を渡すと、停滞が asyncio.TimeoutError として速やかに表面化するため、待機せずに新しいセッション ID を使用して再試行できます。

スクレイパーを住宅用プロキシ経由でルーティングする場合の実用的な制限は何ですか?

プロキシは送信元 IP アドレスを変更するだけです。 robots.txt、サイトの利用規約、レート制限を無視する権限を与えるものではなく、個人データの収集を合法にするものでもありません。同時実行数を妥当な範囲に抑え、429エラーが発生した場合はバックオフを行い、Geonodeのスティッキーセッションは、IPが解放されるまで最大24時間しか持続しないことを覚えておいてください。