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:
- Spider bir
Requestoluşturur. - İstek,
DOWNLOADER_MIDDLEWARESdict'inde tanımlı sırayla middleware'lerden geçer (düşük sayı = yüksek öncelik). - Her middleware isteği değiştirebilir, düşürebilir veya geçirebilir.
- Downloader isteği gönderir, yanıt/istisna döner.
- 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 | Öncelik | Rol |
|---|---|---|
| RetryMiddleware | 500 | Başarısız istekleri yeniden dener |
| HttpProxyMiddleware | 560 | meta['proxy'] adresini uygular |
| Kendi ProxyMiddleware'imiz | 540 | Rotasyon + 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:
| Özellik | scrapy-rotating-proxies | Özel Middleware |
|---|---|---|
| Kurulum | pip install + proxy listesi dosyası | Middleware sınıfı + konfigürasyon |
| Proxy kaynağı | Statik dosya / URL listesi | Dinamik API (residential proxy havuzu) |
| Geo-targeting | Yok | Ülke/sehir bazlı hedefleme |
| Sticky session | Yok | Domain bazlı oturum tutma |
| Ban algılama | Temel (HTTP durum kodları) | Özelleştirilebilir (regex, içerik analizi) |
| Bakım | 3+ yıl güncelleme yok | Tam kontrol |
| Residential proxy desteği | Sı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-proxiesprototipleme 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.






