Il parser su Scrapy va in timeout, il pool di proxy si esaurisce molto più velocemente del previsto e i log sono pieni di risposte 407 e 403 — un quadro familiare? Nel 90% dei casi il problema non è nei proxy stessi, ma in come è scritto il DownloaderMiddleware. Analizziamo cinque degli errori più comuni nel middleware che trasformano il traffico costoso in richieste spazzatura e mostriamo come correggerli con il codice.
Errore 1: rotazione primitiva senza considerare lo stato dei proxy
La costruzione più comune che si può trovare nei tutorial è random.choice(PROXY_LIST) all'interno di process_request. Il problema è che tale rotazione non sa quale proxy ha appena ricevuto un ban e quale è ancora attivo. Di conseguenza, il parser continua a inviare richieste attraverso un IP già bloccato, riceve 403/429, fa retry — e sceglie di nuovo lo stesso indirizzo, perché la selezione è casuale e non esclude i nodi "cattivi".
L'approccio corretto è tenere traccia dello stato di ogni proxy: numero di richieste riuscite, numero di errori, tempo dell'ultimo utilizzo. Ecco una versione minima funzionante:
import random
import time
class ProxyPool:
def __init__(self, proxies):
self.proxies = {p: {"fails": 0, "last_used": 0, "banned_until": 0} for p in proxies}
def get_proxy(self):
now = time.time()
available = [
p for p, state in self.proxies.items()
if state["banned_until"] < now
]
if not available:
# se tutti sono bannati, resettiamo il ban più "vecchio"
available = list(self.proxies.keys())
return random.choice(available)
def mark_fail(self, proxy, cooldown=300):
self.proxies[proxy]["fails"] += 1
self.proxies[proxy]["banned_until"] = time.time() + cooldown
def mark_success(self, proxy):
self.proxies[proxy]["fails"] = 0
self.proxies[proxy]["last_used"] = time.time()
Questo pool esclude gli IP bannati durante il periodo di "raffreddamento" (cooldown) e li restituisce in circolazione successivamente. Questo riduce già il consumo di traffico di diversi ordini di grandezza, perché non si colpisce lo stesso nodo bloccato in continuazione.
Errore 2: gestione errata di retry e codici di stato
Il secondo errore tipico è utilizzare il RetryMiddleware standard di Scrapy senza modifiche. Per impostazione predefinita, ripete la richiesta sullo stesso proxy che ha fallito, a meno che non si sia esplicitamente miscelata la sostituzione del proxy in process_exception. Di conseguenza, otteniamo il classico quadro: 3 retry, 3 ban, la richiesta fallisce comunque e il traffico è già stato speso.
Un secondo punto è che non tutti i codici di stato devono essere ripetuti allo stesso modo. 429 (Too Many Requests) richiede una pausa e un cambio di IP, 403 di solito indica un ban di un proxy specifico (è necessaria una sostituzione immediata), mentre 5xx è spesso un problema temporaneo sul lato del server, quindi si può ripetere sulla stessa proxy. Se si mettono tutto in un unico gruppo, il middleware o brucia i proxy in modo troppo aggressivo, o aspetta troppo a lungo dove sarebbe stato necessario cambiare immediatamente l'IP.
class SmartRetryMiddleware:
def __init__(self, pool):
self.pool = pool
def process_response(self, request, response, spider):
proxy = request.meta.get("proxy")
if response.status in (403, 407):
if proxy:
self.pool.mark_fail(proxy, cooldown=600)
new_request = request.copy()
new_request.meta["proxy"] = self.pool.get_proxy()
new_request.dont_filter = True
return new_request
if response.status == 429:
if proxy:
self.pool.mark_fail(proxy, cooldown=120)
new_request = request.copy()
new_request.meta["proxy"] = self.pool.get_proxy()
new_request.dont_filter = True
return new_request
if proxy:
self.pool.mark_success(proxy)
return response
Qui è importante separare il cooldown in base al tipo di errore: ban severo (403/407) — lunga pausa, limite di frequenza (429) — breve. Questo fa risparmiare decine di percentuale di traffico durante le lunghe esecuzioni.
Errore 3: assenza di sessioni sticky per siti con autorizzazione
Se il parser lavora con un sito che ha login, carrello, paginazione con salvataggio dello stato o captcha con verifica tramite IP — cambiare proxy per ogni richiesta rompe la sessione. Il sito vede che la richiesta n. 1 è arrivata da un IP, mentre la richiesta n. 2 (nell'ambito della stessa sessione cookie) proviene da un altro, e questo attiva la protezione contro il bot immediatamente, anche se entrambi gli IP sono "puliti".
La soluzione è associare un proxy a una singola sessione logica (ad esempio, a un account specifico o a una catena di richieste all'interno di un dominio) per un tempo fisso, invece di cambiarlo per ogni richiesta. Questo è chiamato sessione sticky.
class StickySessionMiddleware:
def __init__(self, pool, ttl=600):
self.pool = pool
self.ttl = ttl
self.sessions = {} # session_id -> (proxy, expires_at)
def process_request(self, request, spider):
session_id = request.meta.get("session_id")
if not session_id:
return
now = time.time()
session = self.sessions.get(session_id)
if session and session[1] > now:
request.meta["proxy"] = session[0]
else:
proxy = self.pool.get_proxy()
self.sessions[session_id] = (proxy, now + self.ttl)
request.meta["proxy"] = proxy
Per compiti che richiedono stabilità dell'IP durante l'intera sessione — autorizzazione, lavoro con il pannello personale, moduli multi-step — i migliori sono proxy residenziali con supporto per sessioni: consentono di mantenere lo stesso IP in uscita per diversi minuti o ore, per poi cambiarlo in modo controllato, non casuale per ogni richiesta.
Errore 4: trasmissione errata dell'autorizzazione del proxy
Il quarto errore è tecnico, ma si presenta in quasi ogni secondo progetto. Gli sviluppatori trasmettono il nome utente e la password del proxy direttamente nell'URL del tipo http://user:pass@ip:port tramite request.meta["proxy"]. Questo funziona nella maggior parte dei casi, ma quando si utilizza un proxy tramite tunnel HTTPS o quando si lavora con alcuni fornitori, questo metodo di autorizzazione non viene elaborato correttamente dal HttpProxyMiddleware standard, e le richieste falliscono con 407 Proxy Authentication Required, anche se le credenziali sono corrette.
Un modo più affidabile è trasmettere esplicitamente l'intestazione Proxy-Authorization, codificata in base64:
import base64
class ProxyAuthMiddleware:
def process_request(self, request, spider):
proxy = request.meta.get("proxy")
if not proxy:
return
# proxy senza credenziali nell'URL
request.meta["proxy"] = proxy
user = request.meta.get("proxy_user")
password = request.meta.get("proxy_pass")
if user and password:
credentials = f"{user}:{password}"
encoded = base64.b64encode(credentials.encode()).decode()
request.headers["Proxy-Authorization"] = f"Basic {encoded}"
Questo approccio funziona in modo più stabile quando si scala a centinaia di richieste parallele e non dipende da come una specifica versione della libreria analizza gli URL con credenziali incorporate. Questo è particolarmente critico quando si lavora con proxy mobili, dove l'autenticazione è spesso legata a una whitelist di IP o a un rigoroso controllo delle intestazioni.
Errore 5: assenza di monitoraggio e registrazione dei ban
L'ultimo e, forse, l'errore più costoso in termini di conseguenze è l'assenza di registrazione di quali proxy vengono bannati, quanto spesso e su quali domini. Senza questi dati, è impossibile capire cosa stia realmente consumando il traffico: se il pool di proxy si è esaurito su un sito specifico, o se il problema è nel parser stesso (richieste troppo frequenti, assenza di ritardi, intestazioni sospette).
Il set minimo di metriche che vale la pena registrare nel middleware:
- Numero di richieste per ogni proxy durante la sessione di parsing
- Numero e codici di errore (403, 407, 429, 5xx) per ogni proxy
- Tempo di vita del proxy fino al primo ban
- Domini sui quali i ban si verificano più frequentemente
import logging
import json
logger = logging.getLogger("proxy_stats")
class ProxyStatsMiddleware:
def __init__(self):
self.stats = {}
def process_response(self, request, response, spider):
proxy = request.meta.get("proxy", "unknown")
domain = request.url.split("/")[2]
key = f"{proxy}|{domain}"
entry = self.stats.setdefault(key, {"requests": 0, "errors": 0})
entry["requests"] += 1
if response.status in (403, 407, 429):
entry["errors"] += 1
if entry["requests"] % 50 == 0:
logger.info(json.dumps(self.stats))
return response
Senza queste statistiche, qualsiasi tentativo di "ottimizzare" il middleware si riduce a congetture. Con esse si vede chiaramente: se, ad esempio, un dominio specifico banna l'80% del pool di proxy dopo le prime 10 richieste — il problema non è nei proxy, ma nel pattern delle richieste (assenza di rotazione dell'User-Agent, frequenza troppo alta, assenza di ritardi tra le richieste).
Esempio funzionante di middleware completo
Mettiamo tutto insieme in settings.py — l'ordine del middleware è critico, perché dipende dall'ordine in cui vengono applicati i controlli:
DOWNLOADER_MIDDLEWARES = {
"myproject.middlewares.ProxyAuthMiddleware": 350,
"myproject.middlewares.StickySessionMiddleware": 400,
"myproject.middlewares.SmartRetryMiddleware": 550,
"myproject.middlewares.ProxyStatsMiddleware": 900,
}
RETRY_ENABLED = False # disabilitiamo il retry standard, utilizziamo il nostro
DOWNLOAD_TIMEOUT = 15
CONCURRENT_REQUESTS_PER_DOMAIN = 8
Fai attenzione a RETRY_ENABLED = False — questo è fondamentale, altrimenti il meccanismo di retry integrato di Scrapy entrerà in conflitto con la tua logica di cambio proxy, e le richieste inizieranno a essere duplicate o ripetute due volte. È anche importante limitare CONCURRENT_REQUESTS_PER_DOMAIN — una concorrenza troppo alta su un dominio, anche con rotazione dei proxy, appare sospetta per i sistemi anti-bot.
Quale tipo di proxy scegliere per Scrapy
Il middleware risolve metà del problema, l'altra metà è la scelta corretta del pool di proxy per il compito. Di seguito un confronto per scenari tipici di parsing.
| Tipo di proxy | Quando utilizzare | Vantaggi | Svantaggi |
|---|---|---|---|
| Proxy dei data center | Parsing massivo di pagine aperte senza una rigorosa protezione anti-bot | Alta velocità, basso costo del traffico | Facilmente rilevabili, spesso in blacklist |
| Proxy residenziali | Parsing di marketplace, siti con protezione JS, autorizzazione | IP reali, basso tasso di ban, supporto per sessioni sticky | Velocità inferiore rispetto ai DC |
| Proxy mobili | Parsing di versioni mobili di siti e API con protezione anti-bot rigorosa | Massima fiducia da parte dei siti target | Il costo del traffico più alto |
Regola pratica: per il parsing di pagine statiche senza una forte protezione, sono adatti proxy dei data center economici in combinazione con un middleware ben progettato di questo articolo. Se il sito utilizza Cloudflare, PerimeterX, DataDome o sistemi simili — gli IP dei data center verranno bannati quasi immediatamente, e in questo caso è più vantaggioso passare direttamente a un pool residenziale, risparmiando tempo nel debug del middleware.
Checklist prima di lanciare il parser in produzione
- Il middleware esclude i proxy bannati durante il cooldown, invece di sceglierli casualmente
- La logica di retry distingue i tipi di errore (403/407 vs 429 vs 5xx) con cooldown diversi
- Per compiti di sessione viene utilizzata l'associazione sticky del proxy, non il cambio per ogni richiesta
- L'autorizzazione del proxy viene trasmessa tramite l'intestazione Proxy-Authorization, non solo tramite URL
- Viene mantenuta la statistica dei ban per domini e proxy per diagnosticare problemi
- Il RetryMiddleware standard di Scrapy è disabilitato, per non entrare in conflitto con la logica personalizzata
- La concorrenza è limitata a valori ragionevoli, non impostata al massimo
Conclusione
La maggior parte dei problemi con il consumo di traffico proxy in Scrapy non si risolve acquistando un pool di IP più grande, ma correggendo la logica del middleware: separando gli errori per tipo, utilizzando sessioni sticky per scenari complessi, autorizzazioni corrette e monitoraggio costante dei ban. Il codice di questo articolo può essere utilizzato come base e adattato a progetti specifici — la struttura rimane funzionante sia per piccoli parser che per installazioni distribuite di Scrapy-Cluster.
Se il middleware è già configurato correttamente e i ban continuano a verificarsi troppo frequentemente — probabilmente il problema è nella qualità stessa del pool di IP. Per il parsing di siti con protezioni anti-bot avanzate, vale la pena provare proxy residenziali con supporto per sessioni — riducono notevolmente il tasso di attivazione della protezione rispetto agli indirizzi dei data center mantenendo la stessa logica del middleware.