株式会社WR

株式会社WR

WEB TOTAL CONSULTING

プロキシローテーションでIPブロックを回避するスクレイピング
ブログ一覧へ
技術ブログ

プロキシローテーションでIPブロックを回避するスクレイピング

大量のスクレイピングを行うと、同一IPからのアクセスとしてブロックされることがあります。プロキシローテーションで複数IPを使い分ける方法を解説します。

IPブロックとは——スクレイピングの最大の壁

大規模なスクレイピングを行うと、対象サイトのサーバーから同一IPアドレスへのリクエストを制限(IPブロック)されることがあります。これはサーバー保護のための正当な措置であり、スクレイパー側は以下の対策が必要です。

前提として:スクレイピングはサイトの利用規約を確認し、許可された範囲内で、サーバーに過大な負荷をかけない形で実施することが大原則です。


プロキシとは

プロキシサーバーは、クライアントとWebサーバーの間に立つ中継サーバーです。プロキシを経由することで、WebサーバーからはプロキシサーバーのIPアドレスが送信元として見えます。

クライアント → プロキシA → Webサーバー(プロキシAのIPを検知)
クライアント → プロキシB → Webサーバー(プロキシBのIPを検知)

プロキシの種類と選び方

種類 説明 コスト 信頼性
無料プロキシ 誰でも使える公開プロキシ 無料 低(すぐブロックされる)
共有プロキシ 複数人で共有 安価 中
専用プロキシ 自分専用 中程度 高
住宅用プロキシ(Residential) 実際のISP IPアドレス 高価 最高
データセンタープロキシ クラウドIPアドレス 中程度 中〜高

Pythonでのプロキシローテーション実装

import requests
import random
import time
from itertools import cycle

class ProxyRotator:
    """プロキシをラウンドロビンでローテーションする"""

    def __init__(self, proxies: list[str]):
        """
        proxies: ["http://user:pass@host:port", ...]
        """
        self._proxies = proxies
        self._cycle   = cycle(proxies)
        self._failed  = set()

    def get_proxy(self) -> dict:
        """使用可能なプロキシを1つ返す"""
        for _ in range(len(self._proxies)):
            proxy = next(self._cycle)
            if proxy not in self._failed:
                return {'http': proxy, 'https': proxy}

        # 全プロキシが失敗したらリセット
        self._failed.clear()
        return {'http': self._proxies[0], 'https': self._proxies[0]}

    def mark_failed(self, proxy_url: str) -> None:
        """失敗したプロキシを記録"""
        # dictからURLを取り出す場合
        self._failed.add(proxy_url)

    @property
    def available_count(self) -> int:
        return len(self._proxies) - len(self._failed)

スクレイパークラスへの組み込み

import requests
from requests.exceptions import ProxyError, ConnectionError, Timeout

class ResilientScraper:
    def __init__(self, proxies: list[str], delay: tuple = (1.0, 3.0)):
        self.rotator = ProxyRotator(proxies)
        self.delay = delay
        self.session = requests.Session()
        self.session.headers.update({
            'User-Agent': (
                'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) '
                'AppleWebKit/537.36 (KHTML, like Gecko) '
                'Chrome/124.0.0.0 Safari/537.36'
            ),
            'Accept-Language': 'ja-JP,ja;q=0.9',
        })

    def fetch(self, url: str, max_retries: int = 3) -> str | None:
        """プロキシローテーション付きでページを取得"""
        for attempt in range(max_retries):
            proxy = self.rotator.get_proxy()
            try:
                response = self.session.get(
                    url,
                    proxies=proxy,
                    timeout=15,
                )
                response.raise_for_status()

                # 成功時のウェイト
                time.sleep(random.uniform(*self.delay))
                return response.text

            except (ProxyError, ConnectionError) as e:
                # プロキシエラー:そのプロキシを無効化
                proxy_url = list(proxy.values())[0]
                self.rotator.mark_failed(proxy_url)
                print(f"プロキシ失敗 [{attempt+1}/{max_retries}]: {proxy_url}")

            except Timeout:
                print(f"タイムアウト [{attempt+1}/{max_retries}]: {url}")
                time.sleep(5)

            except requests.HTTPError as e:
                if e.response.status_code == 403:
                    print(f"403 Forbidden: IPブロックの可能性 {url}")
                    time.sleep(10)
                elif e.response.status_code == 429:
                    print(f"429 Too Many Requests: {e.response.headers.get('Retry-After', 60)}秒待機")
                    time.sleep(int(e.response.headers.get('Retry-After', 60)))
                else:
                    raise

        return None

    def fetch_many(self, urls: list[str]) -> dict[str, str | None]:
        """複数URLを一括取得"""
        results = {}
        for i, url in enumerate(urls):
            print(f"[{i+1}/{len(urls)}] 取得中: {url}")
            results[url] = self.fetch(url)
        return results

実際の使い方

# プロキシリスト(環境変数または設定ファイルから読む)
import os

proxies = os.environ.get('PROXY_LIST', '').split(',')
# 例: "http://user:pass@proxy1.example.com:8080,http://user:pass@proxy2.example.com:8080"

scraper = ResilientScraper(proxies=proxies, delay=(1.5, 3.5))

urls = [
    'https://example.com/page/1',
    'https://example.com/page/2',
    'https://example.com/page/3',
]

results = scraper.fetch_many(urls)
for url, html in results.items():
    if html:
        print(f"取得成功: {url} ({len(html)}文字)")
    else:
        print(f"取得失敗: {url}")

セッション管理でクッキーを維持する

# ログインが必要なサイトではセッションを維持
login_session = requests.Session()
login_session.post('https://example.com/login', data={
    'email':    os.environ['EMAIL'],
    'password': os.environ['PASSWORD'],
})

# ログイン後のプロキシ付きリクエスト
proxy = rotator.get_proxy()
protected_page = login_session.get(
    'https://example.com/protected',
    proxies=proxy,
)

まとめ

プロキシローテーションはIPブロックを回避するための技術ですが、あくまでも倫理的・合法的なスクレイピングの範囲内で使用することが重要です。弊社では適切なウェイト・robots.txt遵守・利用規約確認を前提に、プロキシを組み合わせた安定したデータ収集システムを構築しています。

スクレイピング・データ収集システムの開発についてはお気軽にご相談ください。

Category 技術ブログ

Related Posts

関連記事

開発・技術のご相談はお気軽に

お見積りは無料です。まずはお気軽にご相談ください。

お問い合わせ →