ウェブサイトをパースしたり、Pythonでプロキシを介してリクエストを自動化したりする場合、遅かれ早かれ、次の3つの古典的なエラーに直面することになります: ProxyError,
SSLError: WRONG_VERSION_NUMBER と HTTP
407 Proxy Authentication Required。この記事では、それぞれの原因を分析し、修正のための動作コードを示します。
requestsがプロキシとどのように動作するか:基本構文
requestsライブラリは、プロキシを辞書
proxiesを介して受け入れます。ここで、キーはリクエストのスキーム
(http と https)、
値はオプションの認証を含むプロキシサーバーのURLです。
import requests
proxies = {
"http": "http://user:password@proxy_host:port",
"https": "http://user:password@proxy_host:port",
}
response = requests.get("https://httpbin.org/ip", proxies=proxies, timeout=10)
print(response.json())
注意してください:HTTPSリクエストの場合でも、プロキシのスキームは通常
http://として指定されます。これは、ほとんどのプロキシサーバーが
HTTPプロトコルを介してCONNECTトンネルを受け入れるためであり、SSLでラップするわけではありません。プロキシがこれをサポートしていない場合に
https://を指定すると、SSLErrorが発生します。その分析は後述します。
SOCKS5プロキシを使用する場合は、追加のパッケージが必要です:
pip install requests[socks]
# そして、proxiesの構文が変更されます:
proxies = {
"http": "socks5://user:password@proxy_host:port",
"https": "socks5://user:password@proxy_host:port",
}
ProxyError:原因と解決策
requests.exceptions.ProxyErrorは、プロキシサーバーとの接続を確立しようとしたときに発生する低レベルのネットワーク例外をラップしたものです。最も一般的なシナリオは4つです。
1. プロキシが利用できないか、アドレス/ポートが不正確です
requests.exceptions.ProxyError: HTTPSConnectionPool(host='example.com', port=443):
Max retries exceeded with url: / (Caused by ProxyError('Cannot connect to proxy.',
ConnectionResetError(104, 'Connection reset by peer')))
ホストとポートが正しく入力されていることを確認し、プロキシサーバーが実際に稼働しているか確認してください。最も迅速な診断方法は、curlを介して接続を試みることです:
curl -x http://user:password@proxy_host:port https://httpbin.org/ip -v
2. プロキシがあなたのリクエストをブロックしました(IPまたはUser-Agentによるバン)
データセンターのプロキシは、ターゲットサイトによってバンされることがよくあります。これは、彼らのIP範囲がアンチフロードシステムに知られているためです。特定のサイト(例えば、Wildberries、Ozon、またはソーシャルメディア)にアクセスする際にエラーが安定して発生する場合、それはプロキシ自体の問題ではなく、その評判の問題です。この場合、レジデンシャルプロキシに切り替えると役立ちます。これらは実際のユーザーのIPを使用し、アンチフロードシステムによるブロックにかかることが少なくなります。
3. 同時接続の制限を超えました
マルチスレッドパースを実行し、同じプロキシを介して数百の接続を開くと、サーバーが余分な接続をリセットし始めることがあります。解決策は、requests.adapters.HTTPAdapterを使用して接続プールを利用し、pool_maxsizeと
max_retriesを設定することです:
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
session = requests.Session()
retries = Retry(total=3, backoff_factor=1, status_forcelist=[502, 503, 504])
adapter = HTTPAdapter(max_retries=retries, pool_connections=20, pool_maxsize=20)
session.mount("http://", adapter)
session.mount("https://", adapter)
response = session.get("https://httpbin.org/ip", proxies=proxies, timeout=10)
4. プロキシサーバー側のタイムアウト
プロキシが設定されたタイムアウトを超えて「ハング」している場合、ProxyErrorが発生し、ReadTimeoutErrorでラップされます。タイムアウトを合理的な値(通常は15〜30秒で十分)に増やし、例外処理を追加してください:
try:
response = requests.get(url, proxies=proxies, timeout=20)
except requests.exceptions.ProxyError as e:
print(f"プロキシに問題があります: {e}")
except requests.exceptions.Timeout:
print("プロキシが指定された時間内に応答しませんでした")
SSLError WRONG_VERSION_NUMBER:問題の分析
このエラーは、一見すると最も理解しにくいものの一つですが、実際にはほとんど常に同じ原因があります。
requests.exceptions.SSLError: HTTPSConnectionPool(host='example.com', port=443):
Max retries exceeded with url: / (Caused by SSLError(SSLError(1, '[SSL:
WRONG_VERSION_NUMBER] wrong version number (_ssl.c:1123)')))
WRONG_VERSION_NUMBERは、クライアントが通常のHTTPトラフィック(暗号化なし)を期待している場所でSSL/TLSハンドシェイクを試みたことを意味します。最も一般的な原因は、辞書proxiesにおいてプロキシのスキームが不正確に指定されていることです。
よくあるエラー:プロキシサーバーのアドレスにhttps://を指定する代わりにhttp://を指定すること。ほとんどのHTTP(S)プロキシは、HTTPSトラフィックをCONNECTメカニズムを介してプロキシする場合でも、通常のHTTPで接続を受け入れます。
# 誤り — WRONG_VERSION_NUMBERを引き起こす
proxies = {
"http": "https://user:password@proxy_host:port",
"https": "https://user:password@proxy_host:port",
}
# 正しい
proxies = {
"http": "http://user:password@proxy_host:port",
"https": "http://user:password@proxy_host:port",
}
第二の一般的な原因は、物理的にTLSをサポートしていないポートです。たとえば、プロキシプロバイダーがHTTPトラフィック専用にポート8080を割り当てた場合、TLS接続を直接(CONNECTをバイパスして)確立しようとすると、サーバーはTLSハンドシェイクの代わりに通常のテキストで応答し、クライアントライブラリOpenSSLはこのエラーを発生させます。プロキシプロバイダーのドキュメントで、各ポートで利用可能なポートとプロトコル(HTTP/HTTPS/SOCKS5)を確認してください。
第三のシナリオは、socketやサードパーティのライブラリを使用して、標準のrequestsメカニズムをバイパスしてプロキシに直接TCP接続を確立している場合です。この場合、TLSラッパーがプロキシとの接続ではなく、実際のターゲットサイトにのみ適用されることを確認してください。
407 Proxy Authentication Required:認証
ステータスコード407は、プロキシサーバー用の標準401の類似です。これは、サーバーが認証を要求していることを意味し、あなたがそれを提供しなかったか、誤った形式で提供したことを示します。
方法1:URLに直接ログインとパスワードを含める
proxies = {
"http": "http://login:password@proxy_host:port",
"https": "http://login:password@proxy_host:port",
}
response = requests.get("https://httpbin.org/ip", proxies=proxies)
この方法は最も簡単ですが、ログインやパスワードに特殊文字(例えば、@,
:, /)が含まれている場合、urllib.parse.quoteを使用してエスケープする必要があります:
from urllib.parse import quote
login = quote("user@domain")
password = quote("p@ssw:ord!")
proxy_url = f"http://{login}:{password}@proxy_host:port"
proxies = {"http": proxy_url, "https": proxy_url}
方法2:Proxy-Authorizationヘッダーを手動で設定する
プロキシプロバイダーが非標準の認証スキームを使用している場合や、より多くの制御が必要な場合は、base64エンコーディングを介してヘッダーを手動で構築できます:
import base64
import requests
login = "user"
password = "password"
credentials = base64.b64encode(f"{login}:{password}".encode()).decode()
headers = {
"Proxy-Authorization": f"Basic {credentials}"
}
proxies = {
"http": "http://proxy_host:port",
"https": "http://proxy_host:port",
}
response = requests.get(
"https://httpbin.org/ip",
proxies=proxies,
headers=headers,
timeout=10
)
注意:一部のプロキシプロバイダーは、認証をログイン/パスワードにではなく、特定の静的IPアドレス(ホワイトリスト)に関連付けています。IPを変更すると(たとえば、VPNを介して作業する場合や、プロバイダーの動的IPを使用する場合)、407エラーが正しいクレデンシャルで発生する可能性があります。これは、現在の外部IPがプロキシ側のホワイトリストに登録されていないためです。解決策は、プロバイダーの個人アカウントにログインして許可されたIPのリストを更新するか、IPホワイトリストの代わりにログイン/パスワードによる認証に切り替えることです。
リトライロジックと例外処理
実際には、プロキシのエラーは一時的なものではないことが多いです。特に大量のパースを行う場合。コードの正しいアーキテクチャは、各エラーを個別にログ記録し、遅延を伴う再試行を考慮する必要があります。
import time
import requests
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def fetch_with_retry(url, proxies, max_retries=3, backoff=2):
for attempt in range(1, max_retries + 1):
try:
response = requests.get(url, proxies=proxies, timeout=15)
response.raise_for_status()
return response
except requests.exceptions.ProxyError as e:
logger.warning(f"[試行 {attempt}] ProxyError: {e}")
except requests.exceptions.SSLError as e:
logger.warning(f"[試行 {attempt}] SSLError: {e}")
except requests.exceptions.HTTPError as e:
if response.status_code == 407:
logger.error("407: ログイン/パスワードまたはプロキシのIPホワイトリストを確認してください")
break
logger.warning(f"[試行 {attempt}] HTTPError: {e}")
except requests.exceptions.Timeout:
logger.warning(f"[試行 {attempt}] 接続タイムアウト")
time.sleep(backoff * attempt)
raise RuntimeError(f"{url}を{max_retries}回の試行の後に取得できませんでした")
このアプローチは、デバッグの時間を節約します。スクリプトが最初のネットワークの問題で失敗する代わりに、各試行で特定のエラータイプを示す明確なログが得られます。
プロキシのローテーションとバン対策
ProxyErrorと407は、コードの設定に関連していることが多いですが、大量のバンや特定のドメインでの接続の安定した拒否は、ほぼ常にIPプールの質の問題を示しています。マーケットプレイス(Wildberries、Ozon、Avito)やソーシャルメディアのパースなどのタスクには、各リクエストまたは指定された間隔でのIPローテーションを推奨します。
import random
proxy_pool = [
"http://user:[email protected]:8000",
"http://user:[email protected]:8000",
"http://user:[email protected]:8000",
]
def get_random_proxy():
proxy = random.choice(proxy_pool)
return {"http": proxy, "https": proxy}
response = requests.get(url, proxies=get_random_proxy(), timeout=15)
プロダクションパーサーの場合、プロバイダー側で自動ローテーションを行うプロキシを使用する方が便利です。これにより、IPリストを自分で管理する必要がなくなり、ローテーションが1つのエンドポイントを介して透過的に行われます。この場合、レジデンシャルプロキシは、厳しい検出システムを持つサイトでのアンチボット保護を回避するのに特に適しています。また、データセンターのプロキシは、リクエストの速度が重要であり、ターゲットサイトがデータセンターの範囲を積極的にブロックしない場合に良い選択です。
| プロキシの種類 | 速度 | バンのリスク | タスク |
|---|---|---|---|
| データセンターのプロキシ | 高い | 中〜高 | オープンAPIの高速パース |
| レジデンシャルプロキシ | 中 | 低い | マーケットプレイス、ソーシャルメディアのパース |
| モバイルプロキシ | 中〜低 | 最小 | 厳しいアンチボットシステムとの作業 |
セッション、タイムアウト、SSL検証
長期間実行されるスクリプト(例えば、ループパーサー)には、個別の呼び出しの代わりに
requests.Session()を使用することをお勧めします。セッションはTCP接続を再利用し、プロキシサーバーへの負荷を軽減し、大量のリクエストでのProxyErrorの数を減らします。
session = requests.Session()
session.proxies.update({
"http": "http://user:password@proxy_host:port",
"https": "http://user:password@proxy_host:port",
})
for url in urls_list:
try:
resp = session.get(url, timeout=15)
print(resp.status_code, url)
except requests.exceptions.RequestException as e:
print(f"{url}でエラーが発生しました: {e}")
verifyパラメータについても言及する価値があります。プロキシが自らSSL証明書を置き換える場合(デバッグ用のMITMプロキシや一部の企業プロキシなど)、requestsは証明書の不一致に対して警告を出すことがあります。検証を完全に無効にすること(verify=False)は、テスト環境でのみ行うべきです。プロダクション環境では、「中間者攻撃」のリスクを生じさせます:
# ローカルデバッグ用のみ!
response = requests.get(url, proxies=proxies, verify=False, timeout=15)
# 正しい方法は、プロキシの信頼された証明書へのパスを指定することです
response = requests.get(url, proxies=proxies, verify="/path/to/proxy_cert.pem")
エラーチェックリスト
スクリプトがプロキシエラーで失敗する場合は、このリストを上から下まで確認してください。90%のケースで原因が最初の3つのポイントに見つかります。
- 辞書
proxiesのスキームを確認してください — ほとんどの場合、http://である必要があります。HTTPSリクエストの場合でも。 - コード側の問題を除外するために、
curl -xを介してプロキシを直接テストしてください。 - ログインとパスワードにURL内でエスケープされていない特殊文字が含まれていないことを確認してください。
- プロキシが特定のIP(ホワイトリスト)にバインドされていないことを確認してください。ログイン/パスワードによる認証の代わりに。
- プロキシが機能していないと見なす前に、
timeoutを15〜30秒に増やしてください。 - 特定のサイトでのみエラーが発生する場合 — 問題はIPの評判にあり、コードにはありません。
- 単発の試行の代わりに、指数関数的な遅延を伴うリトライロジックを追加してください。
- 大量のパースを行う場合は、すべてのリクエストに対して1つの静的プロキシを使用するのではなく、IPのローテーションを使用してください。
結論
Python requestsにおけるプロキシエラーのほとんどは、次の3つの要因に起因します:プロキシのURLにおける不正確なスキーム
(httpではなくhttps)、認証の欠如または不正確な形式、そしてIPプールの質です。ProxyErrorは通常、接続の問題やIPのバンを示し、SSLError WRONG_VERSION_NUMBERはほぼ常に不正確なプロキシスキームを示し、407はログイン/パスワードまたはホワイトリストの問題を示します。
マーケットプレイスやソーシャルメディアのパース中にバンや407エラーに頻繁に直面する場合は、コードだけでなくプロキシのソース自体を見直す必要があります。安定したパースを低いブロック率で行うためには、レジデンシャルプロキシを試すことをお勧めします。これらは実際のIPアドレスを使用し、保護されたサイトでのProxyErrorを大幅に減少させます。もし速度とリクエストの量が優先され、複雑なアンチボットシステムを回避する必要がない場合は、データセンターのプロキシを検討してください。これらは簡単なパースタスクに対して安価で高速です。