Scrapy reste le cheval de bataille des crawls en production en 2026 — et reste le framework où la configuration de proxy déroute le plus les gens, parce qu'il existe quatre endroits différents où brancher un proxy et que trois d'entre eux sont inadaptés à la plupart des projets. Ce guide vous donne le bon : un petit middleware personnalisé avec routage par requête, sessions sticky, détection de bannissements et un comportement de retry sensé.
Si vous utilisez plutôt requests/httpx/aiohttp tout court, consultez Comment faire tourner des proxies en Python. Cet article est spécifique à Scrapy.
Pour une passerelle résidentielle rotative, la configuration minimale viable tient en une ligne par requête — sans middleware :
def start_requests(self):
for url in self.urls:
yield scrapy.Request(
url,
meta={"proxy": "http://USERNAME:[email protected]:913"},
)
Le HttpProxyMiddleware intégré de Scrapy lit request.meta["proxy"] et gère l'authentification à partir de l'URL. La passerelle fait tourner l'IP de sortie à votre place. Si c'est tout ce dont vous avez besoin, arrêtez-vous ici. Le reste de ce guide concerne les cas où vous avez besoin de contrôle : sessions sticky, routage par pays, rotation consciente des bannissements et réglage de la concurrence.
Placez ceci dans middlewares.py. Il attribue des sessions sticky par domaine, fait tourner en cas de bannissement et étiquette chaque requête pour que vous puissiez déboguer quelle session a récupéré quoi :
import random
import string
GATEWAY = "us.jibaoproxy.com:913"
USERNAME = "USERNAME" # move to settings.py / env in real projects
PASSWORD = "PASSWORD"
def _new_session(n=8):
return "".join(random.choices(string.ascii_lowercase + string.digits, k=n))
class JibaoProxyMiddleware:
"""Sticky session per domain; rotate session on ban."""
def __init__(self):
self.sessions = {} # domain -> session id
def _proxy_url(self, session_id):
user = f"{USERNAME}-session-{session_id}"
return f"http://{user}:{PASSWORD}@{GATEWAY}"
def process_request(self, request, spider):
domain = request.url.split("/")[2]
session = self.sessions.setdefault(domain, _new_session())
request.meta["proxy"] = self._proxy_url(session)
request.meta["proxy_session"] = session
def rotate(self, domain):
"""Call when a session is burned."""
self.sessions[domain] = _new_session()
Et un middleware de téléchargement complémentaire qui détecte les bannissements et réessaie sur une session neuve :
from scrapy.downloadermiddlewares.retry import RetryMiddleware
from scrapy.utils.response import response_status_message
BAN_CODES = {403, 429}
BAN_MARKERS = (b"captcha", b"access denied", b"unusual traffic")
class BanAwareRetryMiddleware(RetryMiddleware):
def process_response(self, request, response, spider):
banned = (
response.status in BAN_CODES
or any(m in response.body[:2048].lower() for m in BAN_MARKERS)
)
if banned:
domain = request.url.split("/")[2]
proxy_mw = spider.crawler.engine.downloader.middleware.middlewares
for mw in proxy_mw:
if hasattr(mw, "rotate"):
mw.rotate(domain) # burn the session
reason = response_status_message(response.status)
return self._retry(request, reason, spider) or response
return super().process_response(request, response, spider)
Branchez les deux dans settings.py :
DOWNLOADER_MIDDLEWARES = {
"myproject.middlewares.JibaoProxyMiddleware": 350,
"scrapy.downloadermiddlewares.retry.RetryMiddleware": None, # replace stock retry
"myproject.middlewares.BanAwareRetryMiddleware": 550,
}
RETRY_TIMES = 2
La priorité compte : le middleware de proxy doit s'exécuter avant le HttpProxyMiddleware de Scrapy (750), donc n'importe quelle valeur sous 750 fonctionne ; 350 le maintient tôt et prévisible.
| Type de crawl | Mode | Implémentation |
|---|---|---|
| Collecte de pages sans état | Rotatif | Identifiant simple, la passerelle fait tourner par requête |
| Login + crawl derrière authentification | Sticky par compte | -session-{account_id}, ne jamais faire tourner en plein login |
| Listings à forte pagination | Sticky par domaine, rotation en cas de bannissement | Le middleware ci-dessus |
| Tarifs spécifiques par géographie | Rotatif + épinglage de pays | Paramètres de style USERNAME-country-de |
Traitement plus approfondi de ce compromis : Sessions de proxy sticky vs rotatives.
Les valeurs par défaut de Scrapy sont calibrées pour un crawling poli sur une seule IP. Derrière un pool rotatif vous pouvez pousser beaucoup plus fort — mais les limites par domaine comptent toujours parce que la cible voit le comportement agrégé :
# settings.py - sane starting point behind a residential pool
CONCURRENT_REQUESTS = 64
CONCURRENT_REQUESTS_PER_DOMAIN = 8 # what the target experiences
DOWNLOAD_DELAY = 0.25 # jitter applied per slot
RANDOMIZE_DOWNLOAD_DELAY = True # 0.5x-1.5x the delay
AUTOTHROTTLE_ENABLED = True
AUTOTHROTTLE_TARGET_CONCURRENCY = 6.0
DOWNLOAD_TIMEOUT = 30
ROBOTSTXT_OBEY = True
Augmentez CONCURRENT_REQUESTS_PER_DOMAIN uniquement après avoir observé votre taux de 403 au niveau actuel pendant quelques milliers de requêtes. Passer de 8 → 32 parce que « de toute façon les proxies tournent » est exactement la façon dont les gens brûlent des GB en retries.
IMAGES_STORE pour télécharger via un proxy de centre de données ou une connexion directe si le CDN s'en moque — les médias représentent l'essentiel de vos GB et sont rarement protégés.HTTPCACHE_ENABLED = True pendant que vous itérez sur les parsers rejoue les réponses depuis le disque au lieu de les re-télécharger via le pool. Ce seul réglage divise généralement par deux la facture de bande passante d'un projet.COMPRESSION_ENABLED = True (par défaut) — le HTML gzippé est 5–10x plus petit sur le réseau.407 Proxy Authentication RequiredLes identifiants ne sont pas arrivés au proxy. Mettez-les dans l'URL (http://user:pass@host:port) dans meta["proxy"] — Scrapy les parse et définit Proxy-Authorization pour vous. Configurer l'en-tête manuellement et utiliser des identifiants dans l'URL provoque des bizarreries de double authentification ; choisissez l'un ou l'autre.
TunnelError: Could not open CONNECT tunnelPresque toujours un host/port mal saisi, ou une cible HTTPS via un endpoint qui n'autorise pas CONNECT sur ce port. Vérifiez d'abord avec curl -x en dehors de Scrapy.
Votre session sticky a fait de l'usure, ou votre cadence par domaine est trop élevée. Le middleware conscient des bannissements ci-dessus gère le premier cas ; baissez CONCURRENT_REQUESTS_PER_DOMAIN pour le second. Si c'est une cible qui vérifie JA4, la pile TLS de Scrapy elle-même peut être le signal révélateur — voyez JA3/JA4 expliqués pour comprendre pourquoi aucun proxy ne corrige cela.
meta["proxy"] avec l'URL de la passerelle est toute la configuration.Sessions résidentielles rotatives et sticky sur une seule passerelle. 500 Mo de trafic gratuit pour crawler.
Démarrer l'essai gratuitLes nouveaux utilisateurs reçoivent 500 Mo à l'inscription, plus un bonus sur la première recharge. Offre à durée limitée.