Scrapy est l'un des frameworks Python les plus puissants pour le web scraping, mais sans une configuration correcte du proxy, vos scrapers seront bloqués après quelques minutes de fonctionnement. Dans ce guide, je vais vous montrer toutes les façons d'intégrer un proxy dans Scrapy : de la configuration la plus simple aux méthodes avancées de rotation des adresses IP avec gestion automatique des erreurs.
Le matériel est basé sur une expérience réelle de scraping de grandes plateformes de commerce électronique et de sites protégés. Vous obtiendrez des exemples de code prêts à l'emploi que vous pourrez immédiatement utiliser dans vos projets.
Pourquoi Scrapy sans proxy reçoit des blocages
Les sites modernes utilisent une protection multi-niveaux contre le scraping. Même si vous avez configuré l'User-Agent et des délais entre les requêtes, votre adresse IP révèle l'automatisation par plusieurs signes :
- Fréquence des requêtes : une IP effectue 100+ requêtes par minute — un signe évident de bot
- Modèles de comportement : navigation séquentielle des pages sans transitions aléatoires
- Absence de JavaScript : Scrapy n'exécute pas de JS, ce qui est facilement détectable
- Géolocalisation : accès depuis un centre de données au lieu d'un réseau domestique
Le résultat — un ban par IP pendant plusieurs heures ou jours. Les marketplaces (Amazon, Wildberries, Ozon), les réseaux sociaux et les sites avec Cloudflare utilisent une protection particulièrement agressive. Les proxies résolvent ce problème en distribuant les requêtes entre de nombreuses adresses IP.
Important : Même avec des proxies, il est nécessaire de respecter les limites de taux. Vitesse recommandée : 1-3 requêtes par seconde par IP. Pour un scraping à haute vitesse, utilisez un pool de 50+ proxies avec rotation.
Configuration de base du proxy dans Scrapy
La manière la plus simple consiste à spécifier le proxy directement dans les paramètres de l'araignée. Cette méthode convient pour les tests ou le scraping de petits volumes de données avec un seul serveur proxy.
Méthode 1 : Via meta dans la requête
import scrapy
class MySpider(scrapy.Spider):
name = 'example'
start_urls = ['https://example.com']
def start_requests(self):
proxy = 'http://username:password@proxy.example.com:8080'
for url in self.start_urls:
yield scrapy.Request(
url=url,
callback=self.parse,
meta={'proxy': proxy}
)
def parse(self, response):
# Votre logique de scraping
self.log(f'Scraped {response.url} via {response.meta["proxy"]}')
Le format du proxy dépend du protocole et de la méthode d'authentification :
http://proxy.example.com:8080— sans authentificationhttp://user:pass@proxy.example.com:8080— avec login/mot de passesocks5://user:pass@proxy.example.com:1080— proxy SOCKS5
Méthode 2 : Configuration globale dans settings.py
# settings.py
# Proxy HTTP pour toutes les requêtes
HTTPPROXY_ENABLED = True
HTTPPROXY_AUTH_ENCODING = 'utf-8'
# Configuration via des variables d'environnement
HTTP_PROXY = 'http://username:password@proxy.example.com:8080'
HTTPS_PROXY = 'http://username:password@proxy.example.com:8080'
Cette méthode est pratique pour des tests rapides, mais ne convient pas pour la production : pas de rotation d'IP, si le proxy tombe, tout le scraper s'arrête, impossible d'utiliser différents proxies pour différents sites.
Création d'un middleware Proxy personnalisé
Pour le scraping en production, un middleware personnalisé est nécessaire pour gérer le pool de proxies, traiter les erreurs et changer automatiquement d'IP. Voici une implémentation de base :
# middlewares.py
import random
from scrapy import signals
from scrapy.exceptions import NotConfigured
class RandomProxyMiddleware:
def __init__(self, proxy_list):
self.proxy_list = proxy_list
@classmethod
def from_crawler(cls, crawler):
# Chargement de la liste des proxies depuis les paramètres
proxy_list = crawler.settings.getlist('PROXY_LIST')
if not proxy_list:
raise NotConfigured('PROXY_LIST not configured')
return cls(proxy_list)
def process_request(self, request, spider):
# Choisir un proxy aléatoire dans le pool
proxy = random.choice(self.proxy_list)
request.meta['proxy'] = proxy
spider.logger.info(f'Using proxy: {proxy}')
def process_exception(self, request, exception, spider):
# En cas d'erreur, essayer un autre proxy
proxy = random.choice(self.proxy_list)
request.meta['proxy'] = proxy
spider.logger.warning(
f'Proxy error, switching to: {proxy}'
)
return request
Maintenant, configurons l'utilisation du middleware dans settings.py :
# settings.py
# Liste des proxies (peut être chargée depuis un fichier ou une API)
PROXY_LIST = [
'http://user1:pass1@proxy1.example.com:8080',
'http://user2:pass2@proxy2.example.com:8080',
'http://user3:pass3@proxy3.example.com:8080',
# ... ajoutez 50+ proxies pour une rotation efficace
]
# Activation du middleware
DOWNLOADER_MIDDLEWARES = {
'myproject.middlewares.RandomProxyMiddleware': 350,
'scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware': 400,
}
# Tentatives répétées en cas d'erreurs
RETRY_TIMES = 3
RETRY_HTTP_CODES = [500, 502, 503, 504, 408, 429]
Rotation des proxies : trois méthodes fonctionnelles
Le choix aléatoire des proxies (comme dans l'exemple ci-dessus) est la méthode la plus simple, mais pas la plus efficace. Examinons trois stratégies de rotation pour différents scénarios.
Méthode 1 : Round-robin (rotation séquentielle)
Les proxies sont choisis en cercle. Convient pour une répartition uniforme de la charge :
class RoundRobinProxyMiddleware:
def __init__(self, proxy_list):
self.proxy_list = proxy_list
self.current_index = 0
@classmethod
def from_crawler(cls, crawler):
proxy_list = crawler.settings.getlist('PROXY_LIST')
return cls(proxy_list)
def process_request(self, request, spider):
# Prendre le prochain proxy en cercle
proxy = self.proxy_list[self.current_index]
self.current_index = (self.current_index + 1) % len(self.proxy_list)
request.meta['proxy'] = proxy
Méthode 2 : Rotation intelligente avec blacklist
Suivre les proxies problématiques et les exclure temporairement de la rotation :
import time
from collections import defaultdict
class SmartProxyMiddleware:
def __init__(self, proxy_list):
self.proxy_list = proxy_list
self.proxy_errors = defaultdict(int)
self.blacklist = set()
self.blacklist_timeout = 300 # 5 minutes
self.blacklist_time = {}
@classmethod
def from_crawler(cls, crawler):
proxy_list = crawler.settings.getlist('PROXY_LIST')
return cls(proxy_list)
def get_working_proxies(self):
# Retirer de la blacklist les proxies dont le délai a expiré
current_time = time.time()
expired = [
proxy for proxy, ban_time in self.blacklist_time.items()
if current_time - ban_time > self.blacklist_timeout
]
for proxy in expired:
self.blacklist.discard(proxy)
self.proxy_errors[proxy] = 0
# Retourner les proxies fonctionnels
return [p for p in self.proxy_list if p not in self.blacklist]
def process_request(self, request, spider):
working_proxies = self.get_working_proxies()
if not working_proxies:
spider.logger.error('All proxies are blacklisted!')
return
proxy = random.choice(working_proxies)
request.meta['proxy'] = proxy
def process_response(self, request, response, spider):
# Si nous avons reçu un blocage — ajouter à la blacklist
if response.status in [403, 429, 503]:
proxy = request.meta.get('proxy')
self.proxy_errors[proxy] += 1
if self.proxy_errors[proxy] >= 3:
self.blacklist.add(proxy)
self.blacklist_time[proxy] = time.time()
spider.logger.warning(
f'Proxy {proxy} blacklisted for {self.blacklist_timeout}s'
)
return response
Méthode 3 : Rotation via l'API du fournisseur
De nombreux fournisseurs de proxies (y compris les proxies résidentiels) proposent un point de terminaison rotatif — une URL qui change automatiquement d'IP à chaque requête :
# settings.py
# Point de terminaison unique avec rotation automatique
ROTATING_PROXY = 'http://username:password@rotating.proxy.com:8080'
# Middleware simple
class RotatingProxyMiddleware:
def __init__(self, proxy):
self.proxy = proxy
@classmethod
def from_crawler(cls, crawler):
proxy = crawler.settings.get('ROTATING_PROXY')
return cls(proxy)
def process_request(self, request, spider):
# Une seule URL, mais chaque requête utilise une nouvelle IP
request.meta['proxy'] = self.proxy
C'est la méthode la plus pratique pour la production : pas besoin de gérer un pool de proxies, le fournisseur s'occupe de la qualité des IP et remplace les problématiques. Cela fonctionne particulièrement bien avec les proxies résidentiels, où le pool d'IP peut atteindre des millions d'adresses.
Authentification : login/mot de passe vs liste blanche IP
Les fournisseurs de proxies proposent deux méthodes d'authentification. Le choix influence la vitesse de connexion et la facilité de configuration.
Authentification User:Pass
Le login et le mot de passe sont transmis dans l'URL du proxy. Scrapy les convertit automatiquement en en-tête HTTP Proxy-Authorization :
proxy = 'http://username:password@proxy.example.com:8080'
request.meta['proxy'] = proxy
# Scrapy ajoutera automatiquement l'en-tête :
# Proxy-Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=
Avantages : fonctionne depuis n'importe quelle IP, facile à changer les proxies dans le code.
Inconvénients : léger overhead sur chaque requête (~50-100ms), informations d'identification en clair dans le code.
Authentification par liste blanche IP
Vous ajoutez l'IP de votre serveur à la liste blanche chez le fournisseur, aucune authentification n'est requise :
proxy = 'http://proxy.example.com:8080' # sans login/mot de passe
request.meta['proxy'] = proxy
Avantages : plus rapide de 50-100ms, plus sûr (pas de credentials dans le code).
Inconvénients : fonctionne uniquement depuis certaines IP, besoin de mettre à jour la liste blanche lors du changement de serveur.
Recommandation pour la production :
Utilisez la liste blanche IP pour le scraping depuis des serveurs dédiés (AWS, Google Cloud, Hetzner). Pour le développement et les tests depuis une machine locale — utilisez l'authentification user:pass.
Gestion des erreurs et changement automatique d'IP
Même avec des proxies de qualité, il y aura des erreurs : des délais d'attente, des refus de connexion, des blocages. Une bonne gestion des erreurs est cruciale pour le bon fonctionnement du scraper.
Gestion des statuts HTTP
class ProxyMiddleware:
def process_response(self, request, response, spider):
# Codes pour lesquels il faut changer de proxy et réessayer
ban_codes = [403, 407, 429, 503]
if response.status in ban_codes:
proxy = request.meta.get('proxy')
spider.logger.warning(
f'Got {response.status} from {proxy}, retrying...'
)
# Marquer pour réessayer avec un nouveau proxy
request.meta['dont_retry'] = False
request.meta['proxy'] = self.get_new_proxy()
return request
return response
Gestion des exceptions réseau
from twisted.internet.error import TimeoutError, ConnectionRefusedError
from scrapy.exceptions import IgnoreRequest
class ProxyMiddleware:
def process_exception(self, request, exception, spider):
# Erreurs de connexion au proxy
proxy_errors = (
TimeoutError,
ConnectionRefusedError,
ConnectionLost,
)
if isinstance(exception, proxy_errors):
proxy = request.meta.get('proxy')
spider.logger.error(
f'Proxy {proxy} connection failed: {exception}'
)
# Changer de proxy et essayer à nouveau
request.meta['proxy'] = self.get_new_proxy()
return request
# Pour les autres erreurs, utiliser le traitement standard
return None
Détection des blocages par le contenu
Certains sites renvoient un HTTP 200, mais affichent un captcha ou une page de blocage :
class ProxyMiddleware:
def process_response(self, request, response, spider):
# Indicateurs de blocage dans le contenu
ban_indicators = [
'captcha',
'access denied',
'blocked',
'unusual traffic',
'robot check',
]
body_text = response.text.lower()
if any(indicator in body_text for indicator in ban_indicators):
spider.logger.warning(
f'Ban page detected from {request.meta.get("proxy")}'
)
# Changer de proxy et réessayer
request.meta['proxy'] = self.get_new_proxy()
return request
return response
Quel type de proxy choisir pour Scrapy
Le choix du type de proxy dépend du site cible, du budget et de la vitesse de scraping requise. Voici une comparaison des principales options :
| Type de proxy | Vitesse | Coût | Quand utiliser |
|---|---|---|---|
| Proxy de centre de données | Élevée (50-200ms) | Faible ($1-3/IP) | Sites simples sans protection, API, outils internes |
| Proxies résidentiels | Moyenne (300-800ms) | Moyenne ($5-15/GB) | E-commerce, réseaux sociaux, sites avec Cloudflare, géotargeting |
| Proxies mobiles | Faible (500-1500ms) | Élevée ($50-150/IP) | Applications mobiles, Instagram, TikTok, protection maximale |
Recommandations pour le choix
Pour le scraping des marketplaces (Amazon, Wildberries, Ozon, AliExpress) — uniquement des proxies résidentiels. Ces sites bloquent agressivement les centres de données. Une rotation et un géotargeting sont nécessaires (par exemple, des IP russes pour Wildberries).
Pour le scraping de sites d'actualités, blogs, forums — des proxies de centre de données conviennent. La protection est minimale, la vitesse et le coût du trafic sont importants.
Pour le scraping de sites avec Cloudflare — des proxies résidentiels sont obligatoires. Les centres de données sont détectés presque instantanément par Cloudflare. Ajoutez à Scrapy la bibliothèque cloudscraper pour contourner les défis JS.
Pour le scraping de Google Search, outils SEO — des proxies résidentiels avec géotargeting. Google affiche des résultats différents selon les pays et les villes.
Conseil : Commencez avec un pool de 10 proxies résidentiels pour les tests. Si vous recevez des blocages — augmentez le pool à 50-100 IP. Pour un scraping à haute vitesse (1000+ requêtes/minute), utilisez un point de terminaison rotatif avec un pool de 10,000+ IP.
Techniques avancées : sessions et IP collantes
Lors du scraping de certains sites, il est nécessaire de conserver une IP tout au long de la session (authentification, panier d'achats, formulaires multi-étapes). Voici comment réaliser des sessions collantes dans Scrapy.
IP collante pour un domaine
from urllib.parse import urlparse
class StickyProxyMiddleware:
def __init__(self, proxy_list):
self.proxy_list = proxy_list
# Dictionnaire : domaine -> proxy
self.domain_proxy_map = {}
@classmethod
def from_crawler(cls, crawler):
proxy_list = crawler.settings.getlist('PROXY_LIST')
return cls(proxy_list)
def process_request(self, request, spider):
# Extraire le domaine de l'URL
domain = urlparse(request.url).netloc
# Si ce domaine a déjà un proxy — l'utiliser
if domain in self.domain_proxy_map:
proxy = self.domain_proxy_map[domain]
else:
# Sinon, choisir un nouveau et mémoriser
proxy = random.choice(self.proxy_list)
self.domain_proxy_map[domain] = proxy
spider.logger.info(f'Assigned {proxy} to {domain}')
request.meta['proxy'] = proxy
IP collante avec délai de session
Une variante plus avancée : le proxy est lié au domaine pendant une certaine période (par exemple, 10 minutes), puis change :
import time
from urllib.parse import urlparse
class SessionProxyMiddleware:
def __init__(self, proxy_list, session_timeout=600):
self.proxy_list = proxy_list
self.session_timeout = session_timeout # 10 minutes
# Dictionnaire : domaine -> (proxy, temps de création)
self.sessions = {}
@classmethod
def from_crawler(cls, crawler):
proxy_list = crawler.settings.getlist('PROXY_LIST')
timeout = crawler.settings.getint('PROXY_SESSION_TIMEOUT', 600)
return cls(proxy_list, timeout)
def get_proxy_for_domain(self, domain):
current_time = time.time()
# Vérifier s'il y a une session active
if domain in self.sessions:
proxy, created_at = self.sessions[domain]
# Si la session n'a pas expiré — utiliser le même proxy
if current_time - created_at < self.session_timeout:
return proxy
# Créer une nouvelle session avec un nouveau proxy
new_proxy = random.choice(self.proxy_list)
self.sessions[domain] = (new_proxy, current_time)
return new_proxy
def process_request(self, request, spider):
domain = urlparse(request.url).netloc
proxy = self.get_proxy_for_domain(domain)
request.meta['proxy'] = proxy
Intégration avec le middleware des cookies
Pour des sessions complètes, il est nécessaire de synchroniser les proxies et les cookies. Scrapy stocke les cookies séparément pour chaque domaine, mais lors du changement de proxy, il faut nettoyer les cookies :
# settings.py
# Activer le middleware des cookies
COOKIES_ENABLED = True
COOKIES_DEBUG = False
# Middleware pour synchroniser proxies et cookies
class ProxyCookieMiddleware:
def process_request(self, request, spider):
# Obtenir le proxy actuel
current_proxy = request.meta.get('proxy')
# Si le proxy a changé — nettoyer les cookies
previous_proxy = request.meta.get('previous_proxy')
if previous_proxy and previous_proxy != current_proxy:
# Nettoyer les cookies pour ce domaine
jar = spider.crawler.engine.downloader.middleware.middlewares[0].jars
domain = urlparse(request.url).netloc
if domain in jar:
jar[domain].clear()
spider.logger.info(f'Cleared cookies for {domain}')
request.meta['previous_proxy'] = current_proxy
Conclusion
Une configuration correcte des proxies dans Scrapy est la base d'un scraping stable sans blocages. Nous avons examiné tous les aspects clés : de l'intégration de base aux techniques avancées de rotation et de gestion des sessions.
Principales conclusions :
- Pour la production, utilisez un middleware personnalisé avec une rotation intelligente et une blacklist des IP problématiques
- Traitez tous les types d'erreurs : statuts HTTP, exceptions réseau, blocages par contenu
- Choisissez le type de proxy en fonction de la tâche : centres de données pour des sites simples, résidentiels pour des sites protégés
- Pour les sites avec authentification, utilisez des sessions collantes avec liaison du proxy au domaine
- Commencez avec un pool de 10-50 proxies, évoluez avec l'augmentation de la charge
Si vous prévoyez de scraper des sites protégés (marketplaces, réseaux sociaux, sites avec Cloudflare), je recommande d'utiliser des proxies résidentiels — ils offrent une anonymité maximale et un risque minimal de blocages. Pour un scraping à haute vitesse, choisissez des fournisseurs avec un point de terminaison rotatif et un pool de 10,000 adresses IP.
Tous les exemples de code de cet article ont été testés sur Scrapy 2.x et sont prêts à être utilisés en production. Adaptez-les à vos besoins et évoluez au fur et à mesure de la croissance de votre projet.