クイックアンサー
Pythonでレジデンシャルプロキシのローテーションはどう使うの?
requestsのhttpとhttpsにはプロキシとしてhttp://USERNAME:[email protected]:7777を、httpx 0.26+ではproxy=を、aiohttpではリクエストごとにproxy=を渡します。CrawlRelayのレジデンシャルプロキシは新規接続ごとにローテーションし、ユーザー名に含まれる-session-<id>でスティッキーになります。1GBあたり$1.50、50GB以上なら$0.50/GB。支払は暗号通貨で、KYC不要です。
Pythonセットアップの概要
| requests | proxies={"http": url, "https": url};urllib3Retry経由でリトライ可能 |
|---|---|
| httpx | httpx.Client(proxy=url);古いproxies=引数はhttpx 0.28で削除済み |
| aiohttp | session.get(url, proxy="http://USER:PASS@host:port")、認証情報はプロキシURL内で指定 |
| SOCKS5 | pip install "requests[socks]"またはhttpx[socks]、その後socks5h://をポート7778で使用 |
| リクエストごとに新しいIP | キープアライブ無効化:Connection: close、max_keepalive_connections=0またはforce_close=True |
| フロー単位で同じIP | ユーザー名に-session-<id>-ttl-<minutes>、最大30分間 |
| プロキシURLのスキーム | HTTPポートには常にhttp://を使います。HTTPSサイトでも同様です |
requestsでRetry-Afterに対応したリトライを使いながらプロキシを使うには?
Sessionにurllib3のRetryでHTTPAdapterをマウントします。接続エラーやリストにあるステータスコードでリトライし、指数関数的バックオフで待機し、429や503で送られてくるRetry-Afterヘッダーを尊重します。trust_env = Falseを設定して、残ったHTTPS_PROXY変数があなたのレジデンシャルプロキシを上書きできないようにしてください。
import os
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
USER = os.environ.get("CRAWLRELAY_USER", "USERNAME-country-us")
PASSWORD = os.environ.get("CRAWLRELAY_PASSWORD", "PASSWORD")
PROXY = "http://%s:%[email protected]:7777" % (USER, PASSWORD)
retry = Retry(
total=4,
connect=2,
backoff_factor=1.0, # waits of 0 s, 2 s, 4 s, 8 s (Retry-After wins on 429/503)
status_forcelist=(429, 502, 503, 504),
allowed_methods=frozenset({"GET", "HEAD"}),
respect_retry_after_header=True,
)
session = requests.Session()
session.trust_env = False # ignore HTTP(S)_PROXY from the environment
session.mount("http://", HTTPAdapter(max_retries=retry))
session.mount("https://", HTTPAdapter(max_retries=retry))
session.proxies = {"http": PROXY, "https": PROXY}
session.headers["User-Agent"] = "ExampleCrawler/1.0 (+https://example.com/crawler-info)"
session.headers["Connection"] = "close" # new connection per request, so usually a new exit IP
r = session.get("https://api.ipify.org", timeout=(10, 30))
print(r.status_code, r.text)ターゲットを絞ったプロキシURLを安全に作るには?
ユーザー名は関数で作り検証します。国コードのタイプミスやパスワードの未エンコード文字は407の認証エラーに見える問題を起こします。以下のヘルパーは定義された順序(USERNAME-country-{cc}-state-{state}-city-{city}-zip-{zip}-asn-{asn}-session-{id}-ttl-{minutes})を守り、未知のキーを拒否し、両方の認証情報をURLエンコードします。
import re
from urllib.parse import quote
ORDER = ("country", "state", "city", "zip", "asn", "session", "ttl")
VALUE = re.compile(r"^[a-z0-9_]+$")
def build_username(base, **params):
"""build_username("USERNAME", country="us", city="new_york", session="job1", ttl=30)"""
parts = [base]
for key in ORDER:
value = params.pop(key, None)
if value is None:
continue
value = str(value).lower()
if not VALUE.match(value):
raise ValueError("bad value for %s: %r" % (key, value))
if key == "ttl" and not 1 <= int(value) <= 30:
raise ValueError("ttl must be 1 to 30 minutes")
parts.append("%s-%s" % (key, value))
if params:
raise ValueError("unknown parameters: %s" % ", ".join(sorted(params)))
return "-".join(parts)
def proxy_url(username, password, socks=False):
scheme, port = ("socks5h", 7778) if socks else ("http", 7777)
return "%s://%s:%[email protected]:%d" % (
scheme, quote(username, safe=""), quote(password, safe=""), port)
print(proxy_url(build_username("USERNAME", country="us", state="ny", city="new_york", session="job1", ttl=20), "p@ss:word"))httpxでリクエストごとに新しいIPを取得するには?
keep-aliveをオフにします。httpxはデフォルトで接続プールを使い、再利用される接続は同じ出口IPをキープします。max_keepalive_connections=0を使うと、各リクエストはゲートウェイに新しい接続を開き、通常別のレジデンシャルプロキシIPから出ます。num_bytes_downloadedは実際に受信したバイト数を返し、トラフィック課金に近い指標です。
import httpx
PROXY = "http://USERNAME-country-gb:[email protected]:7777"
# No keep-alive: every request opens a new connection, so it usually gets a new exit IP
with httpx.Client(
proxy=PROXY,
timeout=httpx.Timeout(30.0, connect=10.0),
limits=httpx.Limits(max_keepalive_connections=0),
follow_redirects=True,
) as client:
for _ in range(3):
r = client.get("https://api.ipify.org")
print(r.text, r.num_bytes_downloaded, "bytes on the wire")asyncioで同時にスクレイピングする際に1サイトに負荷をかけ過ぎないには?
ホストごとに同時接続数を制限しましょう。全体で50接続の制限だと、URLリストがドメイン順の場合、50リクエストすべてが同じサイトに集中してしまいます。ホスト名ごとにセマフォを使うことで、各サイトの負荷を控えつつ全体のクロールは高速に保てます。ホストごとに2~4から始めて、上げる前に同時実行ガイドを読みましょう。
import asyncio
from collections import defaultdict
from urllib.parse import urlsplit
import httpx
PROXY = "http://USERNAME-country-us:[email protected]:7777"
PER_HOST = 4 # parallel requests per target host
host_slots = defaultdict(lambda: asyncio.Semaphore(PER_HOST))
async def fetch(client, url):
async with host_slots[urlsplit(url).hostname]:
try:
r = await client.get(url)
return url, r.status_code, r.num_bytes_downloaded
except httpx.HTTPError as exc:
return url, type(exc).__name__, 0
async def main(urls):
limits = httpx.Limits(max_connections=50, max_keepalive_connections=0)
timeout = httpx.Timeout(30.0, connect=10.0)
async with httpx.AsyncClient(proxy=PROXY, limits=limits, timeout=timeout) as client:
for result in await asyncio.gather(*(fetch(client, u) for u in urls)):
print(*result)
asyncio.run(main(["https://example.com/", "https://example.org/", "https://example.net/"]))aiohttpでプロキシを使うには?
各リクエストにゲートウェイをproxy=で渡し、認証情報はプロキシURLに入れます。これはaiohttp 3.xの全リリースで警告なく動作します。BasicAuthとproxy_auth=パラメータはaiohttp 3.14以降非推奨で4.0で削除予定です。TCPConnector(force_close=True)を使うとレスポンス後に接続を閉じるため、通常リクエストごとに新しい出口IPになります。使わないと接続を再利用し、一時的に同じIPが続きます。
import asyncio
import aiohttp
# Credentials go in the proxy URL; percent-encode a password that contains @ or :
PROXY = "http://USERNAME-country-fr:[email protected]:7777"
async def main():
# force_close=True: one connection per request, so each request usually gets a new exit IP
connector = aiohttp.TCPConnector(limit=20, force_close=True)
timeout = aiohttp.ClientTimeout(total=60, sock_connect=10)
async with aiohttp.ClientSession(connector=connector, timeout=timeout) as session:
for _ in range(3):
async with session.get("https://api.ipify.org", proxy=PROXY) as resp:
print(resp.status, await resp.text())
asyncio.run(main())複数ステップの処理で1つのIPを使い続けるには?
フローごとに1つのセッションIDを作り、ユーザー名に入れてフローより長いTTLを設定します。keep-aliveは問題ありません。同じ出口IPを使いたいからです。背後のレジデンシャルデバイスがオフラインになると、TTL終了前にIPが変わる可能性があるため、フローは最後のチェックポイントから再開可能にしましょう。
import uuid
import requests
session_id = uuid.uuid4().hex[:10] # one ID per flow, lowercase letters and digits
user = "USERNAME-country-us-state-ca-session-%s-ttl-20" % session_id
proxy = "http://%s:[email protected]:7777" % user
with requests.Session() as s:
s.proxies = {"http": proxy, "https": proxy}
first = s.get("https://api.ipify.org", timeout=(10, 30)).text
second = s.get("https://api.ipify.org", timeout=(10, 30)).text
print(first, second, "same exit" if first == second else "exit changed")Pythonスクレイパーは1ページあたりどのくらいトラフィックを使う?
購入前に実ページ100件で計測しましょう。httpxではnum_bytes_downloadedをログに記録するか、自作ミドルウェアで圧縮済みボディの長さを測り、ページ数をかけます。HTMLのみのリクエストは通常1ページあたり数十KBです。帯域幅ガイドで実数値とCrawlRelay価格の1,000ページあたりのコストがわかります。
結果を変えずにトラフィックを減らす習慣は:
Accept-Encoding: gzip, brを送る(httpxとrequestsはデフォルトでgzipを送信)とページが圧縮されて届く。- ページが読み込むJSONエンドポイントをサイトが許可/文書化している場合は、全HTMLよりそのJSONを取得する。
- 変わっていないページは
If-None-MatchやIf-Modified-Sinceでスキップ。304 Not Modifiedは数百バイトのコスト。
Python とプロキシ:よくある質問
requests、httpx、aiohttp:スクレイピングに使うならどれ?
シンプルなスクリプトや小規模なクローリングには requests を使いましょう。同期・非同期両方のコードで同じ API を使いたい、またhttpx[http2]を使って HTTP/2 対応したい場合は httpx が便利です。aiohttp は asyncio を駆使した高い同時実行性が必要な場合に最適です。スケジューリング・重複排除・パイプラインなどが必要なクロールでは、Scrapy を使いましょう。これらを自作するより効率的です。
Python のプロキシで SSL: WRONG_VERSION_NUMBER エラーが出るのはなぜ?
おそらくプロキシアドレスの前に https:// を書いてしまっているからです。ゲートウェイのポート 7777 はプレーンな HTTP 通信を行い、HTTPS サイト用に CONNECT トンネルを開くため、プロキシの URL は http と https 両方のキーで必ず http:// で始める必要があります。エラーページ を参照してください。
Python で SOCKS5 プロキシを使うには?
追加パッケージ(pip install "requests[socks]" または pip install "httpx[socks]")をインストールし、socks5h://USERNAME:[email protected]:7778 を使います。h はホスト名をプロキシに送信するため DNS 解決はプロキシ側で行われ、現地訪問者と同じ地域の回答が得られます。
なぜ回転式プロキシで同じ IP が2回返ってくる?
クライアントがキープアライブ接続を再利用しており、回転は新規接続ごとに発生します。requests なら Connection: close、httpx は max_keepalive_connections=0、aiohttp は force_close=True を送信しましょう。また、ユーザー名に session- 部分が入っていないか確認してください。これは意図的に同じ IP を使い続けるためです。
プロキシ経由のリクエストでタイムアウトを設定する方法は?
明示的に設定しましょう。requests はデフォルトでタイムアウトなしです。HTML ページなら接続タイムアウト約10秒、読み込みは30秒で十分。モバイル退出や遅いターゲットには45~60秒必要です。長く待つより新しい接続で再試行した方が効果的です。読み込みタイムアウト を参照。
Jupyterやpandasでこれらのプロキシを使える?
使えます。requests や httpx を使うライブラリなら同じプロキシ設定が適用されます。URL にpandas.read_html やread_csvがある場合は、まずプロキシ経由で requests からコンテンツを取得し、そのテキストやバイトデータを pandas に渡してください。
Python スクレイパーをレジデンシャルプロキシ経由で実行しよう
$1.50は1 GB、$0.50/GBは50 GBから。