RequestsでのProxyError、SSLError、ConnectTimeoutの修正方法

requests.exceptions.ProxyErrorは原因ではなく症状です。例外階層を解読し、各トレースバックを実際の修正に対応させ、失敗を分類して死んだ出口を回避するヘルスチェック関数を紹介します。

requests.exceptions.ProxyErrorはPythonで最も役に立たないエラーメッセージの一つです:死んだプロキシ、間違った認証情報、悪いスキーム、過負荷の出口、ファイアウォールブロックなど、ほぼ同じトレースバックで発生します。迅速に修正するコツは、ProxyErrorが根本的な原因ではなくカテゴリであることを知ることです。Requestsのソースでは、ProxyErrorSSLErrorConnectTimeoutはすべてConnectionErrorのサブクラスであり、それぞれがリクエストライフサイクルの特定の段階で発生します。段階を読むことで原因を読むことができます。このガイドは例外階層を解読し、一般的なトレースバックを実際の修正に対応させ、失敗を分類して自動的に死にかけた出口を回避するヘルスチェック関数を提供します。

Requestsの例外階層

Requestsのすべてのプロキシ関連エラーはRequestExceptionから派生しています。デバッグに役立つのはConnectionErrorのブランチで、実際に遭遇する3つの例外がその下にあります:

最初の3つが親を共有しているため、except requests.exceptions.ConnectionErrorでそれらすべてをキャッチして再試行ロジックを実行できますが、個々のサブクラスをキャッチすることで、なぜ失敗したのかをログに記録できます。これらのほとんどを回避するクリーンなセットアップは、Python Requestsプロキシガイドでカバーされています。この投稿は、トレースバックがすでに画面に表示されている場合に何をすべきかについてです。

1つの習慣が、単一の修正よりも多くの時間を節約します:トレースバックを下から上に読むことです。Requestsは基礎となるurllib3の失敗をラップしているため、上部のフレームはどこで呼び出しが行われたかを説明し、下部のフレームは何が間違っていたかを説明します。あなたが欲しい行は最も内側のCaused by句であり、それは外側のProxyErrorまたはConnectionErrorが単に再発している具体的な失敗(接続拒否、証明書の不一致、解析されたポートエラー)を示しています。その行を読むことができれば、このガイドの残りは参照表です。

ProxyErrorの前の古典的なValueError

最も検索されているプロキシトレースバックは実際にはProxyErrorではなく、ValueError: invalid literal for int() with base 10です。これはurllib3の深い部分でスローされます。スキームなしでプロキシ値に認証情報を埋め込むと、パーサーがコロンの後のテキストをポート番号として読み取るためです。

# Broken — no scheme, so 'pass@host' is parsed as host:port
proxies = {"https": "user:pass@45.11.22.33:8000"}
# -> ValueError: invalid literal for int() with base 10: 'pass@45.11.22.33'

# Fixed — scheme in front, password URL-encoded if it has @ : or /
from urllib.parse import quote
pw = quote("p@ss:word", safe="")
proxies = {
    "http":  f"http://user:{pw}@gate.quantumproxies.io:PORT",
    "https": f"http://user:{pw}@gate.quantumproxies.io:PORT",
}
各Python requests例外がリクエストライフサイクル全体でどこで発生するかを示すフローダイアグラム:MissingSchema、ProxyError、SSLError、ReadTimeout
ライフサイクルステージが原因を示します:ビルド時のタイプミスはMissingSchema、プロキシ接続の失敗はProxyError、TLSの失敗はSSLError、遅い出口はReadTimeoutです。

失敗を分類するプロキシヘルスチェック

推測する代わりに、各例外タイプをキャッチして平易な英語の判決に変えます。この関数は成功時に出口IPを返し、失敗時にラベル付きの理由を返します。スクレイプの前にプロキシが生きていることを確認するために使用します。これは住宅プロキシを含む任意の認証ゲートウェイに対して機能します:

import requests

def check_proxy(proxies, url="https://httpbin.org/ip", timeout=(5, 20)):
    try:
        r = requests.get(url, proxies=proxies, timeout=timeout)
        r.raise_for_status()
        return True, r.json().get("origin")
    except requests.exceptions.ProxyError as e:
        return False, f"proxy unreachable or auth rejected: {e}"
    except requests.exceptions.SSLError as e:
        return False, f"TLS failed (https:// in the https key?): {e}"
    except requests.exceptions.ConnectTimeout:
        return False, "proxy did not answer within the connect window"
    except requests.exceptions.ReadTimeout:
        return False, "target too slow after connect (exit quality)"
    except requests.exceptions.RequestException as e:
        return False, f"other request error: {e}"

ok, detail = check_proxy(proxies)
print("OK" if ok else "FAIL", detail)

curlが動作するがPythonがProxyErrorをスローする場合

同じ認証情報がcurlで成功し、PythonでProxyErrorを引き起こす場合、環境変数が辞書を上書きしていることがほとんどです。RequestsはシェルからHTTP_PROXYHTTPS_PROXYNO_PROXYを読み取り、古い企業の値がすべての呼び出しを静かに再ルーティングします。session.proxiesを印刷して実際に使用されているものを確認し、環境のルックアップを完全に無効にします:

import requests

session = requests.Session()
session.trust_env = False          # ignore HTTP_PROXY / HTTPS_PROXY from the shell
session.proxies = {
    "http":  "http://USER:PASS@gate.quantumproxies.io:PORT",
    "https": "http://USER:PASS@gate.quantumproxies.io:PORT",
}
print(session.get("https://httpbin.org/ip", timeout=(5, 20)).json())

正しい認証情報を持っていても生き残る407は、未登録のアドレスから呼び出されたIPホワイトリストプランを示しています。原因の完全なリストは407 Proxy Authentication Requiredガイドにあります。

断続的なProxyError:再起動せずに回転

厄介なケースは、20分間動作し、その後ProxyErrorをスローし、再び動作するコードです。これはスクリプトのバグではなく、単一の出口IPが途中で死んだり、レート制限されたりしていることです。修正は再試行と回転です:呼び出しをラップし、ConnectionErrorをキャッチし、回転ゲートウェイが次の試行で新しいIPを提供するようにします。回転プロキシを通じて、すべての再試行は異なる出口を通過するため、1つの死んだアドレスがリクエストを2回失敗させることはありません:

import requests

def get_with_rotation(url, proxies, attempts=4):
    last = None
    for _ in range(attempts):
        try:
            r = requests.get(url, proxies=proxies, timeout=(5, 20))
            if r.status_code not in (429, 500, 502, 503, 504):
                return r
            last = r.status_code
        except requests.exceptions.ConnectionError as e:  # Proxy/SSL/ConnectTimeout
            last = e
    raise RuntimeError(f"failed after {attempts} attempts: {last}")

ProxyErrorが多くの新しいIPにわたって続く場合、問題はプールからターゲットに移動しています:切断されているのではなく、ブロックされています。それは別の戦いです — ペーシング、ヘッダー、セッションの衛生のためのアンチバンチェックリストを参照してください。

もう1つ覚えておくべき区別があります。それは、どのように対応するかを変えるからです。ProxyErrorまたはConnectTimeoutはリクエストが完了しなかったことを意味するため、再試行は安全です。POSTであっても、何も起こりませんでした。対照的に、ReadTimeoutはターゲットがリクエストを受信し、単に応答が遅すぎたことを意味します。非冪等な書き込みを再試行すると二重送信される可能性があります。再試行ループを構築する際には、接続段階の失敗は自由に再試行可能であり、読み取り段階の失敗はGETとHEADに対してのみ再試行可能と扱います。この単一のルールは、フレークなプロキシが1つのチェックアウトを3つに変える微妙なバグを防ぎます。

一般的なPython requestsプロキシエラーメッセージをその1行の修正に対応させるチェックリスト:プロキシに接続できないから断続的な失敗まで
メッセージを読み、修正を適用します:悪いホスト、スキームの欠如、間違ったTLSキー、エンコードされていないパスワード、または死にかけた出口は、それぞれ単一の修正ステップに対応します。

よくある質問

requests.exceptions.ProxyError: cannot connect to proxyの原因は何ですか?

プロキシホストまたはポートが間違っている、プロキシがダウンしている、またはファイアウォールが接続をブロックしているため、リクエストが出る前に発生します。同じ認証情報を使用してcurl -xでエンドポイントを確認してください。curlも失敗する場合、プロキシは到達不能であり、curlが成功する場合、環境変数またはPython内の不正な辞書が原因です。

なぜProxyErrorはHTTPSConnectionPoolでラップされているのですか?

そのラッパーは、ターゲットに到達するために使用されたurllib3の接続プールを名前付けしているだけであり、内部にネストされた実際のメッセージの周りのノイズです。最も内側のCaused by句を読みます:Cannot connect to proxyは接続の失敗を意味し、同じプール内の407またはSSLメッセージは認証またはTLS/スキームの問題を示しています。

requestsが環境からプロキシを読み取るのを止める方法は?

session.trust_env = Falseをセッションに設定するか、trust_env=Falseを渡して、RequestsがHTTP_PROXYHTTPS_PROXYを無視するようにします。これは、あるシェルでプロキシが動作し、別のシェルでProxyErrorをスローする場合や、企業の変数がプロキシを使用するように設定されていないスクレイパーをハイジャックする場合の修正です。

ConnectTimeoutは再試行しても安全ですか?

はい — Requestsのドキュメントでは、ConnectTimeoutは再試行しても安全であると記載されています。リクエストがサーバーに到達しなかったため、副作用が発生することはありません。再試行してください。理想的には回転ゲートウェイを通じて、次の試行で異なる、より速い出口を使用します。ReadTimeoutは、非冪等なメソッド(POSTなど)で盲目的に再試行するのはリスクが高いです。

ProxyErrorを単一の障害として扱うのをやめ、ライフサイクルステージごとに読み始めると、修正は機械的になります:スキームのタイプミスはネットワークの前に発生し、接続の失敗は死んだプロキシを示し、SSLエラーは間違ったキーを示し、断続的な失敗は再起動ではなく回転を必要とします。クリーンな出口はそれらのほとんどを完全に取り除きます。

生き続ける住宅IPでスクレイプする