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
- Architecture de Scrapy
- Comment récupérer une page : Spider et Request
- Extraction du contenu : les sélecteurs
- Le cyrillique dans Scrapy
- Parallélisme et vitesse
- Proxys
- Scraping via TOR
- HTTPS/SSL
- Gestion des cookies
- Statut de réponse et en-têtes
- Files d'attente et déduplication des URL
- Item Pipeline : stockage des données
- 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
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.
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 :
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 :
# 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 :
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 :
# 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 :
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 :
pip install scrapy-rotating-proxies
# 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 :
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) :
# 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 ».
9. Gestion des cookies
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 :
yield scrapy.Request(url, cookies={"sessionid": "abc123"})
Pour l'authentification, FormRequest est très pratique :
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 :
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é :
# 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 :
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 :
# 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()
# 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.