KYC不要 — IDもセルフィーも書類も不要。暗号通貨で支払い:BTC, USDT, USDC, ETH, SOL, LTC
言語: 日本語
今すぐ始める

ドキュメント · Python 3.9+ · requests · httpx · aiohttp

Pythonでのレジデンシャルプロキシのローテーション:requests、httpx、aiohttp

本番環境向けパターン、単なるワンライナーではありません:Retry-Afterを尊重したリトライ、リクエストごとに新しいIPまたは1フローにつき1IP、asyncioでのホストごとの同時実行制御、そして各ページの実際の消費バイト数。

最終更新: · 3 分 読み

クイックアンサー

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セットアップの概要

requestsproxies={"http": url, "https": url};urllib3Retry経由でリトライ可能
httpxhttpx.Client(proxy=url);古いproxies=引数はhttpx 0.28で削除済み
aiohttpsession.get(url, proxy="http://USER:PASS@host:port")、認証情報はプロキシURL内で指定
SOCKS5pip 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変数があなたのレジデンシャルプロキシを上書きできないようにしてください。

requests + urllib3のRetry
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エンコードします。

ユーザー名ビルダーとプロキシ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は実際に受信したバイト数を返し、トラフィック課金に近い指標です。

httpx:ローテーティングクライアント
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から始めて、上げる前に同時実行ガイドを読みましょう。

ホストごとのセマフォ付きhttpx.AsyncClient
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が続きます。

プロキシURLに認証情報を含むaiohttp使用法
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が変わる可能性があるため、フローは最後のチェックポイントから再開可能にしましょう。

requestsでのスティッキーセッション管理
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から。

$0.50/GBから · KYC不要

今すぐ始める