Screaming Frog est le crawler de référence pour la plupart des SEOs, mais vous avez probablement atteint ses limites : le plafond de 500 URLs sur la version gratuite, la RAM saturée sur les grands sites, ou le désir d’automatiser les crawls sans surveiller une interface graphique. Scrapy est le framework Python open source qui supprime ces limites.
Si vous pouvez exécuter npm install ou git clone, vous pouvez exécuter Scrapy. La courbe d’apprentissage est réelle mais gérable, surtout si vous vous familiarisez déjà avec les outils CLI via les workflows de codage agentique.
Pourquoi Scrapy ?
Screaming Frog fonctionne bien pour les audits rapides. Mais il a des limites :
| Limitation | Impact |
|---|---|
| Limite gratuite de 500 URLs | Nécessite une licence à 259$/an pour les sites plus grands |
| Gourmand en mémoire | Les grands crawls peuvent consommer 8Go+ de RAM |
| Dépendant du GUI | Difficile à automatiser ou planifier |
| Personnalisation limitée | Les options de configuration sont fixes |
Scrapy résout ces problèmes :
| Scrapy | Ce que vous obtenez |
|---|---|
| Gratuit et open source | Pas de limites d’URLs, pas de frais de licence |
| Empreinte mémoire réduite | Files d’attente sur disque gardent la RAM sous contrôle |
| Natif CLI | Scriptable, planifiable avec cron, prêt pour CI/CD |
| Personnalisation Python complète | Extrayez ce dont vous avez besoin, filtrez comme vous voulez |
| Pause/Reprise | Arrêtez et continuez les grands crawls à tout moment |
Installation
Scrapy fonctionne sur Python. Utilisez un environnement virtuel pour garder les choses propres :
Debian/Ubuntu :
sudo apt install python3.11-venv
python3 -m venv venv
source venv/bin/activate
pip install scrapy
macOS :
python3 -m venv venv
source venv/bin/activate
pip install scrapy
Windows :
python -m venv venv
venv\Scripts\activate
pip install scrapy
Créer un Projet
Avec Scrapy installé :
scrapy startproject myproject
cd myproject
scrapy genspider sitename example.com
Cela crée :
myproject/
scrapy.cfg
myproject/
__init__.py
items.py
middlewares.py
pipelines.py
settings.py
spiders/
__init__.py
sitename.py
Le code du spider va dans spiders/sitename.py. La configuration est dans settings.py.
Paramètres pour un Crawling Poli
Configurez settings.py avant d’exécuter quoi que ce soit. Être bloqué gaspille plus de temps que crawler lentement.
# Crawling poli
CONCURRENT_REQUESTS_PER_DOMAIN = 5
DOWNLOAD_DELAY = 1
ROBOTSTXT_OBEY = True
# AutoThrottle - ajuste la vitesse selon la réponse du serveur
AUTOTHROTTLE_ENABLED = True
AUTOTHROTTLE_START_DELAY = 5
AUTOTHROTTLE_MAX_DELAY = 60
AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0
AUTOTHROTTLE_DEBUG = True
# Limites de sécurité
CLOSESPIDER_PAGECOUNT = 10000
# Sortie
FEED_EXPORT_ENCODING = "utf-8"
AutoThrottle
AutoThrottle surveille les temps de réponse du serveur et ajuste automatiquement la vitesse de crawl :
- Réponses rapides → accélère
- Réponses lentes → ralentit
- Erreurs/timeouts → ralentit significativement
Contrairement aux délais fixes de Screaming Frog, il s’adapte aux conditions réelles du serveur.
Gestion des Codes de Statut
Par défaut, le HttpErrorMiddleware de Scrapy supprime silencieusement les réponses non-2xx. Cela signifie que les 404, 301, 500 sont éliminés avant d’atteindre votre callback. Votre crawl pourrait afficher 100% de codes de statut 200, non pas parce que le site est parfait, mais parce que les erreurs sont filtrées.
Ajoutez ceci à votre classe spider pour capturer tous les codes de statut :
handle_httpstatus_list = [200, 301, 302, 403, 404, 500, 502, 503]
Screaming Frog capture tous les codes de statut par défaut. Ce paramètre aligne Scrapy sur ce comportement.
Performance en Conditions Réelles
Chiffres réels d’un crawl de test avec 5 requêtes simultanées et AutoThrottle activé :
| Progression du Crawl | Pages/Minute | Notes |
|---|---|---|
| 0-200 pages | 14-22 | Montée en charge |
| 200-500 pages | 10-12 | Stabilisation |
| 500-1 000 pages | 7-10 | AutoThrottle s’ajuste |
| 1 000+ pages | 5-7 | État stable |
Comparaison des Fonctionnalités
| Fonctionnalité | Screaming Frog | Scrapy |
|---|---|---|
| Coût | Gratuit <500 URLs, ~259$/an | Gratuit, open source |
| Taille max de crawl | Limitée par la mémoire | Files d’attente sur disque |
| Personnalisation | Options de config limitées | Code Python complet |
| Planification | Manuel ou tiers | CLI natif, planifiable avec cron |
| Pause/Reprise | Oui | Oui (avec JOBDIR) |
| Courbe d’apprentissage | Faible (GUI) | Moyenne (code) |
| Limitation de débit | Délais fixes basiques | AutoThrottle (adaptatif) |
| Rendu JavaScript | Optionnel (Chrome) | Optionnel (playwright/splash) |
| Codes de statut | Tous par défaut | Nécessite configuration |
| Filtrage de sous-domaines | Cases à cocher GUI | Code (regex flexible) |
| Formats d’export | CSV, Excel, etc. | JSON, CSV, XML, personnalisé |
| Intégration CI/CD | Difficile | Native |
Filtrage d’URLs
Screaming Frog utilise des cases à cocher. Scrapy utilise du code. Le compromis est la courbe d’apprentissage contre la précision.
Exclure les chemins internationaux :
import re
from urllib.parse import urlparse
class MySiteSpider(scrapy.Spider):
name = "mysite"
allowed_domains = ["example.com", "www.example.com"]
start_urls = ["https://www.example.com/"]
# Ignorer les chemins internationaux comme /uk/, /fr/, /de/
EXCLUDED_PATTERNS = re.compile(
r"/(in|au|th|es|hk|sg|ph|my|ca|cn|uk|kr|id|fr|vn|de|jp|nl|it|tw)/"
)
def filter_links(self, links):
filtered = []
for link in links:
hostname = urlparse(link.url).hostname or ""
if hostname not in ("example.com", "www.example.com"):
continue
if self.EXCLUDED_PATTERNS.search(link.url):
continue
filtered.append(link)
return filtered
Vous pouvez filtrer par motifs d’URL, paramètres de requête, en-têtes de réponse, contenu de page ou toute combinaison.
Pause et Reprise
Pour les crawls de plus de 1 000 pages, activez pause/reprise avec JOBDIR :
scrapy crawl myspider -o output.json -s JOBDIR=crawl_state
Scrapy sauvegarde l’état dans crawl_state/. Appuyez sur Ctrl+C pour mettre en pause. Exécutez la même commande pour reprendre.
L’état comprend les URLs en attente, les URLs vues et la file d’attente des requêtes. C’est plus robuste que la fonctionnalité sauvegarder/charger de Screaming Frog car c’est basé sur des fichiers et survit aux redémarrages système.
Rendu JavaScript
Scrapy récupère uniquement le HTML brut. Il ne rend pas le JavaScript. C’est la même chose que ce que retourne curl.
Pour la plupart des crawls SEO, c’est suffisant :
- Les balises meta, canonicals et h1 sont généralement dans le HTML initial
- Les moteurs de recherche indexent principalement le contenu rendu côté serveur
- La plupart des sites e-commerce et de contenu sont rendus côté serveur
Si votre site cible rend le contenu côté client, vous avez des options :
| Package | Notes |
|---|---|
| scrapy-playwright | Utilise Chromium/Firefox/WebKit. Recommandé pour les sites JS modernes |
| scrapy-splash | Léger, moteur de rendu basé sur Docker |
| scrapy-selenium | Approche plus ancienne, fonctionne toujours |
Le rendu JS est significativement plus lent et consomme plus de ressources. Ne l’ajoutez que si le site le nécessite.
Screaming Frog a un compromis similaire. Activer le rendu JavaScript utilise Chrome en arrière-plan et ralentit considérablement les crawls.
Gestion de la Mémoire
À ~1 300 pages avec extraction complète des champs :
- Mémoire : ~265 Mo
- CPU : ~4%
Utiliser JOBDIR déplace les files d’attente de requêtes sur le disque, gardant la mémoire basse. Pour les très grands crawls (100k+ URLs), ajoutez ces paramètres :
MEMUSAGE_LIMIT_MB = 1024
MEMUSAGE_WARNING_MB = 800
SCHEDULER_DISK_QUEUE = 'scrapy.squeues.PickleFifoDiskQueue'
SCHEDULER_MEMORY_QUEUE = 'scrapy.squeues.FifoMemoryQueue'
Cela limite l’utilisation de la mémoire et force les files d’attente sur disque pour le scheduler.
Données de Sortie
Sortie basique du spider :
{
"url": "https://www.example.com/page/",
"title": "Titre de la Page Ici",
"status": 200
}
Pour les crawls SEO, vous voudrez des champs similaires à ce qu’exporte Screaming Frog :
def parse_page(self, response):
yield {
"url": response.url,
"status": response.status,
"title": response.css("title::text").get(),
"meta_description": response.css("meta[name='description']::attr(content)").get(),
"meta_robots": response.css("meta[name='robots']::attr(content)").get(),
"h1": response.css("h1::text").get(),
"canonical": response.css("link[rel='canonical']::attr(href)").get(),
"og_title": response.css("meta[property='og:title']::attr(content)").get(),
"og_description": response.css("meta[property='og:description']::attr(content)").get(),
"word_count": len(response.text.split()) if response.status == 200 else None,
"content_type": response.headers.get("Content-Type", b"").decode("utf-8", errors="ignore"),
}
Ajoutez ou supprimez des champs selon vos besoins. Les sélecteurs CSS fonctionnent pour n’importe quel élément de la page.
Formats d’export : JSON (-o output.json), JSON Lines (-o output.jsonl), CSV (-o output.csv), XML (-o output.xml).
JSON Lines est meilleur pour les grands crawls. Les fichiers sont valides ligne par ligne pendant le crawl, donc vous pouvez surveiller avec tail -f. Le JSON standard n’est pas valide tant que le crawl n’est pas terminé.
Screaming Frog → Scrapy
Mapper les workflows SF vers Scrapy :
| Action Screaming Frog | Équivalent Scrapy |
|---|---|
| Démarrer un nouveau crawl | scrapy crawl spidername |
| Définir le délai de crawl | DOWNLOAD_DELAY dans settings |
| Limiter les threads simultanés | CONCURRENT_REQUESTS_PER_DOMAIN |
| Respecter robots.txt | ROBOTSTXT_OBEY = True |
| Exporter en CSV | -o output.csv |
| Sauvegarder/Charger le crawl | -s JOBDIR=crawl_state |
| Filtrer les sous-domaines | Code dans le spider (regex) |
| Extraction personnalisée | Sélecteurs CSS/XPath dans parse() |
Changements de mentalité :
- La configuration est du code. Éditez
settings.pyau lieu de cocher des cases. - L’extraction est explicite. Vous écrivez quelles données capturer.
- La planification est native. Ajoutez des commandes à cron ou CI/CD.
- Le débogage c’est les logs. Activez
AUTOTHROTTLE_DEBUGpour voir ce qui se passe.
Workflow Complet
Avec les paramètres standard ci-dessus, vous pouvez avoir Scrapy installé et en train de crawler en moins de 15 minutes :
python3 -m venv venv
source venv/bin/activate # venv\Scripts\activate sur Windows
pip install scrapy
scrapy startproject urlcrawler
cd urlcrawler
scrapy genspider mysite example.com
# Éditez settings.py avec la config de crawl poli
# Éditez spiders/mysite.py avec votre logique de parse
scrapy crawl mysite -o urls.jsonl -s JOBDIR=crawl_state
Scrapy Shell
Pendant que vous construisez des configurations personnalisées, utilisez Scrapy Shell pour tester vos sélecteurs et paramètres de manière interactive :
scrapy shell "https://example.com"
Cela ouvre une console Python interactive avec la réponse déjà chargée. Testez les sélecteurs CSS et XPath en temps réel avant de les ajouter à votre spider :
>>> response.css('title::text').get()
'Example Domain'
>>> response.xpath('//h1/text()').get()
'Example Domain'
Scrapy Shell réduit considérablement le temps d’itération. Validez la logique d’extraction sans exécuter des crawls complets.
Modèle de Spider Complet
Un spider prêt pour la production avec filtrage d’URLs, gestion des codes de statut et extraction complète des champs SEO :
import re
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from urllib.parse import urlparse
class SEOSpider(CrawlSpider):
name = "seospider"
allowed_domains = ["example.com"]
start_urls = ["https://www.example.com"]
# Capturer tous les codes de statut HTTP, pas seulement 2xx
handle_httpstatus_list = [200, 301, 302, 403, 404, 500, 502, 503]
# Motifs d'URLs à exclure
EXCLUDED_PATTERNS = re.compile(
r"/(in|au|th|es|hk|sg|ph|my|ca|cn|uk|kr|id|fr|vn|de|jp|nl|it|tw)/"
)
rules = (
Rule(
LinkExtractor(allow=()),
callback="parse_page",
follow=True,
process_links="filter_links",
),
)
def filter_links(self, links):
filtered = []
for link in links:
parsed = urlparse(link.url)
hostname = parsed.hostname or ""
if hostname not in ("example.com", "www.example.com"):
continue
if self.EXCLUDED_PATTERNS.search(link.url):
continue
filtered.append(link)
return filtered
def parse_page(self, response):
yield {
"url": response.url,
"status": response.status,
"title": response.css("title::text").get(),
"meta_description": response.css("meta[name='description']::attr(content)").get(),
"meta_robots": response.css("meta[name='robots']::attr(content)").get(),
"h1": response.css("h1::text").get(),
"canonical": response.css("link[rel='canonical']::attr(href)").get(),
"og_title": response.css("meta[property='og:title']::attr(content)").get(),
"og_description": response.css("meta[property='og:description']::attr(content)").get(),
"word_count": len(response.text.split()) if response.status == 200 else None,
"content_type": response.headers.get("Content-Type", b"").decode("utf-8", errors="ignore"),
}
Remplacez example.com par votre domaine cible. Ajustez EXCLUDED_PATTERNS pour la structure d’URLs de votre site.
Quand Utiliser Lequel
Screaming Frog :
- Audits rapides sous 500 URLs
- Résultats nécessaires en minutes
- Exploration visuelle du site
- Pas à l’aise avec CLI
- Utiliser les données Screaming Frog avec Redirects.net
Scrapy :
- Sites de plus de 10 000 URLs
- Crawls automatisés et planifiés
- Besoins d’extraction personnalisée
- Intégration CI/CD
- Contraintes de mémoire
- Configurations versionnées
La Conclusion
Scrapy a une courbe de configuration plus raide que Screaming Frog, mais il supprime les limites pratiques qu’imposent les crawlers GUI. Pas de limites d’URLs, pas de frais de licence, utilisation mémoire réduite et automatisation native.
Commencez petit. Crawlez un site que vous connaissez. Utilisez des paramètres conservateurs. Comparez la sortie à Screaming Frog. Les données correspondront, mais vous aurez un outil qui passe à l’échelle.