Le parseur sur Scrapy tombe en timeout, le pool de proxies s'épuise beaucoup plus vite que prévu, et les logs sont remplis de réponses 407 et 403 — un tableau familier ? Dans 90 % des cas, le problème ne vient pas des proxies eux-mêmes, mais de la façon dont le DownloaderMiddleware est écrit. Nous analysons cinq des erreurs les plus courantes dans le middleware qui transforment un trafic coûteux en requêtes inutiles, et montrons comment les corriger avec du code.
Erreur 1 : rotation primitive sans prise en compte de l'état des proxies
La construction la plus courante que l'on trouve dans les tutoriels est random.choice(PROXY_LIST) à l'intérieur de process_request. Le problème est que cette rotation ne sait pas quel proxy vient d'être banni et lequel est encore actif. En fin de compte, le parseur continue d'envoyer des requêtes via une IP déjà bloquée, reçoit des 403/429, fait un retry — et choisit à nouveau la même adresse, car le choix est aléatoire et n'exclut pas les nœuds « mauvais ».
L'approche correcte consiste à maintenir l'état de chaque proxy : le nombre de requêtes réussies, le nombre d'erreurs, le temps de dernière utilisation. Voici une version minimale fonctionnelle :
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:
# si tous sont bannis, réinitialiser le ban le plus "ancien"
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()
Ce pool exclut les IP bannies pendant le temps de « refroidissement » (cooldown) et les remet en circulation plus tard. Cela réduit déjà considérablement la consommation de trafic, car vous ne frappez pas le même nœud bloqué en boucle.
Erreur 2 : traitement incorrect des retries et des codes de statut
La deuxième erreur typique est d'utiliser le RetryMiddleware standard de Scrapy sans modification. Par défaut, il retente la requête sur le même proxy qui a échoué, à moins que vous n'ayez explicitement intégré un changement de proxy dans process_exception. En conséquence, nous obtenons le tableau classique : 3 retries, 3 bans, la requête échoue toujours, et le trafic a déjà été dépensé.
Un autre point — tous les codes de statut ne doivent pas être retentés de la même manière. 429 (Trop de requêtes) nécessite une pause et un changement d'IP, 403 signifie généralement un ban d'un proxy spécifique (un remplacement immédiat est nécessaire), tandis que 5xx — c'est souvent un problème temporaire du côté du serveur, un retry peut être effectué sur le même proxy. Si tout est mélangé, le middleware brûle soit trop agressivement les proxies, soit attend trop longtemps là où il fallait immédiatement changer d'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
Ici, il est important de séparer le cooldown par type d'erreur : un ban strict (403/407) — longue pause, limite de fréquence (429) — courte. Cela économise des dizaines de pourcents de trafic lors de longues exécutions.
Erreur 3 : absence de sessions sticky pour les sites avec autorisation
Si le parseur travaille avec un site où il y a une connexion, un panier, une pagination avec conservation de l'état ou un captcha avec vérification par IP — changer de proxy à chaque requête casse la session. Le site voit que la requête n°1 est venue d'une IP, et la requête n°2 (dans le cadre de la même session de cookie) — d'une autre, et cela déclenche instantanément la protection contre les bots, même si les deux IP sont « propres ».
La solution consiste à lier un proxy à une session logique (par exemple, à un compte spécifique ou à une chaîne de requêtes au sein d'un même domaine) pendant un temps fixe, plutôt que de le changer à chaque requête. Cela s'appelle une session 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
Pour les tâches nécessitant une stabilité de l'IP tout au long de la session — autorisation, travail avec un compte personnel, formulaires multi-étapes — les proxies résidentiels avec support de sessions sont les plus adaptés : ils permettent de maintenir la même IP sortante pendant plusieurs minutes ou heures, puis de la changer de manière contrôlée, plutôt que de manière aléatoire à chaque requête.
Erreur 4 : transmission incorrecte de l'autorisation du proxy
La quatrième erreur est technique, mais se retrouve dans presque tous les deux projets. Les développeurs transmettent le nom d'utilisateur et le mot de passe du proxy directement dans l'URL de type http://user:pass@ip:port via request.meta["proxy"]. Cela fonctionne dans la plupart des cas, mais lors de l'utilisation de proxies via un tunnel HTTPS ou avec certains fournisseurs, cette méthode d'autorisation est mal gérée par le HttpProxyMiddleware standard, et les requêtes échouent avec 407 Proxy Authentication Required, même si les identifiants sont corrects.
Une méthode plus fiable consiste à transmettre explicitement l'en-tête Proxy-Authorization, encodé en base64 :
import base64
class ProxyAuthMiddleware:
def process_request(self, request, spider):
proxy = request.meta.get("proxy")
if not proxy:
return
# proxy sans identifiants dans l'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}"
Cette approche fonctionne de manière plus stable lors de l'échelle à des centaines de requêtes parallèles et ne dépend pas de la manière dont la version spécifique de la bibliothèque analyse les URL avec des identifiants intégrés. Cela est particulièrement critique lors de l'utilisation de proxies mobiles, où l'authentification est souvent liée à une liste blanche d'IP ou à une vérification stricte des en-têtes.
Erreur 5 : pas de surveillance et de journalisation des bans
La dernière erreur, et peut-être la plus coûteuse en conséquences, est l'absence de journalisation des proxies qui sont bannis, à quelle fréquence et sur quels domaines. Sans ces données, il est impossible de comprendre ce qui consomme le trafic : soit le pool de proxies est épuisé sur un site spécifique, soit le problème vient du parseur lui-même (requêtes trop fréquentes, absence de délais, en-têtes suspects).
L'ensemble minimal de métriques à journaliser dans le middleware :
- Nombre de requêtes pour chaque proxy pendant la session de parsing
- Nombre et codes d'erreurs (403, 407, 429, 5xx) pour chaque proxy
- Durée de vie du proxy jusqu'au premier ban
- Domaines où les bans se produisent le plus souvent
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
Sans ces statistiques, toute tentative d'« optimiser » le middleware se résume à deviner. Avec cela, vous voyez clairement : si, par exemple, un domaine spécifique bannit 80 % du pool de proxies après les 10 premières requêtes — le problème ne vient pas des proxies, mais du modèle de requêtes (pas de rotation de User-Agent, fréquence trop élevée, absence de délais entre les requêtes).
Exemple de middleware fonctionnel complet
Rassemblons tout dans settings.py — l'ordre des middlewares est critique, car il détermine dans quel ordre les vérifications sont appliquées :
DOWNLOADER_MIDDLEWARES = {
"myproject.middlewares.ProxyAuthMiddleware": 350,
"myproject.middlewares.StickySessionMiddleware": 400,
"myproject.middlewares.SmartRetryMiddleware": 550,
"myproject.middlewares.ProxyStatsMiddleware": 900,
}
RETRY_ENABLED = False # désactiver le retry standard, utiliser le sien
DOWNLOAD_TIMEOUT = 15
CONCURRENT_REQUESTS_PER_DOMAIN = 8
Faites attention à RETRY_ENABLED = False — c'est fondamental, sinon le mécanisme de retry intégré de Scrapy entrera en conflit avec votre logique de changement de proxy, et les requêtes commenceront à être dupliquées ou à être retentées deux fois. Il est également important de limiter CONCURRENT_REQUESTS_PER_DOMAIN — une trop grande parallélisation sur un domaine, même avec rotation des proxies, semble suspecte pour les systèmes anti-bots.
Quel type de proxy choisir pour Scrapy
Le middleware résout la moitié du problème, l'autre moitié est le choix correct du pool de proxies pour la tâche. Voici une comparaison pour des scénarios typiques de parsing.
| Type de proxy | Quand utiliser | Avantages | Inconvénients |
|---|---|---|---|
| Proxies de data center | Parsing massif de pages ouvertes sans protection anti-bot stricte | Haute vitesse, faible coût de trafic | Facilement détectables, souvent dans des listes noires |
| Proxies résidentiels | Parsing de marketplaces, sites avec protection JS, autorisation | IP réelles, faible pourcentage de bans, support des sessions sticky | Vitesse inférieure par rapport aux DC |
| Proxies mobiles | Parsing des versions mobiles des sites et des API avec protection anti-bot stricte | Confiance maximale de la part des sites cibles | Coût de trafic le plus élevé |
Règle pratique : pour le parsing de pages statiques sans protection forte, des proxies de data center bon marché associés à un middleware bien conçu de cet article conviennent. Si le site utilise Cloudflare, PerimeterX, DataDome ou des systèmes similaires — les IP de data center seront bannies presque immédiatement, et il est alors plus avantageux de passer directement à un pool résidentiel, économisant du temps sur le débogage du middleware.
Checklist avant de lancer le parseur en production
- Le middleware exclut les proxies bannis pendant le cooldown, et ne les choisit pas aléatoirement
- La logique de retry distingue les types d'erreurs (403/407 vs 429 vs 5xx) avec des cooldown différents
- Pour les tâches de session, une liaison sticky du proxy est utilisée, et non un changement à chaque requête
- L'autorisation du proxy est transmise via l'en-tête Proxy-Authorization, et non seulement via l'URL
- Des statistiques de bans par domaine et proxy sont tenues pour diagnostiquer les problèmes
- Le RetryMiddleware standard de Scrapy est désactivé pour ne pas entrer en conflit avec la logique personnalisée
- La concurrence est limitée à des valeurs raisonnables, et non réglée au maximum
Conclusion
La plupart des problèmes de consommation de trafic proxy dans Scrapy ne se résolvent pas par l'achat d'un plus grand pool d'IP, mais par la correction de la logique du middleware : séparation des erreurs par types, sessions sticky pour des scénarios complexes, autorisation correcte et surveillance constante des bans. Le code de cet article peut être utilisé comme base et adapté à un projet spécifique — la structure reste fonctionnelle tant pour les petits parseurs que pour les installations Scrapy-Cluster distribuées.
Si le middleware est déjà configuré correctement, mais que les bans se produisent encore trop souvent — il est probable que le problème réside dans la qualité même du pool d'IP. Pour le parsing de sites avec une protection anti-bot avancée, il vaut la peine d'essayer des proxies résidentiels avec support de sessions — elles réduisent considérablement le pourcentage de déclenchement de la protection par rapport aux adresses de data center avec la même logique de middleware.