Le web scraping — ou extraction de données web — consiste à collecter automatiquement des données sur des pages web et à les transformer en un format structuré : tableaux, JSON, base de données. Python est devenu le standard de facto pour cette tâche : syntaxe simple, immense écosystème de bibliothèques et communauté très active.
Cet article est un « hub » de synthèse. Nous y parcourons tout le processus, de la requête de la page au stockage des files d'URL, avec une carte de toutes les bibliothèques et solutions clés, et des renvois vers des articles détaillés pour les sujets pointus.
Sommaire
- Qu'est-ce que le scraping et de quoi se compose-t-il
- Panorama des bibliothèques et solutions
- Comment récupérer la page
- Bibliothèques pour analyser le contenu
- Résoudre les problèmes d'encodage du cyrillique
- Utiliser le multithreading
- Utiliser des proxys
- Scraper via TOR
- Gérer HTTPS/SSL
- Gérer les cookies
- Statut de réponse et en-têtes
- Stockage des URL et des files d'attente
- Avantages et inconvénients de l'implémentation
- Aspects juridiques et éthiques
1. Qu'est-ce que le scraping et de quoi se compose-t-il
Tout scraper, quelle que soit son échelle, se compose de quatre étapes :
- Récupération de la page — requête HTTP vers le serveur et réception de la réponse (HTML, XML, JSON).
- Analyse du contenu — extraction des données utiles du balisage via des sélecteurs (CSS, XPath) ou des expressions régulières.
- Normalisation et stockage — mise des données dans un format unifié et écriture dans un fichier ou une base de données.
- Gestion du crawl — file d'URL, déduplication, limitation de débit, nouvelles tentatives.
Un script simple peut tenir tout cela en 10 lignes. Un crawler industriel isole chaque étape dans une couche dédiée avec files d'attente, proxys et workers distribués.
2. Panorama des bibliothèques et solutions
Pour y voir clair, classons les outils selon leur rôle.
Chargement des pages (clients HTTP)
| Bibliothèque | Type | Quand l'utiliser |
|---|---|---|
| requests | synchrone | le standard pour la plupart des tâches, API pratique |
| urllib | synchrone | inclus dans la bibliothèque standard, sans dépendances |
| httpx | sync/async | remplaçant moderne de requests avec async et HTTP/2 |
| aiohttp | asynchrone | forte parallélisation, milliers de requêtes |
| pycurl | synchrone | contrôle fin de la requête, vitesse maximale |
Analyse HTML/XML
| Bibliothèque | Moteur | Particularités |
|---|---|---|
| BeautifulSoup (bs4) | html.parser / lxml | l'API la plus accessible, tolère le HTML « sale » |
| lxml | libxml2 (C) | vitesse maximale, XPath complet |
| parsel | lxml | CSS + XPath, cœur de Scrapy |
| selectolax | Modest/Lexbor (C) | parseur CSS très rapide pour les gros volumes |
| pyquery | lxml | syntaxe à la jQuery |
Sites dynamiques (JavaScript)
| Outil | Usage |
|---|---|
| Selenium | pilotage d'un vrai navigateur, le grand classique |
| Playwright | alternative moderne, plus rapide et plus stable |
| Pyppeteer | portage de Puppeteer en Python |
Frameworks et plateformes
| Solution | Usage |
|---|---|
| Scrapy | framework complet pour crawlers : files, pipelines, middlewares |
| Scrapy + Splash/Playwright | Scrapy avec rendu JS |
| Django + Celery | scraping intégré à une application web avec tâches en arrière-plan |
Comment choisir
- Page simple sans JS, tâche ponctuelle → requests + BeautifulSoup.
- Besoin de vitesse sur de gros volumes → httpx/aiohttp + lxml/selectolax.
- Des centaines de milliers de pages, crawl d'un site entier → Scrapy. Détails dans l'article « Scraper des sites avec Scrapy ».
- Contenu rendu via JavaScript → Playwright/Selenium.
- Scraping intégré à un service web → Django, voir « Web scraping avec Django ».
- Besoin d'une parallélisation maximale → approche asynchrone.
3. Comment récupérer la page
Requête de base avec requests :
import requests
url = "https://example.com"
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36",
"Accept-Language": "fr-FR,fr;q=0.9",
}
response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status() # lève une exception en cas de 4xx/5xx
html = response.text
Points clés :
- User-Agent : définissez-le systématiquement — de nombreux sites bloquent les requêtes avec le
python-requestspar défaut. - timeout : indiquez-le toujours, sinon le script peut rester bloqué indéfiniment.
- raise_for_status() vous évite de vérifier le code de réponse à la main.
Si la page est rendue via JavaScript, requests renverra un squelette vide. Il faut alors un moteur de navigateur :
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto("https://example.com")
page.wait_for_selector(".content") # on attend l'apparition des données
html = page.content()
browser.close()
Si vous attendez en sortie non pas du HTML mais des données structurées d'une API, consultez l'article dédié « Parser du JSON en Python ».
4. Bibliothèques pour analyser le contenu
BeautifulSoup — le point d'entrée des débutants
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, "lxml") # le parseur lxml est plus rapide que html.parser
title = soup.find("h1").get_text(strip=True)
links = [a["href"] for a in soup.select("a.product-link")]
price = soup.select_one(".price").text
BeautifulSoup tolère le balisage cassé et se lit presque comme du langage naturel. Pour l'extraction de tableaux, voir l'analyse dédiée : « Extraire des tableaux d'un site avec BeautifulSoup ».
lxml — vitesse et XPath
from lxml import html as lxml_html
tree = lxml_html.fromstring(html)
titles = tree.xpath('//h2[@class="title"]/text()')
prices = tree.xpath('//span[@class="price"]/text()')
lxml est écrit en C et fonctionne bien plus vite sur de gros volumes. Le XPath complet offre une souplesse inaccessible aux sélecteurs CSS. Analyse approfondie dans l'article « Web scraping en Python avec lxml ».
parsel et selectolax
parsel (le cœur de Scrapy) combine CSS et XPath :
from parsel import Selector
sel = Selector(text=html)
sel.css("h1::text").get()
sel.xpath("//a/@href").getall()
selectolax est à privilégier quand il faut analyser des centaines de milliers de documents — il est nettement plus rapide que lxml lui-même sur les sélections CSS.
Expressions régulières
re ne convient que pour des motifs simples et plats (téléphone, e-mail, référence produit). N'analysez pas du HTML imbriqué avec des regex — la structure casse trop facilement.
5. Encodages et Unicode : résoudre les problèmes
Le souci le plus fréquent sur les sites en cyrillique : des caractères illisibles à la place du texte russe (пÑÐ¸Ð²ÐµÑ au lieu du mot attendu). La cause : un encodage de réponse mal détecté.
Pourquoi cela arrive
requests devine l'encodage d'après l'en-tête Content-Type. Si le serveur ne l'envoie pas ou l'envoie de façon erronée, le texte est décodé dans le mauvais encodage (souvent ISO-8859-1 au lieu de windows-1251 ou utf-8).
Solution 1 : définir l'encodage manuellement
response = requests.get(url)
response.encoding = "utf-8" # ou "windows-1251" pour les vieux sites
html = response.text
Solution 2 : détection automatique
response = requests.get(url)
response.encoding = response.apparent_encoding # détecté d'après le contenu
html = response.text
apparent_encoding s'appuie sur la bibliothèque charset-normalizer (ou chardet), qui analyse les octets et déduit l'encodage.
Solution 3 : travailler directement avec les octets
La méthode la plus fiable : passer les octets au parseur et le laisser lire le <meta charset> :
from bs4 import BeautifulSoup
response = requests.get(url)
soup = BeautifulSoup(response.content, "lxml") # .content, et non .text
response.content contient les octets « bruts » ; lxml et BeautifulSoup trouveront eux-mêmes la déclaration d'encodage dans le HTML.
Solution 4 : décodage manuel
html = response.content.decode("windows-1251", errors="replace")
Le paramètre errors="replace" remplace les caractères non convertibles par � sans faire planter le script. Pour le XML en cyrillique, les subtilités sont décrites dans « Parser du XML en Python ».
6. Utiliser le multithreading
Le scraping consiste surtout à attendre le réseau (I/O-bound), donc les threads apportent une accélération sensible malgré le GIL : pendant qu'un thread attend une réponse, un autre travaille.
ThreadPoolExecutor — la méthode la plus simple
from concurrent.futures import ThreadPoolExecutor
import requests
urls = [f"https://example.com/page/{i}" for i in range(1, 101)]
def fetch(url):
r = requests.get(url, timeout=10)
return url, r.status_code
with ThreadPoolExecutor(max_workers=10) as executor:
for url, status in executor.map(fetch, urls):
print(url, status)
Quand passer à multiprocessing
Si le goulot d'étranglement n'est pas le réseau mais l'analyse HTML lourde et le traitement (CPU-bound), les threads butent sur le GIL. C'est là qu'intervient multiprocessing — plusieurs processus avec leur propre interpréteur.
La meilleure alternative — async
Pour des milliers de requêtes simultanées, les threads consomment trop de mémoire. L'asynchrone (asyncio + aiohttp) maintient des dizaines de milliers de connexions dans un seul thread. C'est un vaste sujet à part — voir « Web scraping asynchrone en Python ».
Important : une forte parallélisation ne vous donne pas le droit de « faire tomber » le serveur d'autrui. Limitez le débit et respectez le
robots.txt.
7. Utiliser des proxys
En cas de scraping intensif, le site bannit votre IP en fonction du nombre de requêtes. La solution : un pool de proxys avec rotation.
Connexion simple d'un proxy
proxies = {
"http": "http://user:pass@123.45.67.89:8080",
"https": "http://user:pass@123.45.67.89:8080",
}
response = requests.get(url, proxies=proxies, timeout=15)
Rotation des proxys
import random
import requests
PROXIES = [
"http://user:pass@ip1:port",
"http://user:pass@ip2:port",
"http://user:pass@ip3:port",
]
def fetch_with_rotation(url):
proxy = random.choice(PROXIES)
return requests.get(url, proxies={"http": proxy, "https": proxy}, timeout=15)
Types de proxys
- Datacenter — bon marché et rapides, mais facilement détectés et bannis.
- Residential — IP de vrais fournisseurs d'accès, plus chers mais moins repérables.
- Mobile — IP d'opérateurs mobiles, les plus « fiables » aux yeux des sites, les plus chers.
En production, il est judicieux de maintenir une liste de proxys « sains » : écarter ceux qui renvoient un timeout ou un 403 et les revérifier périodiquement.
8. Scraper via TOR
TOR est un moyen gratuit de changer d'IP de sortie. C'est plus lent que des proxys payants et adapté à des volumes modestes, mais sans aucun coût.
Connexion
Une fois TOR installé (démon ou Tor Browser), il expose un proxy SOCKS5 sur 127.0.0.1:9050 :
import requests
proxies = {
"http": "socks5h://127.0.0.1:9050",
"https": "socks5h://127.0.0.1:9050",
}
# paquet requis : pip install requests[socks]
r = requests.get("https://httpbin.org/ip", proxies=proxies)
print(r.json()) # vous verrez l'IP du nœud de sortie TOR, pas la vôtre
Le schéma socks5h (avec la lettre h) est important : la résolution DNS passe par TOR et non en local — sinon votre vraie requête DNS fuite.
Changer d'identité (nouvelle IP)
Pour obtenir un nouveau nœud de sortie, envoyez le signal NEWNYM via le port de contrôle (9051) avec la bibliothèque stem :
from stem import Signal
from stem.control import Controller
def renew_tor_ip():
with Controller.from_port(port=9051) as controller:
controller.authenticate(password="votre_mot_de_passe")
controller.signal(Signal.NEWNYM)
Le port de contrôle doit être activé dans torrc avec un hash de mot de passe (tor --hash-password).
Attention : beaucoup de grands sites connaissent la liste des nœuds de sortie TOR et les bloquent ou affichent un captcha. TOR convient aux tâches peu sensibles à la vitesse et aux faibles volumes.
9. Gérer HTTPS/SSL
Par défaut, requests vérifie les certificats SSL via le paquet certifi. Le plus souvent, il n'y a rien à configurer. Les problèmes surviennent sur les sites aux certificats auto-signés ou expirés.
Désactiver la vérification (débogage uniquement)
import requests
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
response = requests.get(url, verify=False) # PAS pour la production
Désactiver la vérification est dangereux — cela ouvre la porte aux attaques MITM. C'est acceptable uniquement en local, pour déboguer.
La bonne approche — indiquer votre propre CA
response = requests.get(url, verify="/path/to/custom-ca-bundle.crt")
Mettre à jour les certificats racines
Si vous obtenez SSLCertVerificationError sur des sites tout à fait normaux, mettez à jour certifi :
pip install --upgrade certifi
10. Gérer les cookies
Les cookies servent aux sessions, à l'authentification et au passage des pages « de protection » qui posent un jeton puis redirigent.
Session stocke les cookies automatiquement
import requests
session = requests.Session()
# connexion — le serveur renverra le cookie de session
session.post("https://example.com/login", data={"user": "u", "pass": "p"})
# les requêtes suivantes sont déjà authentifiées
profile = session.get("https://example.com/profile")
Session conserve et réinjecte les cookies entre les requêtes, et réutilise au passage les connexions TCP (plus rapide) ainsi que les en-têtes communs.
Passer des cookies manuellement
cookies = {"sessionid": "abc123", "csrftoken": "xyz789"}
response = requests.get(url, cookies=cookies)
Lire les cookies reçus
response = requests.get(url)
for name, value in response.cookies.items():
print(name, value)
11. Statut de réponse et en-têtes
Contrôler la réponse du serveur est indispensable — sinon vous analyserez une page d'erreur comme si c'étaient des « données ».
response = requests.get(url)
print(response.status_code) # 200, 404, 403, 500 ...
print(response.reason) # 'OK', 'Not Found'
print(response.headers["Content-Type"])
print(response.headers.get("Server"))
print(response.url) # URL finale après les redirections
print(response.elapsed) # temps de réponse
Bien gérer les statuts
if response.status_code == 200:
parse(response.text)
elif response.status_code == 404:
log("Page introuvable")
elif response.status_code == 429:
# Too Many Requests — on nous ralentit
wait = int(response.headers.get("Retry-After", 60))
time.sleep(wait)
elif response.status_code in (403, 503):
rotate_proxy() # sans doute un ban — on change d'IP
L'en-tête Retry-After indique combien de temps attendre avant de réessayer. Les statuts 403/503 signalent souvent une protection anti-bot : changement de proxy, de User-Agent et une pause aident.
12. Stockage des URL et des files d'attente (aperçu)
Quand un scraper parcourt un site entier, il faut stocker quelque part les URL « pas encore visitées » et « déjà visitées ». C'est ce qu'on appelle le frontier (frontière de crawl).
La variante la plus simple — des ensembles en mémoire
from collections import deque
to_visit = deque(["https://example.com"])
visited = set()
while to_visit:
url = to_visit.popleft()
if url in visited:
continue
visited.add(url)
# ... télécharger, analyser, ajouter les nouveaux liens à to_visit
set assure une déduplication instantanée, deque fonctionne comme une file FIFO.
Quand les données sont volumineuses
- Redis — file partagée entre plusieurs workers, survit aux redémarrages. Les listes et ensembles Redis sont parfaits pour un crawl distribué.
- Base de données (PostgreSQL/SQLite) — table d'URL avec statuts
new / in_progress / done / failed, pratique pour la résilience et l'analytique. - Files de tâches (Celery + broker, RabbitMQ) — quand le scraping est intégré à une application ; voir « Web scraping avec Django ».
- Filtre de Bloom — économise la mémoire avec des millions d'URL : test probabiliste « avons-nous déjà vu cette URL ».
Dans Scrapy, la gestion des files, de la déduplication et des priorités est intégrée d'origine — l'une des principales raisons de choisir ce framework sur les grands projets. Détails dans « Scraper des sites avec Scrapy ».
13. Avantages et inconvénients d'une implémentation en Python
Avantages :
- Faible barrière à l'entrée, code lisible, prototype rapide.
- Écosystème extrêmement riche : de requests à Scrapy et Playwright.
- Immense communauté — presque tous les problèmes ont déjà une solution.
- Intégration facile avec l'analyse de données (pandas, numpy) et les bases de données.
Inconvénients :
- Le GIL limite les traitements CPU-bound (contournable via async et multiprocessing).
- Le Python pur est plus lent que les langages compilés sur l'analyse lourde (lxml/selectolax en C sauvent la mise).
- Les sites JS dynamiques exigent des moteurs de navigateur lourds.
- Fragilité : quand la mise en page du site change, les sélecteurs cassent — une maintenance est nécessaire.
14. Aspects juridiques et éthiques
Le web scraping est un outil puissant, à utiliser de manière responsable :
- Respectez le robots.txt et les conditions d'utilisation du site.
- Ne créez pas de charge excessive : ajoutez des délais, limitez la parallélisation.
- Ne collectez pas de données personnelles sans base légale (pensez au RGPD).
- Indiquez un User-Agent honnête quand c'est pertinent et mettez en cache pour ne pas solliciter le serveur inutilement.