Scrapy Proxy Middleware: Residential Proxy Rotasyonu Rehberi

Scrapy'nin downloader middleware mimarisini kullanarak residential proxy rotasyonu yapan özel bir middleware yazın; hata yönetimi, JS render ve production dağıtımını kapsayan kapsamlı rehber.

Scrapy Proxy Middleware: Residential Proxy Rotasyonu Rehberi

Scrapy Proxy Middleware Neden Kritik?

Scrapy ile production seviyesinde scraping yapan her ekip, er ya da geç aynı duvara çarpar: IP bloklamaları. Tek bir IP'den binlerce istek attığınızda, hedef site saniyeler içinde sizi tespit eder ve 403/503 yanıtlarıyla karşılaşırsınız. Scrapy proxy middleware katmanı, bu sorunu çözmenin en temiz ve genişletilebilir yoludur.

Scrapy'nin middleware mimarisi, her isteğin yaşam döngüsüne müdahale etmenize olanak tanır. Proxy rotasyonunu bir middleware olarak implemente ettiğinizde, spider kodunuz temiz kalır, retry mantığı merkezi hale gelir ve farklı proxy sağlayıcıları arasında geçiş yapmak bir konfigürasyon değişikliğine indirgenir.

Bu rehberde, Scrapy'nin downloader middleware modelinden başlayarak, Scrapy residential proxies ile çalışan tam fonksiyonlu bir proxy rotasyon middleware'i yazacağız; hata yönetimi, JS render entegrasyonu ve production dağıtımını kapsayacağız.

Scrapy Downloader Middleware Mimarisi

Scrapy'de tüm HTTP istekleri, downloader'a ulaşmadan önce bir middleware zincirinden geçer. Her middleware, process_request(), process_response() ve process_exception() metodlarını uygulayarak istek-yanıt döngüsüne müdahale edebilir.

Proxy entegrasyonu için önemli olan akış şöyledir:

  1. Spider bir Request oluşturur.
  2. İstek, DOWNLOADER_MIDDLEWARES dict'inde tanımlı sırayla middleware'lerden geçer (düşük sayı = yüksek öncelik).
  3. Her middleware isteği değiştirebilir, düşürebilir veya geçirebilir.
  4. Downloader isteği gönderir, yanıt/istisna döner.
  5. Yanıt ve istisnalar, middleware zincirinden ters sırayla geçer.

Yerleşik HttpProxyMiddleware Yetersiz Kalır

Scrapy'nin yerleşik HttpProxyMiddleware (öncelik 560), Request.meta['proxy'] alanını okur ve isteğe proxy URL'sini atar. İşlevseldir ama tek bir proxy adresi varsayar; rotasyon, sağlık kontrolü veya hata yönetimi sunmaz. Production'da ihtiyacınız olan şey, her isteğe farklı bir IP atayan ve başarısız proxy'leri devre dışı bırakan bir middleware'dir.

Middleware Öncelik Sıralaması

Kendi proxy middleware'inizi yazarken, Scrapy'nin yerleşik middleware'leriyle çakışmamak için öncelik değerini doğru seçmeniz gerekir:

MiddlewareÖncelikRol
RetryMiddleware500Başarısız istekleri yeniden dener
HttpProxyMiddleware560meta['proxy'] adresini uygular
Kendi ProxyMiddleware'imiz540Rotasyon + proxy ataması (Retry'dan sonra, HttpProxy'den önce)

540'ı seçiyoruz çünkü: RetryMiddleware (500) önce çalışıp yeniden deneme kararı vermeli, ardından biz proxy atamalıyız, en son HttpProxyMiddleware proxy URL'sini bağlantıya uygulamalı.

Özel Proxy Rotasyon Middleware'i Implementasyonu

Aşağıdaki middleware sınıfı, ProxyHat residential proxy havuzuyla entegre çalışır. Her isteğe rastgele bir proxy atar, başarısız proxy'leri geçici olarak havuzdan çıkarır ve Scrapy proxy rotation mantığını spider'dan tamamen ayırır.

import random
import time
import logging
from scrapy import signals
from scrapy.exceptions import IgnoreRequest
from scrapy.utils.project import get_project_settings

logger = logging.getLogger(__name__)


class ProxyRotatingMiddleware:
    """Scrapy downloader middleware for residential proxy rotation.

    Assigns a fresh proxy URL to every request, tracks per-proxy
    failure counts, and temporarily removes unhealthy proxies from
    the pool. Designed for ProxyHat residential proxies.
    """

    def __init__(self, proxy_config):
        self.proxy_user = proxy_config.get('username', 'user')
        self.proxy_pass = proxy_config.get('password', 'pass')
        self.proxy_host = 'gate.proxyhat.com'
        self.proxy_port = 8080
        self.countries = proxy_config.get('countries', [])
        self.sticky_sessions = proxy_config.get('sticky_sessions', False)
        self.session_map = {}  # domain -> session_id
        self.failure_counts = {}
        self.max_failures = proxy_config.get('max_failures', 3)
        self.ban_until = {}
        self.ban_duration = proxy_config.get('ban_duration', 300)

    @classmethod
    def from_crawler(cls, crawler):
        proxy_config = crawler.settings.getdict('PROXYHAT_CONFIG', {})
        mw = cls(proxy_config)
        crawler.signals.connect(mw.spider_opened, signal=signals.spider_opened)
        return mw

    def spider_opened(self, spider):
        logger.info(
            f"ProxyRotatingMiddleware initialized — "
            f"countries={self.countries or 'all'}, "
            f"sticky={self.sticky_sessions}"
        )

    def _build_proxy_url(self, request):
        """Construct proxy URL with optional geo-targeting and sticky session."""
        username = self.proxy_user

        # Geo-targeting: assign a random country from configured list
        if self.countries:
            country = random.choice(self.countries)
            username = f"{self.proxy_user}-country-{country}"

        # Sticky sessions: same domain gets the same session ID
        if self.sticky_sessions:
            domain = request.url.split('/')[2]
            if domain not in self.session_map:
                self.session_map[domain] = f"sess_{int(time.time())}_{random.randint(1000,9999)}"
            username = f"{username}-session-{self.session_map[domain]}"

        return f"http://{username}:{self.proxy_pass}@{self.proxy_host}:{self.proxy_port}"

    def process_request(self, request, spider):
        """Assign a proxy to every outgoing request."""
        proxy_url = self._build_proxy_url(request)

        # Skip this proxy if it's in cooldown
        proxy_key = proxy_url.split('@')[1]  # host:port portion
        if proxy_key in self.ban_until:
            if time.time() < self.ban_until[proxy_key]:
                return  # let HttpProxyMiddleware handle with next available
            else:
                del self.ban_until[proxy_key]
                self.failure_counts.pop(proxy_key, None)

        request.meta['proxy'] = proxy_url
        request.meta['proxy_key'] = proxy_key
        logger.debug(f"Assigned proxy {proxy_key} to {request.url[:80]}")

    def process_response(self, request, response, spider):
        """Detect bans and track failures."""
        proxy_key = request.meta.get('proxy_key')
        if not proxy_key:
            return response

        # Common ban signals
        if response.status in (403, 429, 503):
            self._record_failure(proxy_key)
            logger.warning(
                f"Proxy {proxy_key} returned {response.status} "
                f"for {request.url[:80]}"
            )
            # Let RetryMiddleware handle the retry
            return response

        # Successful response — reset failure count
        self.failure_counts.pop(proxy_key, None)
        return response

    def process_exception(self, request, exception, spider):
        """Handle connection-level proxy failures."""
        proxy_key = request.meta.get('proxy_key')
        if proxy_key:
            self._record_failure(proxy_key)
            logger.warning(
                f"Proxy {proxy_key} raised {type(exception).__name__} "
                f"for {request.url[:80]}"
            )
        return None  # let other middleware handle

    def _record_failure(self, proxy_key):
        self.failure_counts[proxy_key] = self.failure_counts.get(proxy_key, 0) + 1
        if self.failure_counts[proxy_key] >= self.max_failures:
            self.ban_until[proxy_key] = time.time() + self.ban_duration
            logger.warning(
                f"Proxy {proxy_key} banned for {self.ban_duration}s "
                f"after {self.max_failures} failures"
            )

Konfigürasyon

Middleware'i settings.py dosyanızda etkinleştirin:

# settings.py

DOWNLOADER_MIDDLEWARES = {
    'scrapy.downloadermiddlewares.retry.RetryMiddleware': 500,
    'myproject.middlewares.ProxyRotatingMiddleware': 540,
    'scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware': 560,
}

PROXYHAT_CONFIG = {
    'username': 'your_username',
    'password': 'your_password',
    'countries': ['US', 'DE', 'GB'],  # boş liste = tüm ülkeler
    'sticky_sessions': True,
    'max_failures': 3,
    'ban_duration': 300,  # saniye
}

# Retry middleware ile koordinasyon
RETRY_ENABLED = True
RETRY_TIMES = 3
RETRY_HTTP_CODES = [403, 429, 503, 500, 502]

Retry Middleware ile Proxy Rotasyon Koordinasyonu

Scrapy'nin yerleşik RetryMiddleware başarısız istekleri yeniden kuyruğa alır — ama varsayılan olarak aynı proxy ile. Bu, banlanmış bir IP'ye tekrar tekrar istek göndermeniz anlamına gelir. Çözüm, retry sonrası proxy değişikliğini garanti altına almaktır.

İki yaklaşım vardır:

  • Yaklaşım 1: RetryMiddleware'i devre dışı bırakıp, kendi hem retry hem proxy mantığınızı yazmak. Karmaşık ve bakımı zor.
  • Yaklaşım 2: RetryMiddleware'i aktif tutup, her retry'da proxy'yi değiştirmek. Daha temiz.

Yaklaşım 2 için, middleware'imizde process_request her çağrıldığında yeni bir proxy üretir — zaten rastgele ülke seçtiğimiz için retry sonrası istek otomatik olarak farklı bir IP alır. Ancak sticky session kullanıyorsanız, banlanmış session'ı sıfırlamanız gerekir:

# ProxyRotatingMiddleware'e ekleyin

def process_response(self, request, response, spider):
    proxy_key = request.meta.get('proxy_key')
    if not proxy_key:
        return response

    if response.status in (403, 429, 503):
        self._record_failure(proxy_key)
        # Sticky session'ı sıfırla — bir sonraki istek yeni session alır
        domain = request.url.split('/')[2]
        if domain in self.session_map:
            old_session = self.session_map.pop(domain)
            logger.info(f"Reset sticky session {old_session} for {domain}")
        return response

    self.failure_counts.pop(proxy_key, None)
    return response

Bu sayede, 403 alan bir domain için sticky session sıfırlanır ve bir sonraki retry tamamen yeni bir IP ile gider.

scrapy-rotating-proxies vs Özel Middleware

Topluluk tarafından geliştirilen scrapy-rotating-proxies paketi, proxy rotasyonu için popüler bir seçenektir. Ancak production'da sınırlamaları vardır:

Özellikscrapy-rotating-proxiesÖzel Middleware
Kurulumpip install + proxy listesi dosyasıMiddleware sınıfı + konfigürasyon
Proxy kaynağıStatik dosya / URL listesiDinamik API (residential proxy havuzu)
Geo-targetingYokÜlke/sehir bazlı hedefleme
Sticky sessionYokDomain bazlı oturum tutma
Ban algılamaTemel (HTTP durum kodları)Özelleştirilebilir (regex, içerik analizi)
Bakım3+ yıl güncelleme yokTam kontrol
Residential proxy desteğiSınırlı (IP listesi gerektirir)Doğal (gateway endpoint)

scrapy-rotating-proxies ne zaman kullanılır? Hızlı prototipleme için ve datacenter proxy listeleriniz varsa uygundur. Ancak residential proxy havuzlarıyla çalışırken, her istekte gateway'den yeni IP almak istersiniz — statik bir IP listesi yönetmek istemezsiniz. ProxyHat gibi residential proxy servisleri, bir IP havuzu yönetmek yerine bir gateway endpoint üzerinden rotasyon sağlar; bu da özel middleware yazmayı daha mantıklı kılar.

Özel middleware ne zaman kullanılır? Geo-targeting, sticky session, ban algılama ve residential proxy havuzu entegrasyonu gerektiğinde — yani production scraping'inin neredeyse her senaryosunda.

JS-Heavy Siteler İçin Proxy Entegrasyonu

Modern web sitelerinin önemli bir kısmı, server-side rendering yerine client-side JavaScript ile içerik oluşturuyor. Scrapy'nin standart downloader'ı JavaScript çalıştırmaz. Bu sorunu çözmek için iki ana yaklaşım vardır:

scrapy-splash ile Proxy Kullanımı

Splash, JavaScript render eden bir proxy hizmetidir. Scrapy ile entegrasyon için scrapy-splash middleware'i kullanılır. Proxy'yi Splash üzerinden yönlendirebilirsiniz:

# settings.py
SPLASH_URL = 'http://splash:8050'

DOWNLOADER_MIDDLEWARES = {
    'scrapy_splash.SplashDedupArgsMiddleware': 100,
    'myproject.middlewares.ProxyRotatingMiddleware': 540,
    'scrapy_splash.SplashMiddleware': 725,
    'scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware': 810,
}

# Splash'a proxy'yi iletin
# spider'da:
def parse(self, response):
    splash_args = {
        'render.html': 1,
        'proxy': 'http://user-country-US:pass@gate.proxyhat.com:8080',
        'wait': 2,
    }
    yield SplashRequest(
        response.url,
        self.parse_rendered,
        endpoint='render.html',
        args=splash_args,
    )

ProxyRotatingMiddleware, Splash isteklerine de proxy atayabilir — ancak Splash kendi proxy parametresini kullanıyorsa, middleware'i atlamak isteyebilirsiniz. Bunun için request.meta.get('splash') kontrolü ekleyin.

scrapy-playwright ile Proxy Entegrasyonu

Playwright, headless Chrome/Firefox çalıştıran daha modern bir çözümdür. scrapy-playwright, proxy desteğini doğrudan browser context seviyesinde sunar:

# settings.py
DOWNLOAD_HANDLERS = {
    'http': 'scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler',
    'https': 'scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler',
}

TWISTED_REACTOR = 'twisted.internet.asyncioreactor.AsyncioSelectorReactor'
# spider'da — her isteğe farklı ülke proxy'si atama
import random

class JsHeavySpider(scrapy.Spider):
    name = 'js_heavy'

    def start_requests(self):
        countries = ['US', 'DE', 'GB', 'FR']
        for url in self.start_urls:
            country = random.choice(countries)
            yield scrapy.Request(
                url,
                callback=self.parse,
                meta={
                    'playwright': True,
                    'playwright_context_kwargs': {
                        'proxy': {
                            'server': 'http://gate.proxyhat.com:8080',
                            'username': f'user-country-{country}',
                            'password': 'your_password',
                        }
                    }
                }
            )

Playwright ile proxy rotasyonunu playwright_context_kwargs üzerinden yönetmek, her browser context'e farklı bir proxy atamanızı sağlar. Bu, aynı anda birden fazla ülke IP'siyle paralel scraping yapmanın en etkili yoludur.

Production Dağıtım Stratejileri

Scrapy projenizi production'a taşırken, proxy yönetimi dışında da düşünmeniz gereken konular var: concurrency, headless browser fleet yönetimi ve zamanlama.

Docker + Cron ile Basit Dağıtım

Küçük-orta ölçekli projeler için en pratik yaklaşım:

  • Scrapy projenizi Docker imajı olarak paketleyin.
  • Her spider için ayrı bir cron job tanımlayın.
  • Logları merkezi bir yere (ELK, Loki, CloudWatch) gönderin.
# Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
ENTRYPOINT ["scrapy", "crawl"]
# docker-compose.yml — paralel spider çalıştırma
version: '3.8'
services:
  spider-serp:
    build: .
    command: ["serp_spider", "-s", "CONCURRENT_REQUESTS=32"]
    environment:
      - PROXYHAT_USER=${PROXYHAT_USER}
      - PROXYHAT_PASS=${PROXYHAT_PASS}
  spider-pricing:
    build: .
    command: ["pricing_spider", "-s", "CONCURRENT_REQUESTS=64"]
    environment:
      - PROXYHAT_USER=${PROXYHAT_USER}
      - PROXYHAT_PASS=${PROXYHAT_PASS}

Scrapyd ve ScrapeOps

Daha büyük operasyonlar için:

  • Scrapyd: Scrapy'nin resmi daemon'u. Spider'ları API üzerinden tetikler, schedule eder. Her spider çalışması ayrı bir process'te çalışır. Proxy middleware'iniz her process'te bağımsız çalışır — sorun yok.
  • ScrapeOps: Yönetilen Scrapy hosting. Proxy yönetimi ve monitoring dahil. Ancak kendi proxy sağlayıcınızı kullanmak istiyorsanız, middleware yaklaşımı yine geçerli.

Concurrency ve Rate Limiting

Residential proxy kullanırken concurrency ayarları kritik:

  • CONCURRENT_REQUESTS: ProxyHat residential proxy havuzunda 50-200 arası başlangıç yapın.
  • CONCURRENT_REQUESTS_PER_DOMAIN: Aynı domain'e eşzamanlı istekleri sınırlayın (3-5 arası ideal).
  • DOWNLOAD_DELAY: Hedef siteye göre ayarlayın; agresif scraping CAPTCHA'ya yol açar.
  • AUTOTHROTTLE_ENABLED: Scrapy'nin otomatik hız ayarı, yanıt sürelerine göre concurrency'yi dinamik olarak ayarlar.

Monitoring: IP Başarı Oranı ve Ban Algılama

Production'da proxy'lerinizin sağlığını izlemek, sorunları proaktif olarak yakalamanızı sağlar. Temel metrikler:

Takip Edilmesi Gereken Metrikler

  • IP başına başarı oranı: Her proxy IP'sinin 2xx yanıt oranı.
  • Ban oranı: 403/429 yanıt yüzdesi.
  • Ortalama yanıt süresi: Proxy gecikmesi scraping hızını doğrudan etkiler.
  • CAPTCHA karşılaşma oranı: Anti-bot sistemleri tarafından ne sıklıkla engellendiğiniz.

Özel Stat Toplama Middleware'i

Aşağıdaki middleware, proxy metriklerini toplar ve Scrapy'nin istatistik sistemine yazar:

from scrapy import signals
import logging

logger = logging.getLogger(__name__)


class ProxyStatsMiddleware:
    """Collect per-country proxy success/ban statistics."""

    def __init__(self):
        self.stats = {}

    @classmethod
    def from_crawler(cls, crawler):
        mw = cls()
        crawler.signals.connect(mw.spider_closed, signal=signals.spider_closed)
        return mw

    def process_response(self, request, response, spider):
        country = request.meta.get('proxy_country', 'unknown')
        if country not in self.stats:
            self.stats[country] = {'success': 0, 'ban': 0, 'error': 0}

        if response.status < 400:
            self.stats[country]['success'] += 1
        elif response.status in (403, 429):
            self.stats[country]['ban'] += 1
        else:
            self.stats[country]['error'] += 1

        return response

    def spider_closed(self, spider, reason):
        logger.info("=== Proxy Statistics ===")
        for country, counts in self.stats.items():
            total = sum(counts.values())
            success_rate = counts['success'] / total * 100 if total else 0
            logger.info(
                f"Country {country}: {success_rate:.1f}% success "
                f"({counts['success']}/{total}), "
                f"bans={counts['ban']}, errors={counts['error']}"
            )

Bu middleware'i kullanmak için ProxyRotatingMiddleware'de proxy URL oluştururken request.meta['proxy_country'] alanını da ayarlayın — böylece ülke bazlı başarı oranlarını izleyebilirsiniz.

Ban Algılama Desenleri

Yalnızca HTTP durum kodlarına güvenmek yeterli değildir. Birçok site, banlanan IP'lere 200 yantı verip CAPTCHA sayfası döndürür. Gelişmiş ban algılama için:

  • İçerik kontrolü: Yanıt gövdesinde "captcha", "blocked", "access denied" gibi kelimeler arayın.
  • Yanıt boyutu: Normal sayfa ~50KB iken aniden 5KB geliyorsa, muhtemelen ban sayfasıdır.
  • Redirect takibi: Beklenmeyen yönlendirmeler (örneğin login sayfasına) ban göstergesi olabilir.
# ProxyRotatingMiddleware.process_response'e ekleyin

def _is_cloaked_ban(self, response):
    """Detect bans that return 200 with CAPTCHA content."""
    if response.status != 200:
        return False
    body = response.text.lower()
    ban_signals = ['captcha', 'recaptcha', 'cloudflare', 'access denied']
    if any(signal in body for signal in ban_signals):
        # Beklenen boyuttan çok küçükse ban kabul et
        expected_size = response.meta.get('expected_size', 10000)
        if len(response.body) < expected_size * 0.3:
            return True
    return False

Key Takeaways

Scrapy proxy middleware entegrasyonunda akılda tutmanız gereken temel noktalar:

  • Middleware önceliği 540 seçin — RetryMiddleware (500) sonrası, HttpProxyMiddleware (560) öncesi.
  • Residential proxy havuzlarında statik IP listesi yönetmek yerine, gateway endpoint üzerinden dinamik rotasyon kullanın.
  • Her retry'da proxy değişikliğini garanti altına alın; sticky session kullanıyorsanız, ban sonrası session'ı sıfırlayın.
  • scrapy-rotating-proxies prototipleme için uygundur, ancak production'da geo-targeting ve residential proxy desteği gerektiğinde özel middleware yazın.
  • JS-heavy siteler için Playwright entegrasyonu, proxy'yi browser context seviyesinde yönetmenizi sağlar — playwright_context_kwargs üzerinden.
  • Docker + cron basit projeler için yeterlidir; Scrapyd büyük operasyonlar için düşünün.
  • Her spider çalışmasında ülke/IP bazlı başarı oranlarını loglayın — ban sorunlarını proaktif olarak yakalamanızı sağlar.
  • CAPTCHA ve cloaked ban'ları algılamak için yalnızca HTTP durum kodlarına değil, yanıt içeriğine de bakın.

ProxyHat'ın residential proxy havuzuyla Scrapy entegrasyonuna başlamak için fiyatlandırma sayfamızı inceleyin ve 200+ lokasyon listemize göz atın. SERP scraping ve fiyat karşılaştırma gibi senaryolar için hazır kullanım örneklerini web scraping kullanım sayfamızda bulabilirsiniz.

Başlamaya hazır mısınız?

148+ ülkede 50M+ konut IP'sine AI destekli filtreleme ile erişin.

Fiyatlandırmayı GörüntüleKonut Proxy'leri
← Bloga Dön