← Tous les articles

Ajouter un Repli via une API de Scraping à Votre Projet Scrapy

Votre araignée Scrapy fonctionne. Elle extrait des milliers de pages par jour, les parse proprement et ne vous coûte rien d’autre que de la bande passante. Puis, un matin, les journaux se remplissent de réponses 403, ou pire, de réponses 200 qui contiennent une page de défi au lieu des données que vous vouliez. Le site a ajouté une couche anti-bot, et vos requêtes directes ne passent plus.

Le réflexe est de faire passer tout le trafic par une API de scraping. Cela fonctionne, mais c’est du gaspillage. La plupart de vos requêtes n’ont jamais été bloquées, et les envoyer toutes par une API payante revient à payer pour des pages que vous auriez pu récupérer gratuitement. Un meilleur modèle est le repli : récupérer directement d’abord, et n’escalader vers l’API que lorsqu’une requête échoue réellement.

Ce guide vous montre comment intégrer ce repli à Scrapy sous forme de middleware de téléchargement, avec une détection correcte, une logique de réessai et une gestion des erreurs. Le code est complet et vous pouvez l’ajouter à un projet existant.

Pourquoi un Repli Plutôt qu’un Interrupteur Tout ou Rien

Une API de scraping est une ressource payante. Chaque requête qui y passe coûte un crédit. Si 90 % de vos pages cibles ne sont pas protégées, les faire toutes passer par l’API multiplie votre facture sans aucun bénéfice.

Un repli inverse la logique :

  • Direct d’abord. Scrapy envoie sa requête normale. Si le site répond avec des données propres, c’est terminé et vous n’avez rien payé.
  • Escalader en cas d’échec. Si la réponse est bloquée, absente ou mal formée, le middleware réessaie la même URL via l’API.
  • Abandonner proprement. Après un nombre fixe de tentatives via l’API, la requête est abandonnée ou journalisée pour qu’elle ne tourne pas en boucle indéfiniment.

Ainsi, votre dépense en crédits reste proportionnelle à la difficulté réelle du site. Cela signifie aussi que vous pouvez pointer une araignée vers un ensemble mixte de domaines, certains faciles et d’autres protégés, sans scinder votre code en deux chemins.

Où le Repli s’Insère dans Scrapy

Scrapy traite chaque requête à travers une chaîne de middlewares de téléchargement avant et après le téléchargement réel. C’est le bon endroit pour un repli, car un middleware peut :

  1. Voir la réponse que Scrapy a obtenue de la requête directe.
  2. Décider si cette réponse compte comme un blocage.
  3. La remplacer par une nouvelle requête acheminée via l’API.

Vous ne touchez pas du tout à la logique de parsing de votre araignée. L’araignée demande une URL et reçoit une réponse fonctionnelle. Que cette réponse vienne directement ou de l’API lui est invisible.

Détecter un Blocage

La partie la plus difficile d’un repli n’est pas l’appel à l’API. C’est de décider quand le déclencher. Une vérification naïve de response.status == 403 passe à côté de l’échec moderne le plus courant : le blocage doux, où le serveur renvoie 200 OK avec un défi ou une page « vérifiez que vous êtes humain » à la place du contenu réel.

Construisez votre détection autour de plusieurs signaux :

  • Codes de statut durs. 403, 429 et 503 sont des blocages explicites ou des limites de débit.
  • Corps suspect sur un 200. Des corps très courts, ou des corps contenant des marqueurs de défi connus, sont des blocages doux.
  • Contenu attendu absent. Si la page devrait contenir une grille de produits et que le sélecteur ne renvoie rien, traitez-le comme une récupération échouée.

Voici un utilitaire de détection que vous pouvez ajuster par projet :

BLOCK_STATUS = {403, 429, 503}
CHALLENGE_MARKERS = (
    b"captcha",
    b"cf-challenge",
    b"just a moment",
    b"verify you are human",
)

def looks_blocked(response):
    if response.status in BLOCK_STATUS:
        return True
    body = response.body[:20000].lower()
    if len(response.body) < 500:
        return True
    return any(marker in body for marker in CHALLENGE_MARKERS)

Gardez la liste de marqueurs courte et spécifique. Si vous faites des correspondances trop larges, vous enverrez des pages propres à l’API et brûlerez des crédits. Si vous voulez creuser pourquoi un 200 peut tout de même être un blocage, consultez notre guide sur la détection des blocages doux en web scraping.

Le Middleware de Repli

Passons au middleware lui-même. Il observe chaque réponse et, lorsque looks_blocked renvoie True, il reconstruit la requête pour qu’elle passe par l’API de scraping plutôt que par l’origine. Il compte combien de fois il a réessayé une URL donnée grâce à un indicateur dans le meta de la requête, donc il ne boucle jamais.

Le point de terminaison de récupération de ScrapeUnblocker prend l’URL cible comme paramètre de requête et une clé d’API dans un en-tête, donc le repli se résume à réécrire l’URL de la requête et à échanger les en-têtes.

import logging
from urllib.parse import quote, urlencode

from scrapy.exceptions import IgnoreRequest
from scrapy.http import Request

logger = logging.getLogger(__name__)

API_ENDPOINT = "https://api.scrapeunblocker.com/getPageSource"


class ScrapingApiFallbackMiddleware:
    def __init__(self, api_key, max_api_retries):
        if not api_key:
            raise ValueError("SCRAPEUNBLOCKER_API_KEY is not set")
        self.api_key = api_key
        self.max_api_retries = max_api_retries

    @classmethod
    def from_crawler(cls, crawler):
        return cls(
            api_key=crawler.settings.get("SCRAPEUNBLOCKER_API_KEY"),
            max_api_retries=crawler.settings.getint("API_FALLBACK_MAX_RETRIES", 2),
        )

    def _build_api_request(self, original, attempt):
        params = {"url": original.meta.get("origin_url", original.url)}
        # Ajoutez ici des options supplémentaires si un site en a besoin, par exemple :
        # params["proxy_country"] = "us"
        api_url = f"{API_ENDPOINT}?{urlencode(params, quote_via=quote)}"

        return original.replace(
            url=api_url,
            method="POST",
            headers={"x-scrapeunblocker-key": self.api_key},
            meta={
                **original.meta,
                "origin_url": original.meta.get("origin_url", original.url),
                "api_attempt": attempt,
                "download_slot": "scrapeunblocker-api",
            },
            dont_filter=True,
        )

    def process_response(self, request, response, spider):
        attempt = request.meta.get("api_attempt", 0)

        if not looks_blocked(response):
            return response

        if attempt >= self.max_api_retries:
            logger.warning(
                "Giving up on %s after %d API attempts",
                request.meta.get("origin_url", request.url),
                attempt,
            )
            raise IgnoreRequest(f"Blocked after {attempt} API retries")

        next_attempt = attempt + 1
        logger.info(
            "Blocked, routing through API (attempt %d): %s",
            next_attempt,
            request.meta.get("origin_url", request.url),
        )
        return self._build_api_request(request, next_attempt)

Quelques détails qu’il vaut la peine de comprendre :

  • origin_url dans le meta. Une fois qu’une requête est réécrite pour pointer vers l’API, request.url est l’URL de l’API, pas la page voulue. Stocker la cible d’origine dans meta["origin_url"] permet à chaque réessai de reconstruire l’appel API à partir de l’URL réelle.
  • Compteur api_attempt. Chaque escalade l’incrémente. Quand il atteint max_api_retries, le middleware lève IgnoreRequest et s’arrête.
  • download_slot. Définir un créneau partagé pour les requêtes API vous permet de les limiter indépendamment de votre trafic direct (voir plus bas).
  • dont_filter=True. Sans cela, le filtre de doublons de Scrapy abandonnerait le réessai parce qu’il cible une URL déjà visitée par l’araignée.

Le Brancher dans les Réglages

Activez le middleware et définissez votre clé et vos limites dans settings.py :

DOWNLOADER_MIDDLEWARES = {
    "myproject.middlewares.ScrapingApiFallbackMiddleware": 610,
}

SCRAPEUNBLOCKER_API_KEY = "YOUR_API_KEY"
API_FALLBACK_MAX_RETRIES = 2

# Limitez le trafic API séparément des requêtes directes.
DOWNLOAD_SLOTS = {
    "scrapeunblocker-api": {"concurrency": 4, "delay": 0},
}

Le numéro de priorité 610 place le middleware juste après le RetryMiddleware intégré de Scrapy (situé à 550), de sorte que les erreurs transitoires ordinaires sont d’abord réessayées directement, et que seuls les blocages persistants tombent vers l’API. Lisez votre clé depuis une variable d’environnement plutôt que de l’inscrire en dur dans le code :

import os
SCRAPEUNBLOCKER_API_KEY = os.environ["SCRAPEUNBLOCKER_API_KEY"]

Gérer les Erreurs et les Délais d’Attente

Les blocages ne sont pas le seul mode d’échec. La requête API elle-même peut expirer ou renvoyer une erreur, et vous voulez gérer cela sans faire planter le crawl. Ajoutez un hook process_exception au même middleware pour que les erreurs réseau sur une requête directe déclenchent aussi le repli :

    def process_exception(self, request, exception, spider):
        attempt = request.meta.get("api_attempt", 0)
        if attempt >= self.max_api_retries:
            return None  # laisse Scrapy gérer l'échec normalement

        logger.info(
            "Download error (%s), routing through API: %s",
            type(exception).__name__,
            request.meta.get("origin_url", request.url),
        )
        return self._build_api_request(request, attempt + 1)

Combinez cela avec les réglages de réessai et de délai d’attente propres à Scrapy pour qu’un appel API lent ne bloque pas tout le crawl :

DOWNLOAD_TIMEOUT = 60
RETRY_ENABLED = True
RETRY_TIMES = 2

Comme l’API rend les pages protégées avec un vrai navigateur, ses réponses sont plus lentes qu’une récupération brute. Un délai d’attente de 60 secondes lui laisse de la marge pour travailler sans faire caler votre araignée indéfiniment.

Tester le Repli

Avant de lancer à grande échelle, confirmez que les deux chemins se comportent bien :

  • Pointez l’araignée vers une page non protégée et vérifiez que les journaux ne montrent aucune tentative API. Cela prouve que vous ne gaspillez pas de crédits sur les pages faciles.
  • Pointez-la vers une page protégée connue et confirmez que vous voyez la ligne de journal « routing through API », suivie d’un parsing réussi. Cela prouve que l’escalade fonctionne.
  • Forcez un échec en réglant max_api_retries sur 0 et confirmez que la requête est abandonnée proprement avec l’avertissement « giving up » plutôt que de boucler.

FAQ

Le repli ralentit-il mon crawl ? Seulement pour les pages qui sont bloquées. Les requêtes directes tournent à pleine vitesse. Les requêtes escaladées sont plus lentes parce que l’API les rend avec un vrai navigateur, ce qui est le prix à payer pour franchir le blocage. Le créneau de téléchargement séparé empêche ce trafic plus lent de brider vos requêtes directes.

Comment éviter de payer pour des pages qui n’étaient pas vraiment bloquées ? Ajustez looks_blocked. Gardez la liste des marqueurs de défi spécifique à ce que vos cibles renvoient réellement, et journalisez chaque escalade pendant les tests pour repérer les faux positifs avant qu’ils ne vous coûtent de l’argent.

Puis-je l’utiliser avec l’AutoThrottle de Scrapy ? Oui. AutoThrottle ajuste les délais en fonction de la latence des réponses. Mettre les requêtes API dans leur propre créneau de téléchargement empêche leur latence plus élevée de tirer vers le bas le calcul du délai pour votre trafic direct.

Et si une page nécessite un rendu JavaScript ou un pays précis ? Ajoutez les paramètres correspondants au dictionnaire params dans _build_api_request. Par exemple, définissez une valeur proxy_country pour acheminer via une région donnée. Le reste du middleware reste identique.

Conclusion

Un repli via une API de scraping vous donne le meilleur des deux modèles : des récupérations directes gratuites et rapides pour l’essentiel de vos pages, et un chemin d’escalade fiable pour la poignée de domaines qui résistent. Le tout tient dans un seul middleware de téléchargement, donc vos araignées restent propres et votre dépense en crédits reste proportionnelle à la difficulté réelle de chaque site.

Si vous voulez essayer le chemin d’escalade, ScrapeUnblocker gère le contournement anti-bot et le rendu du navigateur derrière un seul point de terminaison, facturé à un crédit par requête, rendu JavaScript inclus. Ajoutez le middleware ci-dessus à votre projet, pointez SCRAPEUNBLOCKER_API_KEY vers votre clé, et vos araignées existantes continuent de tourner pendant que les pages difficiles se remettent discrètement à fonctionner.

Essayez ScrapeUnblocker gratuitement

Taux de réussite de plus de 95 % · à partir de 0,55 € pour 1 000 appels · 500 requêtes gratuites à l'inscription.

Essayer gratuitement → Voir les tarifs