Développement Web

Creer un spider Scrapy : projet, selecteurs et items

13 min de lecture

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

Troisième étape de la série. On bascule ici du script artisanal (requests + BeautifulSoup) vers un vrai framework. Avoir fait les deux premiers tutoriels aide, mais ce tutoriel se suit aussi seul.

Le collecteur d’Aminata fonctionne. Mais à chaque besoin nouveau — réessayer une page, exporter en plusieurs formats, suivre les liens vers les fiches détaillées — il faut rallonger le script et tout maintenir soi-même. Arrive un moment où l’on réinvente, en moins bien, ce qu’un outil dédié fait déjà. Pour le scraping, cet outil s’appelle Scrapy : un framework Python qui impose une structure claire, gère le réseau pour vous et transforme un script jetable en projet durable.

On reconstruit donc « VeilleManuels », cette fois en projet Scrapy. Vous allez voir que la logique d’extraction reste la même qu’avec BeautifulSoup ; ce qui change, c’est tout l’échafaudage autour, qui devient gratuit. On reste sur le catalogue-école books.toscrape.com.

🎯 Ce que vous allez apprendre

  • Créer un projet Scrapy et comprendre le rôle de chacun de ses fichiers.
  • Écrire un spider : la classe qui dit quoi visiter et quoi en extraire.
  • Utiliser les sélecteurs CSS de Scrapy (::text, ::attr) et les mettre au point dans le shell interactif.
  • Structurer la sortie avec un Item et exporter le résultat en une seule commande.

🛠️ Ce que vous allez construire

Un projet Scrapy nommé veille, contenant un spider livres qui visite une page du catalogue, en extrait titre, prix et stock sous forme d’Item structurés, et les écrit dans un fichier — sans une ligne de code réseau écrite à la main. C’est le socle que le tutoriel suivant étendra à tout le catalogue avec throttling et pipelines.

Prérequis

  • Python 3.13 et un environnement virtuel actif (revoir le premier tutoriel si besoin).
  • Installer le framework : pip install scrapy (version 2.16 au moment d’écrire).
  • Connaître les sélecteurs CSS de base — sinon, le premier tutoriel les introduit.
  • ⏱️ Temps estimé : ~45 minutes.

Étape 1 — Créer le projet et lire son anatomie

Scrapy ne se contente pas d’un fichier : il génère une arborescence de projet. Cette structure peut sembler lourde au début, mais chaque fichier a un rôle précis, et c’est elle qui rend le projet maintenable. On la crée d’une commande.

scrapy startproject veille
cd veille

Vous obtenez l’arborescence suivante, qu’il vaut la peine de comprendre une bonne fois :

veille/
├── scrapy.cfg            # configuration de déploiement
└── veille/
    ├── settings.py       # réglages : politesse, robots.txt, exports
    ├── items.py          # définition de la forme des données
    ├── pipelines.py      # traitement des données après extraction
    ├── middlewares.py    # interception des requêtes/réponses
    └── spiders/          # vos spiders vivent ici

Trois fichiers nous intéressent aujourd’hui : settings.py (les réglages de comportement), items.py (la structure des données) et le dossier spiders/ (la logique de collecte). Les deux autres — pipelines.py et middlewares.py — entreront en jeu au tutoriel suivant. Bonne nouvelle d’emblée : dans un projet récent, settings.py active par défaut ROBOTSTXT_OBEY = True, c’est-à-dire le respect du fichier robots.txt des sites visités. Le framework vous pousse vers les bonnes pratiques.

Étape 2 — Générer un spider

Un spider est une classe Python qui décrit deux choses : par où commencer (les URL de départ) et quoi faire de chaque page reçue (la méthode parse). Scrapy fournit un générateur pour partir d’un squelette correct plutôt que d’une page blanche.

scrapy genspider livres books.toscrape.com

Cette commande crée spiders/livres.py. On y ajuste l’URL de départ pour viser directement la page du catalogue (le générateur met l’accueil du domaine par défaut) :

import scrapy

class LivresSpider(scrapy.Spider):
    name = "livres"
    allowed_domains = ["books.toscrape.com"]
    start_urls = ["https://books.toscrape.com/catalogue/page-1.html"]

    def parse(self, response):
        ...  # on remplit à l'étape suivante

Trois attributs structurent tout. Le name identifie le spider quand on le lance. allowed_domains est un garde-fou : Scrapy refusera de sortir de ce domaine, ce qui évite qu’un lien mal formé envoie votre robot crawler l’Internet entier. start_urls liste les points de départ. La méthode parse recevra la réponse de chaque URL de départ : c’est là qu’on travaille.

Le modèle mental : ce que Scrapy fait à votre place

Avant de remplir parse, une image mentale aide à comprendre pourquoi ce framework existe. Quand vous lancez le crawl, Scrapy place vos URL de départ dans une file d’attente, le scheduler. Un autre composant, le downloader, retire les requêtes de cette file et les exécute — plusieurs en parallèle, de façon asynchrone, sans que vous ayez à manipuler le moindre thread. Chaque réponse revient à votre méthode parse ; ce que vous y produisez avec yield suit deux chemins selon sa nature : un Item part vers les pipelines de traitement, une nouvelle Request retourne dans la file. La boucle tourne jusqu’à ce que la file soit vide.

Deux conséquences concrètes. D’abord, Scrapy est rapide sans le moindre effort de votre part : pendant qu’une page attend le réseau, d’autres se téléchargent déjà. Votre script séquentiel précédent traitait une page à la fois, en bloquant à chaque requête ; Scrapy en mène plusieurs de front. Ensuite, et c’est l’essentiel, vous ne pilotez plus le comment — connexions, parallélisme, gestion de la file — mais seulement le quoi : quelles pages visiter, quelles données en tirer. Un framework, c’est exactement cette inversion de la charge.

Tout ce comportement se règle dans settings.py : nombre de requêtes simultanées, délai entre elles, respect de robots.txt. On y reviendra en détail au tutoriel suivant ; pour l’instant, les valeurs par défaut, prudentes, conviennent parfaitement à notre apprentissage.

Étape 3 — Extraire avec les sélecteurs de Scrapy

Scrapy intègre son propre moteur de sélecteurs, très proche de ce que vous connaissez déjà. La différence visible : on récupère le texte avec le pseudo-élément ::text et un attribut avec ::attr(nom), puis on appelle .get() pour une valeur ou .getall() pour une liste. Pour un premier résultat rapide, on produit directement un dictionnaire avec yield.

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

Le mot-clé yield est au cœur de Scrapy : chaque dictionnaire produit est envoyé dans le pipeline du framework, qui se charge de l’écrire où vous voulez. Pour le stock, le texte est entrecoupé d’espaces dans le HTML ; on récupère donc tous les fragments avec getall() et on les recolle avant de nettoyer. Lancez maintenant le spider et demandez un export :

scrapy crawl livres -O livres.json

Le -O majuscule écrit (et écrase) le fichier ; le -o minuscule, lui, ajoute à un fichier existant. Ouvrez livres.json : vous y trouvez vos vingt livres. En une commande, Scrapy a téléchargé la page, appliqué votre parse, et sérialisé le résultat.

Point d’étapelivres.json contient 20 entrées avec titre, prix et stock. Si le fichier est vide, relisez la sortie du terminal : Scrapy y affiche le nombre d’items item_scraped_count et la moindre erreur de sélecteur.

Étape 4 — Mettre au point ses sélecteurs dans le shell

Tâtonner en relançant le crawl à chaque essai est lent et bruyant. Scrapy offre bien mieux : un shell interactif qui télécharge une page une fois et vous laisse essayer vos sélecteurs à la main, instantanément. C’est l’outil que les habitués utilisent en permanence, et il vous fera gagner un temps fou.

scrapy shell "https://books.toscrape.com/catalogue/page-1.html"

# Dans le shell, l'objet "response" est déjà chargé :
>>> response.css("article.product_pod").get()        # voir le premier bloc
>>> response.css("h3 a::attr(title)").get()           # le premier titre
>>> len(response.css("p.price_color::text").getall()) # combien de prix ? -> 20

Vous testez, vous voyez le résultat, vous ajustez — sans relancer quoi que ce soit. Une fois le bon sélecteur trouvé, vous le recopiez dans parse. Prenez l’habitude d’ouvrir le shell avant d’écrire la moindre ligne de parse : c’est la différence entre coder à l’aveugle et coder en regardant.

Étape 5 — Structurer la sortie avec un Item

Un dictionnaire fonctionne, mais rien n’empêche une faute de frappe ("prixe" au lieu de "prix") de passer inaperçue jusqu’au fichier final. Un Item déclare à l’avance les champs autorisés : Scrapy refuse alors tout champ non prévu, ce qui attrape les erreurs au plus tôt. On le définit dans items.py.

# items.py
import scrapy

class LivreItem(scrapy.Item):
    titre = scrapy.Field()
    prix = scrapy.Field()
    stock = scrapy.Field()

Puis on l’utilise dans le spider à la place du dictionnaire. Le code se lit de la même façon, mais gagne en sûreté :

# spiders/livres.py
from veille.items import LivreItem

    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

Si vous tentez d’écrire item["auteur"] sans l’avoir déclaré, Scrapy lève une KeyError immédiate. Cette rigueur paraît contraignante sur trois champs ; elle devient salvatrice sur un projet qui en compte quinze et que l’on reprend trois mois plus tard.

Étape 6 — Relancer, exporter et vérifier

L’extraction passe maintenant par un Item ; le reste ne change pas. On relance en demandant cette fois un CSV, le format qu’attend Aminata.

scrapy crawl livres -O livres.csv

À la fin du crawl, Scrapy affiche un récapitulatif (les stats) : nombre de requêtes, d’items extraits, durée. C’est votre tableau de bord. Cherchez la ligne item_scraped_count : elle doit indiquer 20. Le CSV produit a une colonne par champ de l’Item, dans l’ordre déclaré.

Point d’étapescrapy crawl livres -O livres.csv se termine sans erreur, item_scraped_count vaut 20, et livres.csv a trois colonnes nommées. Vous tenez un projet Scrapy fonctionnel.

🐞 Pièges fréquents

Symptôme / erreur Cause probable Correctif
Unknown command: crawl Commande lancée hors du dossier du projet Se placer dans le dossier contenant scrapy.cfg avant de lancer crawl
0 items scraped et un message « Forbidden by robots.txt » ROBOTSTXT_OBEY bloque l’URL Vérifier que le site autorise la page ; ne désactiver le réglage que si vous en avez le droit
ModuleNotFoundError: veille.items Mauvais chemin d’import dans le spider Importer depuis le paquet du projet : from veille.items import LivreItem
Le champ prix contient None Sélecteur incorrect Tester le sélecteur dans scrapy shell jusqu’à obtenir la bonne valeur

🌍 Adaptation au contexte ouest-africain

Le scrapy shell est un allié précieux quand la connexion est chère ou instable : il télécharge la page une seule fois, puis vous travaillez vos sélecteurs hors ligne, sans nouvelle requête. Vous pouvez aussi enregistrer une réponse pour la rejouer plus tard. Côté installation, pip install scrapy tire quelques dépendances compilées ; sur une connexion lente, lancez l’installation une fois pour toutes et conservez le cache de pip (~/.cache/pip) pour ne pas re-télécharger à chaque nouvel environnement. Sur un poste modeste d’un cybercafé à Bamako ou Niamey, Scrapy reste léger : c’est un outil en ligne de commande, sans interface gourmande.

✅ Récapitulatif

Vous êtes passé d’un script à un projet : une arborescence où chaque fichier a sa place, un spider qui déclare ses points de départ et sa logique d’extraction, des sélecteurs mis au point dans le shell interactif, et une sortie structurée par un Item exportable en une commande. La logique d’extraction n’a pas changé par rapport à BeautifulSoup — mais le réseau, la sérialisation et le respect de robots.txt sont désormais pris en charge par le framework. Reste à faire crawler le catalogue entier, proprement et sans surcharger le serveur : c’est l’objet du tutoriel suivant.

🧾 Aide-mémoire

Commande / élément Rôle
scrapy startproject nom Créer un projet
scrapy genspider nom domaine Générer un spider
scrapy crawl nom -O fichier.csv Lancer le spider et exporter (écrase)
scrapy shell "url" Tester ses sélecteurs en interactif
response.css("sel::text").get() Extraire un texte
response.css("sel::attr(x)").get() Extraire un attribut
scrapy.Item / scrapy.Field() Déclarer la forme des données

💪 À vous de jouer

Chaque livre du catalogue a sa propre fiche détaillée, atteignable par le lien du titre. Ajoutez à l’Item un champ url_fiche contenant l’URL absolue de cette fiche.

Voir une solution
            lien_relatif = livre.css("h3 a::attr(href)").get()
            item["url_fiche"] = response.urljoin(lien_relatif)

Déclarez d’abord le champ dans items.py (url_fiche = scrapy.Field()), sinon Scrapy lève une KeyError. response.urljoin() est l’équivalent Scrapy de urljoin : il reconstruit une URL absolue à partir du lien relatif et de l’adresse de la page courante. Pratique pour, plus tard, suivre ces liens et scraper les fiches elles-mêmes.

Tutoriels frères

Pour aller plus loin

FAQ

Scrapy ou BeautifulSoup, lequel apprendre ?
Les deux ont leur place. BeautifulSoup est parfait pour un besoin ponctuel sur quelques pages. Scrapy s’impose dès qu’il faut crawler un site, gérer la reprise, exporter proprement et maintenir le tout dans la durée. Le guide principal détaille ce choix.

Faut-il connaître XPath ?
Non, les sélecteurs CSS suffisent pour l’immense majorité des cas. XPath devient utile pour des sélections complexes (remonter vers un parent, filtrer sur le texte). Scrapy gère les deux : response.xpath(...).

Pourquoi yield et pas return ?
Parce qu’une page peut produire plusieurs items et plusieurs requêtes à suivre. yield les émet un par un dans le moteur de Scrapy, qui les traite au fil de l’eau ; return arrêterait la fonction au premier.

Partager