Scrapy Proxy Middleware : Rotation des Proxies Sans Bannissement

Scrapy prend en charge les proxies depuis la version 0.8, mais la documentation ne vous explique jamais comment les faire tourner, réessayer et survivre aux bannissements à grande échelle. Voici l'anatomie du middleware, le code, et les paramètres qui décident si votre crawl se termine.

Scrapy propose un proxy middleware depuis la version 0.8, donc la question 'Scrapy prend-il en charge les proxies' a été réglée il y a quinze ans. Ce que la documentation n'assemble jamais vraiment, c'est le tableau de production : comment le middleware proxy intégré de Scrapy traite réellement les identifiants, quand définir des proxies par requête par rapport à globalement, et comment tourner et récupérer lorsqu'une cible commence à bannir les sorties en milieu de crawl. Ce guide parcourt toute la chaîne — le HttpProxyMiddleware intégré, le meta par requête, un middleware de rotation personnalisé avec gestion des bannissements, et les paramètres qui décident si un crawl de 100k pages se termine ou meurt à 3h du matin.

Comment fonctionne le middleware proxy intégré de Scrapy

scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware est activé par défaut avec une priorité de 750 dans DOWNLOADER_MIDDLEWARES_BASE. Lire son code source (environ 100 lignes dans Scrapy 2.17) vous dit tout ce dont vous avez besoin pour le débogage :

Conséquence pratique : vous n'avez presque jamais besoin d'un package de proxy tiers. Tout ce qui met une URL de proxy valide dans request.meta['proxy'] avant la priorité 750 obtient l'authentification et la gestion des en-têtes gratuitement.

Proxies par requête avec méta

L'intégration la plus légère consiste à définir meta directement dans le spider — utile lorsque seules certaines requêtes nécessitent un proxy, ou que différentes cibles nécessitent différents pays de sortie :

import scrapy


class PricesSpider(scrapy.Spider):
    name = "prices"

    def start_requests(self):
        yield scrapy.Request(
            "https://example.com/product/1",
            meta={"proxy": "http://USER:PASS@gate.quantumproxies.io:PORT"},
        )

La méta par requête brille lorsque le choix du proxy lui-même est basé sur les données : acheminer les pages de produits allemandes via un nom d'utilisateur ciblé pour l'Allemagne, envoyer les téléchargements d'images via des sorties de centre de données bon marché tandis que le HTML passe par résidentiel, ou escalader une URL récalcitrante vers une session plus collante à sa deuxième tentative. Parce que la méta survit aux réessais et aux redirections, la décision que vous prenez lors de la construction de la requête la suit tout au long du cycle de téléchargement. Définir la méta sur des milliers de requêtes à la main ne passe pas à l'échelle, cependant — c'est pour cela que les middlewares de téléchargeur existent.

La configuration de production la plus simple : une passerelle rotative

Avec des proxies rotatifs derrière un seul point de terminaison de passerelle, la rotation se fait côté serveur : chaque requête via la même URL sort d'une IP différente dans un pool résidentiel de plus de 90M couvrant plus de 200 pays. Votre middleware se réduit à trois lignes, et il n'y a pas de liste à vérifier, élaguer ou rafraîchir :

# middlewares.py
class RotatingGatewayMiddleware:
    PROXY = "http://USER:PASS@gate.quantumproxies.io:PORT"

    def process_request(self, request, spider):
        request.meta["proxy"] = self.PROXY


# settings.py
DOWNLOADER_MIDDLEWARES = {
    "myproject.middlewares.RotatingGatewayMiddleware": 350,
}

La priorité 350 est importante : votre middleware doit s'exécuter avant celui intégré à 750 pour que HttpProxyMiddleware puisse toujours convertir les identifiants en en-tête d'authentification. Cette association — rotation de passerelle plus la mécanique de réessai standard de Scrapy — offre la fiabilité la plus élevée par ligne de code, car chaque réessai passe automatiquement par une nouvelle IP de sortie.

Diagramme de flux de la chaîne de middleware proxy de Scrapy : requête du spider, middleware personnalisé définit le proxy méta, HttpProxyMiddleware ajoute l'authentification à la priorité 750, le téléchargeur sort via la passerelle rotative
Votre middleware n'a besoin que de définir meta['proxy']; celui intégré à la priorité 750 gère les identifiants, et les réessais réintègrent la chaîne sur une nouvelle IP.

Un middleware de proxy rotatif personnalisé avec gestion des bannissements

Si vous gérez votre propre liste de proxies — adresses ISP statiques ou un pool mixte — le schéma classique (popularisé par le package scrapy-proxies, qui recommandait RETRY_TIMES = 10 car les proxies de liste gratuite échouent si souvent) est : choisir aléatoirement par requête, évincer sur les signaux de bannissement, remettre la requête en file d'attente :

import random


class ProxyListMiddleware:
    BAN_CODES = {403, 429}

    def __init__(self, proxies):
        self.proxies = list(proxies)

    @classmethod
    def from_crawler(cls, crawler):
        return cls(crawler.settings.getlist("PROXY_LIST"))

    def process_request(self, request, spider):
        if self.proxies and "proxy" not in request.meta:
            request.meta["proxy"] = random.choice(self.proxies)

    def process_response(self, request, response, spider):
        if response.status in self.BAN_CODES:
            bad = request.meta.get("proxy")
            if bad in self.proxies and len(self.proxies) > 1:
                self.proxies.remove(bad)
                spider.logger.warning("Evicted %s (%d left)", bad, len(self.proxies))
            return request.replace(dont_filter=True)  # re-queue on a new proxy
        return response

Notez tout ce que ce middleware doit faire qu'une passerelle fait gratuitement : suivre la santé du pool, évincer les IP brûlées, remettre les requêtes en file d'attente. Il se réduit également sous le feu — un pool de 20 IP statiques peut s'évaporer en minutes sur une cible agressive. Le livre de jeu anti-bannissement plus large (cadence, en-têtes, discipline de session) est dans notre checklist d'évitement des bannissements IP.

Paramètres Scrapy qui décident du résultat

Le middleware place le proxy ; les paramètres décident si le crawl se comporte. Ceux-ci sont les plus importants :

# settings.py — a sane baseline for proxied crawls
RETRY_TIMES = 5
RETRY_HTTP_CODES = [429, 403, 500, 502, 503, 504, 408]

CONCURRENT_REQUESTS = 32
CONCURRENT_REQUESTS_PER_DOMAIN = 8
DOWNLOAD_TIMEOUT = 30

AUTOTHROTTLE_ENABLED = True
AUTOTHROTTLE_TARGET_CONCURRENCY = 4.0
Comparaison d'un middleware de liste de proxies Scrapy DIY versus une passerelle résidentielle rotative pour la rotation des proxies
Une liste DIY signifie des vérifications de santé, une logique d'éviction et des IP obsolètes. Une passerelle est un point de terminaison qui tourne côté serveur — les réessais atterrissent par défaut sur des IP fraîches.

Détection de bannissement au-delà des codes de statut

Les cibles sophistiquées n'envoient pas de 403 honnêtes. Elles servent des 200 contenant une page CAPTCHA, une grille de produits vide, ou un modèle dépouillé. Les spiders robustes vérifient le contenu, pas seulement le statut — vérifiez un élément qui n'existe que sur la vraie page, et remettez en file d'attente lorsqu'il manque. Si les pages reviennent structurellement vides même à travers de bons proxies, le contenu est probablement rendu côté client ; voir pourquoi les scrapers obtiennent des pages vides. Et lorsqu'un domaine défie le HTTP simple peu importe l'IP, confiez ce domaine à une Scraper API qui rend JavaScript et renvoie du HTML propre ou du markdown — Scrapy le consomme comme toute autre réponse, et vous gardez votre pipeline.

def parse(self, response):
    if not response.css("div.product-grid"):
        # 200 OK but the real content is missing: soft ban or JS wall
        yield response.request.replace(dont_filter=True)
        return
    for product in response.css("div.product-grid article"):
        yield {"name": product.css("h2::text").get()}

Questions fréquemment posées

Scrapy prend-il en charge les proxies par défaut ?

Oui. HttpProxyMiddleware est livré activé à la priorité 750 : il lit les variables d'environnement http_proxy / https_proxy et respecte request.meta['proxy'] par requête, y compris les identifiants user:pass@, qu'il convertit en un en-tête Proxy-Authorization. Vous ne codez que pour décider quel proxy chaque requête obtient.

Comment définir un proxy pour une seule requête dans Scrapy ?

Passez-le dans le méta de la requête : scrapy.Request(url, meta={'proxy': 'http://user:pass@host:port'}). La méta l'emporte toujours sur les proxies au niveau de l'environnement, et définir la valeur sur None force cette requête à aller directement — utile pour mélanger le trafic avec et sans proxy dans un seul spider.

Comment faire tourner les proxies dans Scrapy ?

Soit écrivez un middleware de téléchargeur qui choisit un proxy par requête à partir d'une liste et évince ceux bannis, soit pointez chaque requête vers un point de terminaison de passerelle rotative qui assigne une nouvelle IP de sortie côté serveur. L'approche de la passerelle nécessite un middleware de trois lignes, pas de vérifications de santé, et transforme chaque réessai Scrapy en rotation gratuite.

Pourquoi mon proxy Scrapy renvoie-t-il 407 ?

Le proxy a rejeté l'authentification. Vérifiez que les identifiants sont dans l'URL du proxy dans la méta, que les caractères spéciaux sont encodés en URL, et que votre IP de serveur est sur liste blanche si votre plan utilise l'authentification IP. Si les identifiants contiennent des caractères non-ASCII, définissez HTTPPROXY_AUTH_ENCODING pour correspondre à votre fournisseur.

Le schéma à retenir : définissez meta['proxy'] tôt, laissez le middleware intégré à 750 faire l'authentification, ajoutez 429 et 403 à vos codes de réessai, et préférez la rotation côté serveur à la gestion de liste. Si certaines parties de votre crawl nécessitent JavaScript, pesez le coût des navigateurs sans tête par rapport aux requêtes HTTP avant d'en utiliser un — la plupart des projets Scrapy ont besoin de meilleures IP, pas d'un navigateur.

Intégrez des proxies rotatifs dans votre projet Scrapy