Lorsque le scraping n'est pas un simple script ponctuel, mais une brique d'un véritable service web — avec interface, planification, stockage en base de données et traitement en arrière-plan — Django s'impose. Le framework fournit un ORM pour le stockage, des commandes de management pour le lancement et, associé à Celery, des tâches en arrière-plan et périodiques.
Cet article est le prolongement pratique du guide de référence « Web scraping avec Python ». Les techniques de base (requests, BeautifulSoup, encodages) y sont détaillées ; ici, l'accent est mis sur l'intégration dans Django.
Sommaire
- Pourquoi scraper dans Django
- Comment récupérer la page
- Bibliothèques pour parser le contenu
- Stockage des résultats dans les modèles
- Lancer le scraping : commandes de management et Celery
- Le cyrillique dans Django
- Multithreading et tâches en arrière-plan
- Proxys
- Scraping via TOR
- HTTPS/SSL
- Gestion des cookies
- Code de statut et en-têtes de réponse
- Stockage des URL et des files d'attente via l'ORM
- Avantages et inconvénients
1. Pourquoi scraper dans Django
Django se justifie lorsque le scraping n'est qu'une partie du produit :
- agrégateur de produits/offres d'emploi/actualités avec vitrine web ;
- veille tarifaire avec historique en base de données et tableau de bord ;
- mise à jour régulière et planifiée d'un catalogue ;
- panneau d'administration pour gérer les sources et consulter les résultats.
Django fournit « clé en main » : un ORM (stockage et déduplication), une interface d'administration (gestion des sources), un système de migrations, et avec Celery — l'exécution en arrière-plan et périodique, afin que le scraping lourd ne bloque pas les requêtes web.
2. Comment récupérer la page
Le client HTTP dans Django ne diffère en rien du Python classique — on prend requests ou httpx. La logique de scraping reste en dehors des views : les views doivent être rapides, et les requêtes réseau sont déportées vers une couche de services ou des tâches Celery.
# parser/services.py
import requests
HEADERS = {
"User-Agent": "Mozilla/5.0 (compatible; MyParserBot/1.0)",
"Accept-Language": "ru-RU,ru;q=0.9",
}
def fetch_page(url: str) -> str | None:
try:
resp = requests.get(url, headers=HEADERS, timeout=15)
resp.raise_for_status()
resp.encoding = resp.apparent_encoding
return resp.text
except requests.RequestException as exc:
# on journalise sans faire tomber l'application
import logging
logging.getLogger("parser").warning("fetch failed %s: %s", url, exc)
return None
Règle d'or : ne lancez jamais un scraping long directement dans une view — l'utilisateur attendrait et le worker du serveur web serait bloqué. La requête est mise en file d'attente et la réponse est renvoyée immédiatement.
3. Bibliothèques pour parser le contenu
Au sein du service, on utilise les mêmes outils que partout ailleurs — BeautifulSoup pour la commodité, lxml pour la vitesse :
from bs4 import BeautifulSoup
def parse_products(html: str) -> list[dict]:
soup = BeautifulSoup(html, "lxml")
items = []
for card in soup.select(".product-card"):
items.append({
"title": card.select_one(".title").get_text(strip=True),
"price": card.select_one(".price").get_text(strip=True),
"url": card.select_one("a")["href"],
})
return items
Une analyse détaillée des parseurs se trouve dans « Web scraping avec Python » et « lxml ». Si la source contient des tableaux, voir « Extraction de tableaux avec BeautifulSoup ». Si l'API renvoie du JSON — « Parsing de JSON ».
4. Stockage des résultats dans les modèles
La force de Django, c'est l'ORM. On décrit un modèle, et la déduplication, le filtrage et l'historique des mises à jour deviennent triviaux.
# parser/models.py
from django.db import models
class Product(models.Model):
source_url = models.URLField(unique=True) # unicité = protection contre les doublons
title = models.CharField(max_length=500)
price = models.DecimalField(max_digits=10, decimal_places=2, null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.title
Enregistrement avec protection contre les doublons via update_or_create :
from .models import Product
def save_products(items: list[dict]):
for item in items:
Product.objects.update_or_create(
source_url=item["url"],
defaults={"title": item["title"], "price": item["price"]},
)
update_or_create met à jour l'enregistrement existant ou en crée un nouveau — idéal pour un scraping récurrent.
5. Lancer le scraping : commandes de management et Celery
Commande de management — pour un lancement manuel ou via cron
# parser/management/commands/run_parser.py
from django.core.management.base import BaseCommand
from parser.services import fetch_page, parse_products, save_products
class Command(BaseCommand):
help = "Lance le scraping du catalogue"
def add_arguments(self, parser):
parser.add_argument("--url", required=True)
def handle(self, *args, **options):
html = fetch_page(options["url"])
if html:
items = parse_products(html)
save_products(items)
self.stdout.write(self.style.SUCCESS(f"{len(items)} produits enregistrés"))
Lancement : python manage.py run_parser --url https://example.com/catalog. Une telle commande se branche facilement sur le cron du système.
Celery — pour les tâches en arrière-plan et périodiques
# parser/tasks.py
from celery import shared_task
from .services import fetch_page, parse_products, save_products
@shared_task(bind=True, max_retries=3, default_retry_delay=60)
def parse_url_task(self, url):
html = fetch_page(url)
if html is None:
raise self.retry() # nouvelle tentative en cas d'échec
items = parse_products(html)
save_products(items)
return len(items)
L'exécution périodique se configure via django-celery-beat directement dans l'interface d'administration — par exemple, mettre à jour le catalogue toutes les 6 heures. Le retry de Celery apporte une résilience aux pannes réseau temporaires sans traitement manuel.
6. Encodages et Unicode dans Django
Le problème est le même que dans tout scraper Python : un encodage de réponse incorrect. Le remède aussi — resp.apparent_encoding ou le travail avec resp.content (voir en détail « Web scraping avec Python », section sur le cyrillique).
Spécificité Django : assurez-vous que la base de données utilise UTF-8 (pour PostgreSQL — l'encodage UTF8, pour MySQL — utf8mb4), sinon le cyrillique se cassera dès l'enregistrement, et non au scraping. C'est la valeur par défaut dans la configuration du projet, mais avec une base de données ancienne, mieux vaut vérifier.
7. Multithreading et tâches en arrière-plan
Dans Django, on utilise rarement des threads « bruts » — pour le parallélisme, il y a Celery avec son pool de workers. En lançant plusieurs workers, vous obtenez un traitement parallèle de la file d'attente sans gestion manuelle des threads :
celery -A myproject worker --concurrency=8 --loglevel=info
S'il faut parcourir rapidement une liste d'URL au sein d'une même tâche, ThreadPoolExecutor convient (voir la section sur le multithreading dans le hub). Pour un parallélisme très élevé à l'intérieur d'une tâche, on peut recourir à l'async — mais dans Django cela exige de la prudence avec l'ORM (sync_to_async). Le sujet est traité dans « Scraping asynchrone en Python ».
8. Proxys
Les proxys se branchent au niveau de la couche de services — Django n'y est pour rien. Il est pratique de stocker la liste des proxys dans un modèle et de marquer ceux qui sont « sains » :
class Proxy(models.Model):
address = models.CharField(max_length=200) # http://user:pass@ip:port
is_active = models.BooleanField(default=True)
fail_count = models.IntegerField(default=0)
Le service prend alors un proxy actif au hasard, incrémente fail_count en cas d'erreur et le désactive lorsque le seuil est dépassé. Les techniques générales de rotation sont dans le hub, section « Proxys ».
9. Scraping via TOR
TOR se connecte comme un proxy SOCKS5 (socks5h://127.0.0.1:9050) dans le même fetch_page. Django n'a aucune spécificité ici — voir la section sur TOR dans le hub. Sur un serveur hébergeant Django, le démon TOR tourne comme un processus séparé (service systemd).
10. HTTPS/SSL
La vérification des certificats s'effectue au niveau de requests. En production, conservez verify=True. Si les certificats racine du serveur sont obsolètes, mettez à jour certifi dans l'environnement virtuel du projet. Détails dans le hub.
11. Gestion des cookies
Pour le scraping avec authentification, utilisez requests.Session à l'intérieur de la tâche. S'il faut réutiliser la session entre plusieurs tâches Celery, sauvegardez le cookie-jar (par exemple sérialisé dans Redis ou dans un modèle) et restaurez-le avant la requête. Les techniques de base sont dans le hub, section « Cookies ».
12. Code de statut et en-têtes de réponse
Journalisez le code de réponse et décidez s'il faut réessayer la tâche. Dans Celery, cela s'appuie élégamment sur le mécanisme de retry :
@shared_task(bind=True, max_retries=5)
def fetch_task(self, url):
resp = requests.get(url, timeout=15)
if resp.status_code == 429:
delay = int(resp.headers.get("Retry-After", 60))
raise self.retry(countdown=delay)
if resp.status_code in (403, 503):
raise self.retry(countdown=120) # probablement un blocage, on attend plus longtemps
resp.raise_for_status()
return resp.text
Ainsi, les blocages anti-bot (429/403/503) se transforment en nouvelles tentatives maîtrisées avec temporisation, et non en perte de données.
13. Stockage des URL et des files d'attente via l'ORM
Dans Django, le frontier se met commodément en œuvre par une table avec statuts — cela survit aux redémarrages et reste visible dans l'interface d'administration.
class CrawlURL(models.Model):
STATUS = [("new", "new"), ("in_progress", "in_progress"),
("done", "done"), ("failed", "failed")]
url = models.URLField(unique=True) # unique = déduplication automatique
status = models.CharField(max_length=20, choices=STATUS, default="new")
attempts = models.IntegerField(default=0)
created_at = models.DateTimeField(auto_now_add=True)
Le worker prend les URL « new », les passe en in_progress, les traite puis les bascule en done/failed. L'index unique sur url élimine les doublons. Pour les projets à forte charge, la vraie file d'attente est à conserver dans Redis/le broker Celery, la base de données ne stockant que les résultats et l'historique. Un panorama des approches de files d'attente est dans le hub, section « Files d'attente ».
14. Avantages et inconvénients du scraping avec Django
Avantages :
- L'ORM prend en charge le stockage, la déduplication et les migrations.
- Interface d'administration prête à l'emploi — gestion des sources et consultation des données.
- Celery + beat — des tâches en arrière-plan et périodiques fiables, avec retry.
- Base de code unifiée : le scraper et la vitrine web dans un même projet.
Inconvénients :
- Surdimensionné pour des scripts ponctuels — un simple
.pysuffit. - Exige de l'infrastructure : un broker (Redis/RabbitMQ), des workers Celery.
- L'async dans Django reste sous conditions (l'ORM est essentiellement synchrone).
- Pour parcourir des millions de pages, le spécialisé Scrapy est plus efficace.