Le scraping par langage 12 min de lecture

Web scraping avec Python : le guide complet

Toutes les étapes du web scraping en Python : requests et BeautifulSoup, contenu dynamique, contournement des blocages et organisation d'un scraper prêt pour la production.

ÉW
Équipe Web-Scraping.fr
Collecte de données pour votre activité
Publié le: 1 janvier 2025

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

  1. Qu'est-ce que le scraping et de quoi se compose-t-il
  2. Panorama des bibliothèques et solutions
  3. Comment récupérer la page
  4. Bibliothèques pour analyser le contenu
  5. Résoudre les problèmes d'encodage du cyrillique
  6. Utiliser le multithreading
  7. Utiliser des proxys
  8. Scraper via TOR
  9. Gérer HTTPS/SSL
  10. Gérer les cookies
  11. Statut de réponse et en-têtes
  12. Stockage des URL et des files d'attente
  13. Avantages et inconvénients de l'implémentation
  14. 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 :

  1. Récupération de la page — requête HTTP vers le serveur et réception de la réponse (HTML, XML, JSON).
  2. Analyse du contenu — extraction des données utiles du balisage via des sélecteurs (CSS, XPath) ou des expressions régulières.
  3. Normalisation et stockage — mise des données dans un format unifié et écriture dans un fichier ou une base de données.
  4. 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 :

python
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-requests par 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 :

python
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

python
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

python
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 :

python
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

python
response = requests.get(url)
response.encoding = "utf-8"      # ou "windows-1251" pour les vieux sites
html = response.text

Solution 2 : détection automatique

python
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> :

python
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

python
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

python
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

python
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

python
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 :

python
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 :

python
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)

python
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

python
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 :

bash
pip install --upgrade certifi

Les cookies servent aux sessions, à l'authentification et au passage des pages « de protection » qui posent un jeton puis redirigent.

python
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.

python
cookies = {"sessionid": "abc123", "csrftoken": "xyz789"}
response = requests.get(url, cookies=cookies)
python
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 ».

python
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

python
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

python
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.