Le scraping par langage 8 min de lecture

Web scraping de sites en Python avec Scrapy

Découverte du framework Scrapy : spiders, pipelines, middlewares et réglages qui transforment un script en scraper industriel.

ÉW
Équipe Web-Scraping.fr
Collecte de données pour votre activité
Publié le: 9 janvier 2025

Quand il faut parcourir non pas une page, mais tout un site — des milliers, voire des millions d'URL — un script maison se transforme vite en bouillie de files d'attente, de nouvelles tentatives et de rotation de proxys. Scrapy résout tout cela pour vous : c'est un framework de crawling asynchrone complet, avec files d'attente intégrées, déduplication, pipelines de traitement et système de middlewares.

Cet article prolonge le guide de synthèse « Web scraping en Python ». S'il vous faut une collecte ponctuelle sur quelques pages, requests + BeautifulSoup suffisent ; Scrapy révèle tout son potentiel à grande échelle.

Sommaire

  1. Architecture de Scrapy
  2. Comment récupérer une page : Spider et Request
  3. Extraction du contenu : les sélecteurs
  4. Le cyrillique dans Scrapy
  5. Parallélisme et vitesse
  6. Proxys
  7. Scraping via TOR
  8. HTTPS/SSL
  9. Gestion des cookies
  10. Statut de réponse et en-têtes
  11. Files d'attente et déduplication des URL
  12. Item Pipeline : stockage des données
  13. Avantages et inconvénients

1. Architecture de Scrapy

Scrapy repose sur un moteur asynchrone (Twisted) et se compose de composants interconnectés :

  • Spider — votre classe : de quelles URL partir et comment analyser les réponses.
  • Scheduler — file de requêtes avec déduplication et priorités.
  • Downloader — télécharge les pages de manière asynchrone.
  • Middlewares — intercepteurs de requêtes/réponses (proxys, en-têtes, nouvelles tentatives).
  • Item Pipeline — traitement et sauvegarde des données extraites.

Vous n'écrivez que le Spider et le Pipeline — le framework se charge de tout le reste.

Création du projet

bash
pip install scrapy
scrapy startproject myparser
cd myparser
scrapy genspider example example.com

2. Comment récupérer une page : Spider et Request

Avec Scrapy, vous n'écrivez pas la boucle de requêtes à la main — vous renvoyez (yield) des objets Request, et le moteur les exécute lui-même de manière asynchrone.

python
import scrapy

class CatalogSpider(scrapy.Spider):
    name = "catalog"
    start_urls = ["https://example.com/catalog"]

    custom_settings = {
        "USER_AGENT": "Mozilla/5.0 (compatible; MyBot/1.0)",
        "DOWNLOAD_DELAY": 1.0,          # pause entre les requêtes
        "ROBOTSTXT_OBEY": True,         # on respecte robots.txt
    }

    def parse(self, response):
        # on extrait les fiches produits
        for card in response.css(".product-card"):
            yield {
                "title": card.css(".title::text").get(),
                "price": card.css(".price::text").get(),
                "url": response.urljoin(card.css("a::attr(href)").get()),
            }

        # on passe à la page suivante
        next_page = response.css("a.next::attr(href)").get()
        if next_page:
            yield response.follow(next_page, callback=self.parse)

response.follow reconstruit automatiquement l'URL relative et met la requête en file d'attente. La pagination se décrit ainsi en quelques lignes. Lancement :

bash
scrapy crawl catalog -o products.json

3. Extraction du contenu : les sélecteurs

Sous le capot, Scrapy utilise la bibliothèque parsel (basée sur lxml), qui prend en charge à la fois CSS et XPath :

python
# CSS
response.css("h1::text").get()
response.css(".price::text").getall()
response.css("a::attr(href)").getall()

# XPath
response.xpath("//h1/text()").get()
response.xpath('//div[@class="price"]/text()').get()

# expressions régulières directement dans le sélecteur
response.css(".price::text").re_first(r"\d+")

get() renvoie le premier résultat (ou None), getall() — une liste. C'est plus pratique que lxml « brut » grâce à la gestion sûre des éléments absents. Pour en savoir plus sur XPath, consultez « Web scraping en Python avec lxml ».

Si la page interroge en réalité une API et renvoie du JSON plutôt que du HTML, Scrapy permet de l'analyser directement via response.json() — sans aucun sélecteur. Le nettoyage et la structuration de ce type de réponses sont détaillés dans « Parser du JSON en Python ».

Items et ItemLoader

Pour les projets structurés, il est pratique de décrire les données sous forme d'Item et de les remplir via un ItemLoader avec des processeurs de nettoyage (suppression des espaces, conversion de types). Pour des spiders simples, de simples dictionnaires suffisent, comme ci-dessus.


4. Encodages et Unicode dans Scrapy

La plupart du temps, Scrapy détecte correctement l'encodage à partir des en-têtes et de <meta charset> — le cyrillique « fonctionne tout seul ». Si vous tombez sur des caractères illisibles, vous pouvez indiquer explicitement l'encodage à la création de la réponse ou décoder le corps manuellement :

python
def parse(self, response):
    # relire le corps de force dans le bon encodage
    text = response.body.decode("windows-1251", errors="replace")
    sel = scrapy.Selector(text=text)

La théorie générale des problèmes d'encodage se trouve dans le guide central, section « Encodages ». Vérifiez aussi FEED_EXPORT_ENCODING = "utf-8" dans les réglages, afin que le cyrillique ne se transforme pas en \uXXXX dans le JSON final.


5. Parallélisme et vitesse

Le principal atout de Scrapy est l'asynchronisme prêt à l'emploi. Des dizaines de requêtes s'exécutent simultanément sans threads. Tout se règle via les paramètres :

python
# settings.py
CONCURRENT_REQUESTS = 16              # nombre total de requêtes simultanées
CONCURRENT_REQUESTS_PER_DOMAIN = 8    # par domaine
DOWNLOAD_DELAY = 0.5                  # pause de base
AUTOTHROTTLE_ENABLED = True           # ajustement automatique de la vitesse
AUTOTHROTTLE_TARGET_CONCURRENCY = 4.0

AutoThrottle est une fonctionnalité intelligente : Scrapy réduit lui-même la cadence si le serveur commence à répondre plus lentement, en équilibrant vitesse et politesse. Cela vous évite de calibrer les délais à la main. Comme tout est asynchrone, le multithreading est inutile — comparez avec l'approche décrite dans « Scraping asynchrone en Python », que Scrapy implémente « sous le capot ».


6. Proxys

Le moyen le plus simple : définir le proxy dans le meta de la requête :

python
yield scrapy.Request(url, meta={"proxy": "http://user:pass@ip:port"})

Pour la rotation d'un pool, un paquet prêt à l'emploi est plus commode :

bash
pip install scrapy-rotating-proxies
python
# settings.py
ROTATING_PROXY_LIST = [
    "ip1:port",
    "ip2:port",
    "ip3:port",
]
DOWNLOADER_MIDDLEWARES = {
    "rotating_proxies.middlewares.RotatingProxyMiddleware": 610,
    "rotating_proxies.middlewares.BanDetectionMiddleware": 620,
}

Le middleware assure lui-même la rotation des proxys, repère ceux qui sont « bannis » et écarte les proxys morts. La théorie générale se trouve dans le guide central, section « Proxys ».


7. Scraping via TOR

TOR se branche comme proxy SOCKS5 via meta :

python
yield scrapy.Request(url, meta={"proxy": "socks5h://127.0.0.1:9050"})

Le changement de nœud de sortie via stem (signal NEWNYM) est décrit dans le guide central, section « TOR ». En pratique, pour Scrapy on préfère souvent des proxys rotatifs payants — ils sont plus rapides et moins souvent bloqués que les nœuds de sortie de TOR.


8. HTTPS/SSL

Par défaut, Scrapy vérifie les certificats. S'il faut assouplir la vérification pour un site problématique (en connaissance de cause uniquement) :

python
# settings.py
DOWNLOADER_CLIENT_TLS_METHOD = "TLS"
# pour les certificats auto-signés, on peut configurer la context factory

Dans la plupart des cas, SSL « fonctionne tout seul ». Les principes généraux de sécurité de la connexion sont dans le guide central, section « HTTPS/SSL ».


Les cookies sont activés par défaut dans Scrapy (COOKIES_ENABLED = True) — le moteur conserve automatiquement la session entre les requêtes. Pour transmettre un cookie manuellement :

python
yield scrapy.Request(url, cookies={"sessionid": "abc123"})

Pour l'authentification, FormRequest est très pratique :

python
def parse(self, response):
    return scrapy.FormRequest.from_response(
        response,
        formdata={"username": "user", "password": "pass"},
        callback=self.after_login,
    )

from_response récupère lui-même les champs cachés du formulaire (y compris le jeton CSRF) — ce qui élimine le casse-tête classique de l'authentification.


10. Statut de réponse et en-têtes

L'accès au statut et aux en-têtes se fait via l'objet response :

python
def parse(self, response):
    print(response.status)              # 200, 404 ...
    print(response.headers.get("Content-Type"))

Par défaut, Scrapy ne traite que les 2xx et ignore les 4xx/5xx. La gestion des nouvelles tentatives passe par le RetryMiddleware intégré :

python
# settings.py
RETRY_ENABLED = True
RETRY_TIMES = 3
RETRY_HTTP_CODES = [429, 500, 502, 503, 504, 403]

Scrapy sait respecter le statut 429 (Too Many Requests) ainsi que l'en-tête Retry-After. La logique des statuts en général est décrite dans le guide central, section « Statut et en-têtes ».


11. Files d'attente et déduplication des URL

C'est ici que Scrapy excelle particulièrement — ce qu'il faut construire à la main dans un scraper maison (voir le guide central, section « Files d'attente ») est intégré d'origine :

  • Le Scheduler maintient une file de requêtes avec priorités.
  • Le Dupefilter écarte automatiquement les URL déjà rencontrées (par empreinte de la requête).
  • La file peut être déportée sur disque (JOBDIR) pour reprendre un crawl interrompu :
bash
scrapy crawl catalog -s JOBDIR=crawls/catalog-1

Pour le crawling distribué sur plusieurs machines, il existe scrapy-redis — file d'attente et dupefilter partagés dans Redis, ce qui permet à plusieurs workers de parcourir un même site conjointement.


12. Item Pipeline : stockage des données

Les éléments extraits passent par le pipeline — c'est là qu'ils sont validés, nettoyés et sauvegardés :

python
# pipelines.py
import pymongo

class MongoPipeline:
    def open_spider(self, spider):
        self.client = pymongo.MongoClient("mongodb://localhost:27017")
        self.db = self.client["scraping"]

    def process_item(self, item, spider):
        self.db["products"].update_one(
            {"url": item["url"]}, {"$set": dict(item)}, upsert=True
        )
        return item

    def close_spider(self, spider):
        self.client.close()
python
# settings.py
ITEM_PIPELINES = {"myparser.pipelines.MongoPipeline": 300}

Pour un export simple, aucun pipeline n'est nécessaire — le drapeau -o products.csv (ou .json, .jsonl) enregistre le résultat directement.


13. Avantages et inconvénients de Scrapy

Avantages :

  • Asynchronisme, files d'attente, déduplication et nouvelles tentatives — prêts à l'emploi.
  • Hautes performances sur un grand nombre de pages.
  • Architecture propre : Spider, Middleware et Pipeline bien séparés.
  • Extensions prêtes à l'emploi : rotation de proxys, scrapy-redis, auto-throttling.
  • Tâches reprenables et export pratique vers n'importe quel format.

Inconvénients :

  • Courbe d'apprentissage élevée — il faut comprendre l'architecture et Twisted.
  • Surdimensionné pour quelques pages (requests + BeautifulSoup y sont plus simples).
  • Les sites JavaScript exigent une intégration (scrapy-playwright ou Splash).
  • Le modèle asynchrone de Twisted déroute face à l'asyncio moderne.

Pour les sites dynamiques, on ajoute scrapy-playwright, qui rend les pages avec un vrai navigateur. Si le contenu est simple et statique, et que l'infrastructure Django existe déjà, il est parfois plus simple de scraper avec Django.