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.
Intégration de Sitemap
Screaming Frog dispose d’une simple case à cocher “utiliser sitemap”. Scrapy nécessite du code personnalisé, mais vous donne un contrôle total sur la façon dont les sitemaps sont parsés et intégrés à votre crawl.
Pourquoi ajouter le support des sitemaps ?
- Découvre des URLs non liées depuis la navigation principale
- Trouve des pages orphelines que le crawl basé sur les liens manquerait
- Obtient la liste “officielle” des URLs du site pour comparaison
- Peut révéler plus de pages que le suivi des liens seul
- Essentiel pour des audits SEO complets
Ajoutez ces méthodes à votre CrawlSpider pour activer la détection et le parsing des sitemaps :
def start_requests(self):
# First, fetch robots.txt to find sitemaps
yield Request(
"https://www.example.com/robots.txt",
callback=self.parse_robots,
errback=self.handle_error,
dont_filter=True,
)
# Also try common sitemap locations directly
common_sitemaps = [
"https://www.example.com/sitemap.xml",
"https://www.example.com/sitemap_index.xml",
]
for sitemap_url in common_sitemaps:
yield Request(
sitemap_url,
callback=self.parse_sitemap,
errback=self.handle_error,
meta={"sitemap_url": sitemap_url},
)
# Also start normal crawl from homepage
for url in self.start_urls:
yield Request(url, callback=self.parse_start_url)
def parse_robots(self, response):
"""Parse robots.txt to find sitemap declarations"""
if response.status != 200:
return
for line in response.text.splitlines():
line = line.strip()
if line.lower().startswith("sitemap:"):
sitemap_url = line.split(":", 1)[1].strip()
if self.is_valid_url(sitemap_url):
self.logger.info(f"Found sitemap in robots.txt: {sitemap_url}")
yield Request(
sitemap_url,
callback=self.parse_sitemap,
errback=self.handle_error,
meta={"sitemap_url": sitemap_url},
)
def parse_sitemap(self, response):
"""Parse XML sitemap or sitemap index"""
if response.status != 200:
return
content_type = response.headers.get("Content-Type", b"").decode("utf-8", errors="ignore")
# Check if this is XML content
if "xml" not in content_type and not response.text.strip().startswith("<?xml"):
return
# Check for sitemap index (contains other sitemaps)
sitemap_locs = response.xpath("//sitemap/loc/text()").getall()
if sitemap_locs:
self.logger.info(f"Found sitemap index with {len(sitemap_locs)} sitemaps")
for loc in sitemap_locs:
if self.is_valid_url(loc):
yield Request(
loc,
callback=self.parse_sitemap,
errback=self.handle_error,
meta={"sitemap_url": loc},
)
# Parse URL entries from sitemap
url_locs = response.xpath("//url/loc/text()").getall()
if url_locs:
self.logger.info(f"Found {len(url_locs)} URLs in sitemap: {response.url}")
for loc in url_locs:
if self.is_valid_url(loc) and self.should_crawl_url(loc):
yield Request(
loc,
callback=self.parse_page,
errback=self.handle_error,
)
def parse_start_url(self, response):
"""Handle the start URL and trigger rules"""
yield from self.parse_page(response)
yield from self._requests_to_follow(response)
def is_valid_url(self, url):
"""Check if URL is valid and within allowed domains"""
try:
parsed = urlparse(url)
hostname = parsed.hostname or ""
return hostname in ("example.com", "www.example.com")
except Exception:
return False
def should_crawl_url(self, url):
"""Apply the same filtering as filter_links"""
if self.EXCLUDED_PATTERNS.search(url):
return False
return True
def handle_error(self, failure):
"""Handle request errors gracefully"""
self.logger.warning(f"Request failed: {failure.request.url}")
Comment ça fonctionne :
start_requests()surcharge le comportement par défaut pour récupérer d’abord les sitemapsparse_robots()trouve les lignesSitemap:dans robots.txtparse_sitemap()gère à la fois les index de sitemap et les sitemaps réguliers- XPath
//sitemap/loctrouve les sitemaps imbriqués dans les fichiers d’index de sitemap - XPath
//url/loctrouve les URLs de pages réelles - Le même filtrage de domaine et de motifs s’applique aux URLs de sitemap
- La déduplication intégrée de Scrapy empêche le double crawl des pages trouvées à la fois dans les sitemaps et les liens
| Fonctionnalité | Screaming Frog | Scrapy |
|---|---|---|
| Détection de sitemap | Case à cocher | Code personnalisé |
| Parsing de robots.txt | Automatique | Code personnalisé |
| Support d’index de sitemap | Oui | Oui (avec code) |
| Filtrage d’URLs | Options GUI | Code (contrôle total) |
| Fusion avec le crawl | Oui | Oui |
| Emplacements de sitemap personnalisés | Saisie manuelle | Code pour tout emplacement |
Avec l’intégration de sitemap, vous pouvez découvrir des pages orphelines non liées depuis la navigation, du contenu archivé ancien encore listé dans les sitemaps, des variations d’URLs avec ou sans slash final, et des pages bloquées par robots.txt mais toujours présentes dans le sitemap. Cela donne une image plus complète du site pour les audits SEO.
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.