Perché il Proxy Middleware in Scrapy è Fondamentale
Se hai mai lanciato uno spider Scrapy contro un sito protetto da Cloudflare o PerimeterX, sai cosa succede: le prime 50 richieste funzionano, poi arriva il ban di massa. Il problema non è Scrapy — è che il tuo spider usa un singolo IP per centinaia di richieste al minuto. I server target vedono questo traffico per quello che è: automatizzato.
La soluzione non è semplicemente «aggiungere un proxy». La soluzione è integrare la rotazione dei proxy nel middleware layer di Scrapy, dove puoi controllare quale IP usare per ogni richiesta, come gestire i fallimenti e quando scartare un proxy bannato. Questa guida ti porta attraverso l'intero stack — dall'architettura del middleware al deployment in produzione.
L'Architettura del Downloader Middleware in Scrapy
Scrapy processa ogni richiesta attraverso una catena di downloader middleware prima che raggiunga il server remoto, e ogni risposta attraversa la stessa catena al ritorno. Questo è il punto di estensione naturale per i proxy.
La catena del middleware è definita in settings.py tramite un dizionario ordinato dove il valore numerico determina la priorità — valori più bassi vengono eseguiti prima:
DOWNLOADER_MIDDLEWARES = {
'myproject.middlewares.ProxyRotationMiddleware': 543,
'scrapy.downloadermiddlewares.retry.RetryMiddleware': 550,
'scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware': 560,
}I middleware chiave per i proxy sono:
- HttpProxyMiddleware (560) — legge
http_proxydall'ambiente o dal metaproxydella richiesta e imposta l'header appropriato. - RetryMiddleware (550) — ritenta le richieste fallite, ma di default non ruota il proxy.
- Il tuo middleware personalizzato — si inserisce nella catena per assegnare proxy, gestire ban e coordinare i retry.
Il flusso è: Request → process_request(middleware) → Downloader → Response → process_response(middleware). Se un middleware solleva IgnoreRequest, la richiesta viene scartata. Se ritorna None, il prossimo middleware nella catena continua.
Il meta proxy — il punto di aggancio
Scrapy utilizza request.meta['proxy'] come URL del proxy per quella specifica richiesta. Qualsiasi middleware può impostare questo valore prima che HttpProxyMiddleware lo legga:
# In un middleware personalizzato
def process_request(self, request, spider):
request.meta['proxy'] = 'http://user-country-US:pass@gate.proxyhat.com:8080'
return None # passa al prossimo middlewareQuesto è il meccanismo fondamentale. Tutto il resto — rotazione, sticky sessions, geo-targeting — si costruisce sopra questo hook.
Implementare un ProxyRotationMiddleware Personalizzato
Il middleware built-in HttpProxyMiddleware applica un proxy statico. Per la rotazione residenziale hai bisogno di un middleware che selezioni un IP diverso per ogni richiesta, supporti sessioni sticky e gestisca il geo-targeting. Ecco un'implementazione completa:
import random
import time
from collections import defaultdict
from scrapy import signals
from scrapy.exceptions import IgnoreRequest
from scrapy.utils.request import fingerprint
class ProxyRotationMiddleware:
"""Rotazione proxy residenziali con ban tracking e sticky session."""
def __init__(self, proxy_url, max_retries=3, sticky_timeout=600):
self.proxy_url = proxy_url # es. http://USERNAME:PASSWORD@gate.proxyhat.com:8080
self.max_retries = max_retries
self.sticky_timeout = sticky_timeout
# session_id → timestamp di creazione
self.sessions = {}
# proxy_fingerprint → contatore ban
self.ban_counts = defaultdict(int)
# request_fingerprint → session_id per retry
self.request_session = {}
@classmethod
def from_crawler(cls, crawler):
proxy_url = crawler.settings.get(
'PROXYHAT_URL',
'http://USERNAME:PASSWORD@gate.proxyhat.com:8080'
)
max_retries = crawler.settings.get('PROXY_MAX_RETRIES', 3)
sticky_timeout = crawler.settings.get('PROXY_STICKY_TIMEOUT', 600)
middleware = cls(proxy_url, max_retries, sticky_timeout)
crawler.signals.connect(middleware.spider_opened, signal=signals.spider_opened)
return middleware
def spider_opened(self, spider):
spider.logger.info('ProxyRotationMiddleware attivo con %s', self.proxy_url.split('@')[-1])
def _build_proxy_url(self, request):
"""Costruisce URL proxy con geo-targeting e sessione."""
fp = fingerprint(request)
# Se la richiesta ha già una sessione assegnata (retry), riutilizzala
if fp in self.request_session:
session_id = self.request_session[fp]
# Controlla se la sessione è scaduta
if time.time() - self.sessions.get(session_id, 0) < self.sticky_timeout:
return self._inject_session(self.proxy_url, session_id, request)
# Nuova sessione
session_id = f'sess-{random.randint(100000, 999999)}'
self.sessions[session_id] = time.time()
self.request_session[fp] = session_id
return self._inject_session(self.proxy_url, session_id, request)
def _inject_session(self, base_url, session_id, request):
"""Inietta sessione e geo-targeting nell'username del proxy."""
# base_url: http://user:pass@gate.proxyhat.com:8080
country = request.meta.get('proxy_country', 'US')
# Risultato: http://user-country-US-session-sess-123:pass@gate.proxyhat.com:8080
parts = base_url.split('://')
protocol = parts[0]
rest = parts[1]
credentials, host = rest.split('@')
user, password = credentials.split(':')
new_user = f'{user}-country-{country}-session-{session_id}'
return f'{protocol}://{new_user}:{password}@{host}'
def process_request(self, request, spider):
proxy = self._build_proxy_url(request)
request.meta['proxy'] = proxy
request.meta['proxy_country'] = request.meta.get('proxy_country', 'US')
# Traccia il tentativo corrente
retry_times = request.meta.get('retry_times', 0)
request.meta['retry_times'] = retry_times
return None
def process_response(self, request, response, spider):
# Rileva ban — status 403, 429, o CAPTCHA pages
if response.status in (403, 429):
spider.logger.warning('Ban rilevato: %s status=%d', response.url, response.status)
self.ban_counts[fingerprint(request)] += 1
# Se non abbiamo superato il max retry, ritenta con nuovo proxy
if request.meta.get('retry_times', 0) < self.max_retries:
request.meta['retry_times'] = request.meta.get('retry_times', 0) + 1
# Forza nuova sessione rimuovendo il mapping
fp = fingerprint(request)
if fp in self.request_session:
del self.request_session[fp]
request.dont_filter = True
return request # ritenta
else:
spider.logger.error('Max retry raggiunto per %s', request.url)
return response
def process_exception(self, request, exception, spider):
# Timeout, connection errors — ritenta con nuovo proxy
if request.meta.get('retry_times', 0) < self.max_retries:
request.meta['retry_times'] = request.meta.get('retry_times', 0) + 1
fp = fingerprint(request)
if fp in self.request_session:
del self.request_session[fp]
request.dont_filter = True
spider.logger.info('Retry per eccezione: %s → %s', request.url, exception)
return request
return NoneQuesto middleware fa tre cose fondamentali: assegna un proxy diverso per ogni richiesta usando le flag di sessione di ProxyHat, mantiene sessioni sticky per un timeout configurabile, e ruota automaticamente il proxy quando rileva un ban (403/429) o un'eccezione di connessione.
Configurazione in settings.py:
# settings.py
PROXYHAT_URL = 'http://USERNAME:PASSWORD@gate.proxyhat.com:8080'
PROXY_MAX_RETRIES = 3
PROXY_STICKY_TIMEOUT = 600 # 10 minuti in secondi
DOWNLOADER_MIDDLEWARES = {
'myproject.middlewares.ProxyRotationMiddleware': 543,
'scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware': 560,
}
# Disabilita il RetryMiddleware di default — il nostro gestisce i retry
RETRY_ENABLED = False
# Aumenta la concorrenza per sfruttare il pool residenziale
CONCURRENT_REQUESTS = 32
CONCURRENT_REQUESTS_PER_DOMAIN = 8
DOWNLOAD_TIMEOUT = 30Nota importante: disabilitiamo il RetryMiddleware di Scrapy perché il nostro middleware gestisce i retry con rotazione proxy. Se entrambi fossero attivi, ogni richiesta bannata verrebbe ritentata due volte — una dal nostro middleware e una dal RetryMiddleware di default.
scrapy-rotating-proxies vs Middleware Personalizzato
La community offre scrapy-rotating-proxies, un pacchetto che gestisce rotazione e ban detection. Vale la pena considerarne i pro e i contro:
| Caratteristica | scrapy-rotating-proxies | Middleware Personalizzato |
|---|---|---|
| Configurazione | Lista di proxy in settings.py | URL singolo con rotazione lato provider |
| Ban detection | Basata su status code configurabili | Personalizzabile per sito (403, 429, CAPTCHA) |
| Sticky sessions | Non supportate nativamente | Supporto completo via flag session |
| Geo-targeting | Manuale — devi filtrare i proxy per paese | Per-request via flag country |
| Pool management | Gestisci tu il pool di IP | Delegato al provider (ProxyHat) |
| Proxy residenziali | Nessun supporto specifico | Ottimizzato per proxy residenziali |
| Manutenzione | Pacchetto terze parti, aggiornamenti rari | Controlli tu il codice |
Quando usare scrapy-rotating-proxies: hai una lista di proxy datacenter gratuiti o acquistati e vuoi rotazione rapida senza scrivere codice.
Quando usare un middleware personalizzato: usi proxy residenziali con sessioni sticky, geo-targeting per-request, e vuoi pieno controllo sulla logica di retry. Per la maggior parte dei casi di produzione con proxy a pagamento, il middleware personalizzato è la scelta migliore.
Gestire i Retry con Rotazione Proxy
Il pattern più comune di fallimento: una richiesta riceve status 403, il RetryMiddleware di Scrapy ritenta la stessa richiesta con lo stesso IP, e riceve di nuovo 403. È un ciclo inutile.
Il nostro middleware risolve questo problema rimuovendo il mapping della sessione quando rileva un ban, forzando la richiesta successiva a ottenere un nuovo IP. Ma c'è di più — puoi combinare questo con il RetryMiddleware di Scrapy per avere una strategia a due livelli:
# middlewares.py — RetryMiddleware personalizzato che ruota il proxy
from scrapy.downloadermiddlewares.retry import RetryMiddleware as BaseRetry
from scrapy.utils.request import fingerprint
class ProxyAwareRetryMiddleware(BaseRetry):
"""Retry che invalida la sessione proxy prima di ritentare."""
def __init__(self, settings):
super().__init__(settings)
self.max_retry_times = settings.getint('RETRY_TIMES', 3)
def process_response(self, request, response, spider):
if request.meta.get('dont_retry', False):
return response
# Se è un ban, invalida la sessione proxy
if response.status in (403, 429, 503):
fp = fingerprint(request)
# Segnala al ProxyRotationMiddleware di usare un nuovo IP
if fp in spider.proxy_middleware.request_session if hasattr(spider, 'proxy_middleware') else {}:
del spider.proxy_middleware.request_session[fp]
spider.logger.info('Ban rilevato, ruoto proxy per %s', request.url)
return super().process_response(request, response, spider)
def process_exception(self, request, exception, spider):
# Timeout e connection errors — ruota proxy
fp = fingerprint(request)
if hasattr(spider, 'proxy_middleware') and fp in spider.proxy_middleware.request_session:
del spider.proxy_middleware.request_session[fp]
return super().process_exception(request, exception, spider)Con questo approccio, ogni retry usa automaticamente un IP diverso. Configuralo così:
DOWNLOADER_MIDDLEWARES = {
'myproject.middlewares.ProxyRotationMiddleware': 543,
'myproject.middlewares.ProxyAwareRetryMiddleware': 550,
}
# NON disabilitare RETRY_ENABLED qui — il nostro retry middleware lo usa
RETRY_TIMES = 3
RETRY_HTTP_CODES = [403, 429, 500, 502, 503, 504]Siti JS-Heavy: scrapy-splash e scrapy-playwright con Proxy
I siti moderni spesso richiedono esecuzione JavaScript per renderizzare il contenuto. Scrapy gestisce questo scenario tramite scrapy-splash (per Splash, un renderer headless) o scrapy-playwright (per Playwright, che controlla browser reali).
Configurare scrapy-playwright con proxy residenziali
scrapy-playwright è diventato lo standard de facto per il rendering JavaScript in Scrapy. Ecco come integrare i proxy residenziali:
# settings.py
DOWNLOAD_HANDLERS = {
'http': 'scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler',
'https': 'scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler',
}
TWISTED_REACTOR = 'twisted.internet.asyncioreactor.AsyncioSelectorReactor'
PLAYWRIGHT_BROWSER_TYPE = 'chromium'
# Il proxy per Playwright va configurato per-request nel meta
PLAYWRIGHT_LAUNCH_OPTIONS = {
'headless': True,
}Nello spider, assegni il proxy per ogni richiesta tramite il meta playwright:
import scrapy
import random
class JSSpider(scrapy.Spider):
name = 'js_spider'
def start_requests(self):
urls = ['https://example.com/data']
for url in urls:
session_id = f'sess-{random.randint(100000, 999999)}'
yield scrapy.Request(
url=url,
callback=self.parse,
meta={
'playwright': True,
'playwright_include_page': False,
'playwright_proxy': {
'server': 'http://gate.proxyhat.com:8080',
'username': f'USERNAME-country-US-session-{session_id}',
'password': 'PASSWORD',
},
'proxy_country': 'US',
},
)
def parse(self, response):
for item in response.css('.data-item'):
yield {
'title': item.css('h2::text').get(),
'price': item.css('.price::text').get(),
}Per scrapy-splash, il pattern è simile ma passi il proxy attraverso i parametri di Splash:
# Con scrapy-splash
yield scrapy.Request(
url=target_url,
callback=self.parse,
meta={
'splash': {
'args': {
'render': 1,
'proxy': 'http://gate.proxyhat.com:8080',
},
'endpoint': 'render.html',
}
}
)Attenzione: con scrapy-splash, il proxy è configurato lato server Splash, non per-request. Se hai bisogno di rotazione per-request, devi usare un pool di istanze Splash o passare a scrapy-playwright che supporta proxy per-request nativamente.
Deployment: Scrapyd, Docker, e Oltre
Opzione 1: Docker + cron — semplice ed efficace
Per la maggior parte dei casi d'uso, un container Docker con cron è sufficiente:
# Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
# cron job per eseguire lo spider ogni ora
RUN echo "0 * * * * cd /app && scrapy crawl my_spider >> /var/log/spider.log 2>&1" | crontab -
CMD ["crontab", "-l", "&&", "tail", "-f", "/var/log/spider.log"]Esegui con docker run -d --name spider my-spider-image. Per scaling orizzontale, usa Docker Compose con multiple istanze, ognuna con un diverso PROXYHAT_URL per distribuire il carico.
Opzione 2: Scrapyd — gestione centralizzata
Scrapyd offre un'API per deployare e schedulare spider. È utile se hai multipli spider che girano su diverse macchine:
# Deploy su Scrapyd
scrapyd-deploy
# Schedula un job
curl http://localhost:6800/schedule.json -d project=myproject -d spider=my_spiderOpzione 3: ScrapeOps — monitoraggio avanzato
ScrapeOps fornisce un dashboard per monitorare job, success rate e log. Si integra bene con Scrapy tramite il loro scrapeops-scrapy-proxy-sdk, ma se usi ProxyHat come provider, il nostro middleware personalizzato è più flessibile.
Monitoraggio: Success Rate, Ban Detection, e Per-IP Stats
In produzione, devi sapere quali IP funzionano e quali sono bannati. Ecco un'estensione Scrapy che raccoglie statistiche per paese e tipo di risposta:
# extensions.py
from scrapy import signals
from collections import defaultdict
class ProxyStatsExtension:
"""Raccoglie statistiche per-request sui proxy."""
def __init__(self, stats):
self.stats = stats
self.country_stats = defaultdict(lambda: {'success': 0, 'ban': 0, 'error': 0})
@classmethod
def from_crawler(cls, crawler):
ext = cls(crawler.stats)
crawler.signals.connect(ext.request_scheduled, signal=signals.request_scheduled)
crawler.signals.connect(ext.response_received, signal=signals.response_received)
crawler.signals.connect(ext.request_dropped, signal=signals.request_dropped)
return ext
def request_scheduled(self, request, spider):
country = request.meta.get('proxy_country', 'unknown')
self.stats.inc_value(f'proxy/request/{country}')
def response_received(self, response, spider):
country = response.meta.get('proxy_country', 'unknown')
if response.status >= 400:
self.country_stats[country]['ban'] += 1
self.stats.inc_value(f'proxy/ban/{country}')
else:
self.country_stats[country]['success'] += 1
self.stats.inc_value(f'proxy/success/{country}')
def request_dropped(self, request, spider):
country = request.meta.get('proxy_country', 'unknown')
self.country_stats[country]['error'] += 1
self.stats.inc_value(f'proxy/error/{country}')Abilita l'estensione in settings.py:
EXTENSIONS = {
'myproject.extensions.ProxyStatsExtension': 500,
}Con queste stats puoi monitorare il tasso di successo per paese — se i proxy US hanno un success rate del 95% ma quelli DE solo del 60%, sai dove ottimizzare.
Ban detection avanzata
Non tutti i ban restituiscono 403. Alcuni siti restituiscono 200 con una pagina CAPTCHA, o redirect a una pagina di verifica. Aggiungi questi pattern al tuo middleware:
# Nel process_response del ProxyRotationMiddleware
def _is_ban(self, response):
"""Rileva ban che non sono ovvi status code."""
# Status code ban
if response.status in (403, 429, 503):
return True
# CAPTCHA pages
captcha_signals = ['captcha', 'cf-challenge', 'recaptcha', 'hcaptcha']
body_lower = response.text[:5000].lower()
if any(sig in body_lower for sig in captcha_signals):
return True
# Redirect a pagine di verifica
if response.status == 200 and '/verify' in response.url:
return True
return FalsePattern di Scaling per Produzione
Quando passi da uno spider che gira su un laptop a un sistema che processa milioni di pagine al giorno, i pattern di scaling diventano critici:
Concorrenza e rate limiting
Con proxy residenziali, puoi aumentare la concorrenza significativamente perché ogni richiesta usa un IP diverso:
CONCURRENT_REQUESTS = 64
CONCURRENT_REQUESTS_PER_DOMAIN = 16
DOWNLOAD_DELAY = 0.5 # ancora utile per non saturare la banda
# AutoThrottle per adattarsi alla velocità del sito
AUTOTHROTTLE_ENABLED = True
AUTOTHROTTLE_START_DELAY = 0.5
AUTOTHROTTLE_MAX_DELAY = 5
AUTOTHROTTLE_TARGET_CONCURRENCY = 16Container fleet con Docker Compose
Per scaling orizzontale, esegui multiple istanze dello spider con diverse configurazioni geo:
# docker-compose.yml
version: '3.8'
services:
spider-us:
build: .
environment:
- PROXY_COUNTRY=US
- PROXYHAT_URL=http://user-country-US:pass@gate.proxyhat.com:8080
command: scrapy crawl my_spider
spider-de:
build: .
environment:
- PROXY_COUNTRY=DE
- PROXYHAT_URL=http://user-country-DE:pass@gate.proxyhat.com:8080
command: scrapy crawl my_spider
spider-uk:
build: .
environment:
- PROXY_COUNTRY=UK
- PROXYHAT_URL=http://user-country-GB:pass@gate.proxyhat.com:8080
command: scrapy crawl my_spiderItem pipelines e deduplicazione
Con multiple istanze che girano in parallelo, hai bisogno di deduplicazione a livello di item. Usa un DuplicateFilter basato su Redis:
# pipelines.py
import redis
class RedisDuplicateFilterPipeline:
def __init__(self, redis_url):
self.redis = redis.from_url(redis_url)
self.ttl = 86400 # 24 ore
@classmethod
def from_crawler(cls, crawler):
return cls(crawler.settings.get('REDIS_URL', 'redis://localhost:6379'))
def process_item(self, item, spider):
item_id = item.get('url') or item.get('id')
if self.redis.setnx(f'dedup:{item_id}', 1):
self.redis.expire(f'dedup:{item_id}', self.ttl)
return item
raise DropItem(f'Duplicato: {item_id}')Considerazioni Etiche e Legali
L'uso di proxy per web scraping solleva questioni importanti:
- Rispetta
robots.txt— usaROBOTSTXT_OBEY = Truea meno che tu non abbia una buona ragione per non farlo. - Rate limiting responsabile — anche con proxy residenziali, non bombardare un server. Usa
DOWNLOAD_DELAYe AutoThrottle. - GDPR e CCPA — se raccogli dati personali, assicurati di essere in conformità. I proxy non ti esentano dalle obbligazioni legali.
- ToS dei siti — molti siti proibiscono lo scraping nei loro Termini di Servizio. Valuta i rischi legali.
Per approfondire le best practice per lo scraping etico, consulta la nostra guida su web scraping responsabile.
Key Takeaways
1. Il middleware è il punto di estensione corretto. Non hackerare il proxy URL nei tuoi spider — implementa un middleware che centralizza la logica di rotazione.
2. Rotazione per-request, non per-spider. Usa
request.meta['proxy']per assegnare un IP diverso per ogni richiesta, non un proxy globale.3. I retry devono ruotare il proxy. Ritentare con lo stesso IP bannato è inutile. Invalida la sessione prima di ogni retry.
4. I proxy residenziali richiedono sessioni sticky. Per login, carrelli e paginazioni, mantieni la stessa sessione IP per un timeout configurabile.
5. Monitora i tassi di successo per paese. Se i proxy di un paese hanno un success rate basso, investiga — potrebbe essere un problema di targeting o un blocco regionale.
6. scrapy-playwright > scrapy-splash per proxy per-request. Playwright supporta proxy per-request nativamente, Splash no.
Prossimi Passi
Ora hai tutte le componenti per un sistema Scrapy con proxy residenziali di livello production. Il prossimo passo è configurare il tuo account ProxyHat e testare il middleware contro il tuo sito target.
- Piani ProxyHat — scegli il pool residenziale adatto al tuo volume.
- Posizioni ProxyHat — verifica la copertura geo per i tuoi target.
- SERP tracking con proxy — caso d'uso specifico per SERP scraping.
Se hai domande sull'integrazione, il team ProxyHat è disponibile per supporto tecnico direttamente dalla dashboard.






