Développement Web

Scrapy avance : pagination, throttling et pipelines

12 min de lecture

📍 Guide principal : Web scraping en Python : le guide complet.

Quatrième étape de la série. Elle prolonge directement le projet Scrapy monté dans Créer un spider Scrapy : on réutilise le même projet veille et le spider livres.

Le spider d’Aminata sait extraire une page. Pour couvrir les 1000 références du catalogue, il lui manque trois choses : suivre la pagination tout seul, le faire sans assommer le serveur du fournisseur, et nettoyer les données au passage plutôt qu’après coup. Bonne nouvelle : Scrapy fournit un mécanisme dédié pour chacune. On ne rallonge pas le spider — on branche les bons composants.

À la fin de ce tutoriel, un seul scrapy crawl livres parcourra tout le catalogue, à un rythme poli réglé automatiquement, et déposera un CSV déjà propre. C’est « VeilleManuels » en version industrielle. On reste sur le catalogue-école books.toscrape.com.

🎯 Ce que vous allez apprendre

  • Suivre la pagination avec response.follow et un rappel (callback).
  • Régler la politesse du crawl avec DOWNLOAD_DELAY et AutoThrottle.
  • Nettoyer et valider chaque donnée dans une Item Pipeline, et écarter les lignes invalides.
  • Dédupliquer à la volée et exporter automatiquement avec le réglage FEEDS.

🛠️ Ce que vous allez construire

Le même projet veille, complété de trois éléments : un spider qui s’étend à tout le catalogue, une pipeline de nettoyage et de déduplication dans pipelines.py, et une configuration d’export dans settings.py. Le résultat : data/livres.csv, mille références propres, produites en une commande.

Prérequis

  • Avoir le projet Scrapy du tutoriel précédent en état de marche (spider livres avec son Item).
  • Savoir lancer scrapy crawl et lire les statistiques de fin de crawl.
  • ⏱️ Temps estimé : ~50 minutes.

Étape 1 — Suivre la pagination avec response.follow

Dans le spider artisanal, on rebouclait à la main sur les pages. Avec Scrapy, on procède autrement : à la fin de parse, on repère le lien « suivant » et on demande au framework de le suivre. Scrapy met alors la nouvelle page dans sa file et rappellera parse dessus. La fonction s’appelle ainsi elle-même, de page en page, jusqu’à ce qu’il n’y ait plus de lien suivant.

    def parse(self, response):
        for livre in response.css("article.product_pod"):
            item = LivreItem()
            item["titre"] = livre.css("h3 a::attr(title)").get()
            item["prix"] = livre.css("p.price_color::text").get()
            item["stock"] = "".join(
                livre.css("p.instock.availability::text").getall()
            ).strip()
            yield item

        # Suivre la page suivante, s'il y en a une
        suivante = response.css("li.next a::attr(href)").get()
        if suivante:
            yield response.follow(suivante, callback=self.parse)

Le response.follow est l’équivalent Scrapy d’urljoin + nouvelle requête : il gère le lien relatif et crée la requête pour vous. Le callback=self.parse indique quelle méthode traitera la réponse — ici la même, puisque chaque page du catalogue se traite à l’identique. Notez qu’on yield à la fois des items et une requête : Scrapy fait la part des choses selon le type.

Point d’étape — Lancez scrapy crawl livres -O test.csv : le fichier doit maintenant contenir 1000 lignes, pas 20. Dans les stats finales, response_received_count doit avoisiner 50 (les 50 pages).

Étape 2 — Crawler poliment : délai et AutoThrottle

Suivre 50 pages aussi vite que possible, c’est envoyer une rafale de requêtes qui peut ralentir un petit serveur, voire déclencher un blocage. La politesse n’est pas qu’une question d’éthique : c’est ce qui vous permet de continuer à scraper demain. Scrapy se règle dans settings.py, où l’on impose un délai minimal entre requêtes et où l’on active l’auto-régulation.

# settings.py
DOWNLOAD_DELAY = 1.0              # au moins 1 seconde entre deux requêtes
CONCURRENT_REQUESTS_PER_DOMAIN = 2

AUTOTHROTTLE_ENABLED = True       # ajuste le délai selon la charge du serveur
AUTOTHROTTLE_START_DELAY = 1.0
AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0

DOWNLOAD_DELAY fixe un plancher ; AutoThrottle, lui, observe les temps de réponse du serveur et augmente le délai quand celui-ci peine. La combinaison donne un crawl qui s’adapte : rapide quand le serveur suit, prudent quand il fatigue. C’est le réglage par défaut recommandé pour tout site qui ne vous appartient pas, et il rend votre robot nettement moins repérable qu’une rafale brutale.

Un dernier réglage de politesse, souvent oublié : l’identité que votre robot présente. Par défaut, Scrapy s’annonce sous un nom générique. Mieux vaut afficher un identifiant honnête, assorti d’un moyen de vous joindre — c’est ce qu’un administrateur regarde en premier dans ses journaux, et cela vous distingue d’un robot anonyme suspect.

# settings.py
USER_AGENT = "VeilleManuels/1.0 (+mailto:contact@exemple.sn)"

On approfondira cette question d’identité et de courtoisie dans le tutoriel sur le scraping responsable ; pour l’instant, retenez qu’un robot bien élevé dit toujours qui il est.

Étape 3 — Nettoyer dans une Item Pipeline

Jusqu’ici, le prix sort sous forme de texte (« £51.77 »). On pourrait le nettoyer dans parse, mais Scrapy offre un endroit fait pour ça : la pipeline. Chaque item produit y transite avant d’être écrit, ce qui sépare proprement deux responsabilités — le spider extrait, la pipeline transforme et valide. On l’écrit dans pipelines.py.

# pipelines.py
from scrapy.exceptions import DropItem

class NettoyagePipeline:
    def process_item(self, item, spider):
        brut = (item.get("prix") or "").replace("£", "").replace("Â", "").strip()
        try:
            item["prix"] = float(brut)
        except ValueError:
            raise DropItem(f"Prix illisible : {item.get('titre')!r}")
        item["en_stock"] = "in stock" in item.get("stock", "").lower()
        return item

La méthode process_item est appelée pour chaque item. Si le prix se convertit, on le remplace par un nombre ; sinon, on lève DropItem, et Scrapy écarte la ligne sans interrompre le crawl. On en profite pour dériver un booléen en_stock à partir du texte de disponibilité — un champ qu’il faut, comme tout champ d’un Item, déclarer dans items.py (en_stock = scrapy.Field()). Une règle d’or : une pipeline retourne l’item (modifié) pour le laisser continuer, ou lève DropItem pour le rejeter.

Étape 4 — Dédupliquer à la volée

Sur un vrai site, un même article apparaît parfois sur deux pages (mises en avant, tris). Plutôt que de dédupliquer après coup avec pandas, on peut le faire pendant le crawl, dans une seconde pipeline qui se souvient des titres déjà vus. C’est plus efficace : la ligne en double n’est jamais écrite.

# pipelines.py (à la suite)
class DeduplicationPipeline:
    def __init__(self):
        self.titres_vus = set()

    def process_item(self, item, spider):
        titre = item.get("titre")
        if titre in self.titres_vus:
            raise DropItem(f"Doublon ignoré : {titre!r}")
        self.titres_vus.add(titre)
        return item

Le set initialisé dans __init__ persiste pendant toute la durée du crawl. À chaque item, on vérifie sa présence : déjà vu, on le jette ; sinon, on le mémorise et on le laisse passer. Cette pipeline est totalement indépendante de la précédente — c’est l’intérêt du découpage : chaque pipeline fait une seule chose, et on les enchaîne.

Étape 5 — Activer les pipelines et configurer l’export

Écrire des pipelines ne suffit pas : il faut dire à Scrapy de les utiliser, et dans quel ordre. On les déclare dans settings.py avec un numéro de priorité — plus il est petit, plus tôt la pipeline s’exécute. On nettoie donc avant de dédupliquer. Dans la foulée, on configure l’export automatique avec FEEDS, ce qui rend l’option -O de la ligne de commande inutile.

# settings.py
ITEM_PIPELINES = {
    "veille.pipelines.NettoyagePipeline": 100,
    "veille.pipelines.DeduplicationPipeline": 200,
}

FEEDS = {
    "data/livres.csv": {
        "format": "csv",
        "encoding": "utf-8",
        "overwrite": True,
    },
}

L’ordre des numéros compte : le nettoyage (100) passe avant la déduplication (200), ce qui est logique — inutile de dédupliquer des lignes qu’on s’apprête à jeter. Le bloc FEEDS indique où et comment exporter ; overwrite: True écrase le fichier à chaque crawl. Scrapy crée le dossier data/ au besoin.

Étape 6 — Lancer le crawl complet et lire les stats

Tout est en place. On lance le crawl, cette fois sans option de sortie puisque FEEDS s’en charge, et on lit attentivement le récapitulatif final — c’est lui qui prouve que tout a fonctionné.

scrapy crawl livres

À la fin, repérez trois lignes dans les stats : item_scraped_count (les items réellement exportés), item_dropped_count (ceux écartés par les pipelines) et response_received_count (les pages visitées). Sur ce catalogue propre, vous devez voir 1000 items conservés, 0 ou très peu de rejets, et une cinquantaine de pages. Ouvrez data/livres.csv : les prix sont des nombres, la colonne en_stock est remplie, et aucun doublon ne subsiste.

Point d’étapedata/livres.csv contient ~1000 lignes, la colonne prix est numérique et en_stock vaut vrai/faux. Si item_dropped_count est élevé, examinez le journal : Scrapy logge la raison de chaque DropItem.

Comprendre les statistiques du crawl

Scrapy clôt chaque exécution par un bloc de statistiques qu’il serait dommage d’ignorer : c’est le compte rendu de santé de votre collecte. Quelques lignes méritent un coup d’œil systématique. downloader/response_status_count/200 indique combien de pages ont répondu correctement ; des 301 ou 302 signalent des redirections (souvent bénignes), des 404 des liens morts, des 429 un freinage du serveur pour cadence excessive. item_scraped_count et item_dropped_count donnent le rendement réel : combien de données conservées, combien écartées. elapsed_time_seconds mesure la durée totale, utile pour estimer le coût d’un crawl récurrent.

Prendre l’habitude de lire ces chiffres change la façon de déboguer : au lieu de deviner, vous constatez. Un item_scraped_count à 980 au lieu de 1000 vous oriente aussitôt vers vingt rejets à expliquer dans le journal ; une volée de 429 vous dit d’augmenter le délai. C’est le tableau de bord d’un scraper sérieux, et le premier endroit à regarder quand un résultat surprend.

🐞 Pièges fréquents

Symptôme / erreur Cause probable Correctif
La pipeline ne s’exécute pas Oubli de la déclarer dans ITEM_PIPELINES Ajouter la classe avec son chemin complet et un numéro de priorité
item_dropped_count très élevé Sélecteur ou nettoyage trop strict Lire la raison des DropItem dans le journal et ajuster
Le crawl est anormalement lent DOWNLOAD_DELAY ou AutoThrottle trop conservateur Normal et souhaitable sur un site tiers ; ne réduire que sur votre propre serveur
Le fichier d’export reste vide Chemin FEEDS erroné ou pipeline qui jette tout Vérifier le chemin et les stats item_scraped_count

🌍 Adaptation au contexte ouest-africain

AutoThrottle est un allié inattendu sur une connexion partagée ou instable : en s’adaptant aux temps de réponse, il évite de saturer le lien quand la bande passante est faible, et limite les requêtes perdues qu’il faudrait rejouer. Pour la mise au point, deux réglages font gagner data et temps : CLOSESPIDER_PAGECOUNT = 3 dans les settings arrête le crawl après trois pages, et le cache HTTP intégré de Scrapy (HTTPCACHE_ENABLED = True) rejoue les réponses déjà téléchargées sans repasser par le réseau. Vous développez ainsi vos pipelines hors ligne, ce qui change tout depuis un point d’accès facturé au volume à Niamey ou Conakry.

✅ Récapitulatif

Votre spider couvre désormais le catalogue entier grâce à response.follow, à un rythme réglé par DOWNLOAD_DELAY et AutoThrottle. Deux pipelines indépendantes nettoient les prix, dérivent la disponibilité et suppriment les doublons avant écriture, et FEEDS produit le CSV automatiquement. Vous avez vu la philosophie de Scrapy à l’œuvre : un spider qui extrait, des composants enfichables qui transforment, des réglages qui gouvernent le comportement. C’est cette séparation qui fait tenir un projet de scraping dans la durée. Reste un cas que ni requests ni Scrapy ne savent traiter seuls : les pages dont le contenu n’apparaît qu’après exécution du JavaScript.

🧾 Aide-mémoire

Élément Rôle
response.follow(href, callback=) Suivre un lien (gère le relatif)
DOWNLOAD_DELAY Délai minimal entre requêtes
AUTOTHROTTLE_ENABLED Auto-régulation de la cadence
process_item(self, item, spider) Méthode d’une pipeline
raise DropItem(...) Rejeter un item
ITEM_PIPELINES = {chemin: priorité} Activer et ordonner les pipelines
FEEDS = {fichier: {format, ...}} Export automatique

💪 À vous de jouer

Aminata veut être alertée quand un manuel passe sous un certain prix. Ajoutez une pipeline qui n’écrit que les livres à moins de 20 (unités du catalogue), tout en laissant les autres dans le journal.

Voir une solution
class FiltrePrixPipeline:
    SEUIL = 20.0

    def process_item(self, item, spider):
        if item["prix"] >= self.SEUIL:
            raise DropItem(f"Au-dessus du seuil : {item['titre']!r}")
        return item

À déclarer dans ITEM_PIPELINES avec une priorité supérieure à 100 (après le nettoyage, donc le prix est déjà un nombre). Le crawl ne conservera que les références sous le seuil.

Tutoriels frères

Pour aller plus loin

FAQ

Pipeline ou nettoyage dans parse ?
Les deux marchent, mais la pipeline sépare l’extraction de la transformation, ce qui rend chaque partie plus simple à tester et à faire évoluer. Sur un vrai projet, c’est l’option à privilégier.

AutoThrottle remplace-t-il DOWNLOAD_DELAY ?
Non, ils se complètent. DOWNLOAD_DELAY fixe un plancher, AutoThrottle ajuste au-dessus selon la charge observée. Garder les deux donne un crawl à la fois poli et réactif.

Comment reprendre un gros crawl interrompu ?
Scrapy sait sauvegarder son état avec l’option JOBDIR : le crawl reprend là où il s’était arrêté. Pratique pour les très gros sites ou les connexions instables ; voir la documentation des « jobs ».

Peut-on exporter en JSON plutôt qu’en CSV ?
Oui : il suffit de changer le format dans FEEDS (« json » ou « jsonlines »). Le format « jsonlines » — un objet JSON par ligne — est idéal pour les gros volumes : il s’écrit au fil de l’eau et se relit ligne par ligne, sans tout charger en mémoire. On peut même déclarer plusieurs sorties à la fois dans FEEDS.

Partager