プロキシローテーションは、本番環境のWebスクレイピングにおいて成功と失敗を分ける最も重要な要素の一つです。Crawlee for Pythonは、プロキシ管理とセッション管理をフレームワークレベルで統合しており、開発者がIPローテーションを自前で実装する必要をなくします。本記事では、CrawleeのProxyConfigurationクラスとSessionPoolを使ったプロキシローテーションの実装方法を、ProxyHatのresidentialプロキシと組み合わせた実例とともに詳しく解説します。
Crawlee for Pythonのアーキテクチャとプロキシローテーションの位置づけ
Crawlee for Pythonは、公式ドキュメントに記載されている通り、複数のクローラー実装(BeautifulSoupCrawler、PlaywrightCrawler)を統合されたリクエストキューの上に構築しています。各クローラーは共通のインフラストラクチャを利用します:
- Request Queue — すべてのクローラーが共有するURL管理レイヤー。重複排除、優先度付け、再試行の追跡を行います。
- Autoscaled Pool — システムリソースとエラー率に基づいて並行度を動的に調整するスケジューラ。最大同時リクエスト数を自動的に増減します。
- SessionPool — Cookie、ブラウザフィンガープリント、プロキシIPを1つのセッションオブジェクトに紐付ける管理機構。これがプロキシローテーションの中核となります。
- ProxyConfiguration — プロキシURLの生成とローテーション戦略を定義するクラス。SessionPoolと連携して、セッションごとに異なるプロキシを割り当てます。
これらのコンポーネントが連携することで、開発者はプロキシの選択、Cookieの保持、IPの切り替えを個別に管理する必要がなくなります。フレームワークが一貫した方法でこれらを処理してくれます。
なぜプロキシローテーションが必要なのか
現代のWebサイトは、Cloudflare、DataDome、Akamai Bot Managerなどの高度なボット検知システムを導入しています。これらのシステムは、単一IPからのリクエスト頻度、リクエストパターンの規則性、TLSフィンガープリント、ブラウザの挙動など、複数のシグナルを組み合わせてボットを検知します。1つのIPから短時間に大量のリクエストを送信すると、レート制限(HTTP 429)やCAPTCHAチャレンジ、IPブロックが発生します。
プロキシローテーションは、リクエストを複数のIPに分散させることで、各IPあたりのリクエスト頻度を下げ、ブロックされる確率を減らします。特にresidentialプロキシは、実際のISPに割り当てられたIPアドレスを使用するため、datacenterプロキシと比べて検知されにくいという利点があります。
ProxyConfigurationクラスの使い方
CrawleeのProxyConfigurationは、プロキシURLを動的に生成するためのクラスです。主なメソッドはnew_url()で、セッションIDを渡すことで同じセッションには同じプロキシIPを割り当てる「sticky session」を実現できます。
ラウンドロビン vs セッション固定
プロキシローテーションには主に2つの戦略があります:
| 戦略 | 説明 | 適用例 |
|---|---|---|
| ラウンドロビン | リクエストごとに異なるプロキシIPを使用 | ステートレスなSERPスクレイピング、単発ページ取得 |
| セッション固定(Sticky) | 同じセッションIDには同じIPを維持 | ログイン状態の保持、Cookieベースの認証、ページネーション |
CrawleeのSessionPoolは、セッションごとに一意のIDを生成し、そのIDにプロキシを紐付けます。セッションがブロックされた場合、session.retire()を呼ぶことで古いセッションを破棄し、新しいIPで新しいセッションを開始できます。
ResidentialプロキシがCloudflareやDataDomeに対して有効な理由
Datacenterプロキシは、AWS、Google Cloud、DigitalOceanなどのクラウドプロバイダーのIPレンジを使用します。これらのIPレンジは公開されており、ボット検知システムは容易に識別できます。一方、residentialプロキシは、実際のISP(Comcast、AT&T、NTTなど)に登録されたIPアドレスを使用するため、通常のユーザートラフィックと区別が困難です。
MDNのHTTPヘッダー仕様にある通り、プロキシを経由したリクエストにはForwardedやX-Forwarded-Forヘッダーが付与されることがありますが、透過型でないresidentialプロキシはこれらのヘッダーを付与しません。これにより、ターゲットサイトからは通常のユーザーとして認識されます。
階層型プロキシ戦略では、まずresidentialプロキシでリクエストを試行し、HTTP 403やCAPTCHAが返された場合にdatacenterプロキシにフォールバックする、といった構成が可能です。ただし、Crawleeでは標準で単一のProxyConfigurationを使用するため、複数のプロキシティアを使う場合はカスタムロジックをrequest_handler内に実装します。
実装例:BeautifulSoupCrawler + ProxyConfiguration
以下は、Crawlee for PythonでBeautifulSoupCrawlerを使用し、ProxyHatのresidentialプロキシをセッション固定で利用する実例です。
import asyncio
from crawlee.beautifulsoup_crawler import BeautifulSoupCrawler
from crawlee.proxy_configuration import ProxyConfiguration
from crawlee.sessions import SessionPool
def build_proxy_url(session_id: str, country: str = "US") -> str:
"""ProxyHatのゲートウェイにセッションIDと国を埋め込んだプロキシURLを生成"""
username = f"user-country-{country}-session-{session_id}"
password = "your_password"
return f"http://{username}:{password}@gate.proxyhat.com:8080"
async def main():
# ProxyConfigurationを構築 — new_urlがセッションごとに呼ばれる
proxy_configuration = ProxyConfiguration(
proxy_urls=[
# セッションIDはCrawleeが動的に生成するため、
# new_urlオーバーライドで対応
build_proxy_url("default", "US"),
],
)
# カスタムプロキシ生成を行う場合は、ProxyConfigurationを継承
class ProxyHatProxyConfiguration(ProxyConfiguration):
async def new_url(self, session_id: str | None = None) -> str:
sid = session_id or "default"
return build_proxy_url(sid, "US")
proxy_config = ProxyHatProxyConfiguration(
proxy_urls=[build_proxy_url("default", "US")]
)
crawler = BeautifulSoupCrawler(
proxy_configuration=proxy_config,
max_request_retries=3,
request_handler_timeout=timedelta(seconds=30),
max_requests_per_minute=60, # レート制限を明示的に設定
)
@crawler.router.default_handler
async def request_handler(context):
# セッションとプロキシはcontextから取得
session = context.session
proxy_info = await proxy_config.new_url(
session_id=session.id if session else None
)
context.log.info(
f"Using proxy for session {session.id}: {proxy_info}"
)
# ページを取得
response = await context.http_client.send(
context.request,
proxy_info=proxy_info,
session=session,
)
# ブロック検知
if response.status_code in (403, 429):
context.log.warning(
f"Blocked with status {response.status_code}, retiring session"
)
if session:
session.retire()
raise context.retry_request()
# データ抽出
title = context.soup.find("title")
context.log.info(f"Title: {title.text if title else 'N/A'}")
await crawler.run(["https://example.com"])
if __name__ == "__main__":
asyncio.run(main())
この例の重要なポイントは、ProxyHatProxyConfigurationがnew_urlをオーバーライドし、セッションIDをプロキシユーザー名に埋め込んでいる点です。これにより、同じセッションIDには常に同じプロキシIPが割り当てられ、CookieとIPの一貫性が保たれます。
curlでの動作確認
Crawleeに統合する前に、プロキシが正しく動作することをcurlで確認できます:
# USのresidentialプロキシをセッションID abc123で使用
curl -x "http://user-country-US-session-abc123:your_password@gate.proxyhat.com:8080" \
"https://httpbin.org/ip"
# ドイツ・ベルリンのIPを使用
curl -x "http://user-country-DE-city-berlin-session-abc123:your_password@gate.proxyhat.com:8080" \
"https://httpbin.org/ip"
PlaywrightCrawlerでのプロキシ統合
JavaScriptレンダリングが必要なサイトではPlaywrightCrawlerを使用します。ProxyConfigurationは同じように機能しますが、ブラウザレベルでプロキシが適用されます。
from crawlee.playwright_crawler import PlaywrightCrawler
from crawlee.proxy_configuration import ProxyConfiguration
class ProxyHatProxyConfiguration(ProxyConfiguration):
async def new_url(self, session_id: str | None = None) -> str:
sid = session_id or "default"
username = f"user-country-US-session-{sid}"
return f"http://{username}:your_password@gate.proxyhat.com:8080"
proxy_config = ProxyHatProxyConfiguration(
proxy_urls=["http://user-country-US-session-default:your_password@gate.proxyhat.com:8080"]
)
crawler = PlaywrightCrawler(
proxy_configuration=proxy_config,
max_request_retries=3,
browser_type="chromium",
headless=True,
)
@crawler.router.default_handler
async def handler(context):
page = context.page
session = context.session
# ページのタイトルを取得
title = await page.title()
context.log.info(f"Title: {title}")
# ブロック検知 — Cloudflareのチャレンジページをチェック
content = await page.content()
if "cf-challenge" in content or "Just a moment" in content:
context.log.warning("Cloudflare challenge detected")
if session:
session.retire()
raise context.retry_request()
# データ抽出
await context.enqueue_links()
await crawler.run(["https://example.com"])
本番運用のベストプラクティス
1. session.retire()でブロックに対応する
HTTP 403、429、またはCAPTCHAページが返された場合、そのセッションのIPは「焼失」したと判断し、session.retire()を呼んで新しいセッションとIPを取得します。CrawleeのSessionPoolは自動的に新しいセッションを作成し、ProxyConfigurationが新しいIPを割り当てます。
@crawler.router.default_handler
async def handler(context):
response = await context.http_client.send(
context.request,
session=context.session,
)
if response.status_code in (403, 429, 503):
if context.session:
context.session.retire()
# Crawleeが自動的に再試行
raise context.retry_request()
2. max_request_retriesで再試行を制限
無限再試行を防ぐため、max_request_retriesを明示的に設定します。一般的には3〜5回が適切です。再試行ごとに新しいセッションとIPが使用されるため、同じブロックされたIPで再試行されることはありません。
3. 並行度の管理
CrawleeのAutoscaled Poolは、CPU使用率、メモリ、エラー率に基づいて並行度を自動調整します。ただし、プロキシプロバイダーの同時接続制限を超えないよう、max_concurrencyを明示的に設定することを推奨します。例えば、ProxyHatで100並行セッションを許可する場合:
crawler = BeautifulSoupCrawler(
proxy_configuration=proxy_config,
max_request_retries=3,
max_concurrency=50, # プロキシの同時接続制限内に設定
max_requests_per_minute=200, # 1分あたりの最大リクエスト数
)
4. エラーハンドリングの階層化
ネットワークエラー、プロキシタイムアウト、ブロック応答など、異なるエラータイプに対して異なる対応を行います:
- TimeoutError — プロキシのレイテンシが高い。セッションを維持しつつ再試行。
- ProxyError — プロキシが応答しない。セッションを破棄して新しいIPで再試行。
- HTTP 403/429 — ブロックされた。セッションを破棄し、バックオフ後に再試行。
ブラウザクロールを使うべきでないケース
PlaywrightCrawlerは強力ですが、リソース消費が大きく、1ページあたりのレイテンシが500ms〜3秒増加します。以下の場合はBeautifulSoupCrawlerを優先すべきです:
- ターゲットサイトが静的HTMLを返す(SSR)
- JavaScriptレンダリングが不要
- 高いスループットが必要(1秒あたり100リクエスト以上)
- サーバーコストを抑えたい
PlaywrightCrawlerは、CloudflareのJavaScriptチャレンジを解決する必要がある場合や、SPA(Single Page Application)からデータを抽出する場合にのみ使用すべきです。詳細なユースケースについては、Webスクレイピングのユースケースを参照してください。
倫理的・法的な考慮事項
重要: Webスクレイピングを行う際は、対象サイトの利用規約(ToS)、robots.txt、および適用される法律(米国Computer Fraud and Abuse Act、EUのGDPRなど)を遵守してください。公開データのみを収集し、認証が必要なデータにはアクセスしないでください。公式APIが利用可能な場合は、そちらを優先してください。
米国のCFAAは、認可なしにコンピュータシステムにアクセスすることを犯罪としています。公開Webページのスクレイピングは一般的に合法とされていますが、利用規約で明示的に禁止されている場合は法的リスクがあります。EUのGDPRは個人データの処理に適用されるため、スクレイピングしたデータに個人情報が含まれる場合は、データ保護要件を満たす必要があります。
詳細なガイドラインについては、ProxyHat公式ドキュメントを参照してください。また、利用可能なプロキシロケーションと料金プランも確認できます。
Key Takeaways
- CrawleeのProxyConfigurationとSessionPoolを組み合わせることで、プロキシローテーションをフレームワークレベルで管理できる。
new_url(session_id=...)をオーバーライドすることで、セッションごとにresidentialプロキシIPを固定できる。- Residentialプロキシはdatacenterプロキシより検知されにくく、CloudflareやDataDomeに対して高い成功率を示す。
session.retire()でブロックされたIPを破棄し、新しいIPで再試行する仕組みを必ず実装する。max_request_retriesとmax_concurrencyを明示的に設定し、無限再試行とプロキシ同時接続制限超過を防ぐ。- JavaScriptレンダリングが不要な場合はBeautifulSoupCrawlerを使用し、リソース消費とレイテンシを最小化する。
- スクレイピングは公開データのみに限定し、robots.txt、ToS、CFAA、GDPRを遵守する。
ProxyHatのresidentialプロキシは、Crawlee for Pythonとの統合がシンプルで、ゲートウェイホストgate.proxyhat.com:8080にユーザー名パラメータを埋め込むだけでgeo-targetingとセッション固定を実現できます。SERPトラッキングのユースケースについては、SERPトラッキングのページも参照してください。






