Aller au contenu
Retour au blog

Le web scraping avec Python en 2026 : le guide pratique du développeur

Raluca PenciucDernière mise à jour le 35 min read
Le web scraping avec Python en 2026 : le guide pratique du développeur
En bref : voici un guide pratique sur le web scraping en Python qui commence par un arbre de décision, puis aborde successivement Requests et Beautiful Soup, XPath avec lxml, Playwright, Selenium, Scrapy, ainsi qu'une API de scraping pour les cibles difficiles. À la fin, vous disposerez d’un code fonctionnel, d’un guide pour contourner les blocages, de modèles de stockage compatibles avec les grands modèles de langage (LLM) et d’une liste de contrôle pour la mise en production.

Si vous tapez python web scraping dans une barre de recherche, vous obtiendrez une centaine de tutoriels qui installent tous les trois mêmes bibliothèques et s’arrêtent avant d’aborder les parties intéressantes. Ce guide est différent. Le scraping Web avec Python consiste à écrire un script qui récupère une page Web, analyse son code HTML et extrait des champs structurés que vous pouvez importer dans une feuille de calcul, une base de données ou un pipeline de modèles. Le principe est simple. La raison pour laquelle les gens rencontrent des difficultés est que le choix de l’outil approprié dépend du site cible, et qu’un mauvais choix fait perdre des heures.

Ce tutoriel est subjectif. Il commence par un petit arbre de décision pour vous éviter d’installer Playwright pour une page que Requests aurait pu analyser en cinquante lignes. Il passe ensuite en revue chaque couche d’une pile de scraping réelle : HTML statique, rendu JavaScript, sélecteurs CSS par rapport à XPath, nettoyage des champs, mise à l’échelle avec Scrapy, rotation des proxys, gestion des erreurs HTTP, stockage des résultats au format CSV, SQLite ou JSONL pour les LLM en aval, et planification d’exécutions récurrentes.

Vous verrez le code de différentes bibliothèques côte à côte pour une même cible, ce qui vous permettra d’identifier clairement les compromis plutôt que de les ignorer. Vous trouverez également des remarques franches sur les blocages, les coûts de maintenance et les cas où une API hébergée représente un meilleur investissement de votre temps qu’une autre time.sleep(random.uniform(1, 3)) . Si vous avez déjà écrit un scraper Python qui a échoué en production, ce guide vous expliquera pourquoi et comment éviter que cela ne se reproduise.

Pourquoi le web scraping en Python est le choix par défaut en 2026

Le web scraping en Python domine le secteur pour trois raisons : une syntaxe lisible, un écosystème mature et une intégration sans heurts avec le reste de la pile de données. Une douzaine de lignes de Python suffisent pour télécharger une page, l’analyser et vous fournir une liste de dictionnaires prêts à être utilisés avec pandas. Ce cheminement rapide de « l’URL » au « dataframe » explique pourquoi les ingénieurs de données, les analystes et les équipes de ML continuent de le privilégier plutôt que Go ou Node.

L’ensemble des bibliothèques est exceptionnellement complet. Requests et httpx gèrent la couche HTTP. Beautiful Soup et lxml analysent le HTML à l’aide de sélecteurs CSS ou d’XPath. Playwright et Selenium pilotent de véritables navigateurs pour les pages rendues en JavaScript. Scrapy fournit un framework complet de crawling avec concurrence, tentatives de récupération, pipelines d’éléments et formats d’exportation prêts à l’emploi. Chacun d’entre eux a fait ses preuves en production et fait l’objet d’une maintenance active.

La deuxième partie de l’histoire concerne les possibilités d’intégration. Les données extraites sont directement transférées vers pandas, DuckDB, SQLite, Parquet ou JSONL pour l’ingestion dans des LLM. Les notebooks accélèrent les itérations. Les indications de type et les classes de données garantissent l’intégrité des enregistrements. Il n’y a par ailleurs aucun écart significatif entre le prototype et la production, car le même script qui fonctionne dans Jupyter s’exécute sous cron ou dans un conteneur sans nécessiter de réécriture. C’est cette continuité de bout en bout qui fait de Python le choix pragmatique par défaut pour le scraping web en 2026, et c’est pourquoi le reste de ce guide s’appuie exclusivement sur ce langage.

Choisissez le bon outil : un arbre de décision pour votre site cible

Avant toute installation, répondez à quatre questions concernant la cible. Chacune d’entre elles réduit considérablement le choix de bibliothèques, et suivre cet arbre de décision vous fera gagner plus de temps que n’importe quelle astuce d’optimisation.

Étape 1 : Le site dispose-t-il d’une API officielle ? Si oui, utilisez-la. Une API est plus rapide, moins coûteuse et juridiquement plus sûre que le scraping. Réservez le scraping aux pages publiques mais non lisibles par machine.

Étape 2 : Les données sont-elles présentes dans le code HTML brut que vous obtenez à partir de curl ou requests.get? Ouvrez la page dans un navigateur, affichez le code source (et non « inspecter l’élément »), puis recherchez l’une des valeurs qui vous intéressent. Si elle s’y trouve, vous avez affaire à une page statique. Requests associé à Beautiful Soup suffit. N’utilisez pas de navigateur sans interface utilisateur.

Étape 3 : Le contenu est-il injecté par JavaScript après le chargement initial du code HTML ? Dans ce cas, vous avez besoin d’un véritable contexte de navigateur. Playwright est la solution moderne par défaut. Selenium convient si votre équipe l’utilise déjà en intégration continue (CI). Les deux permettent d’attendre des sélecteurs, de cliquer, de faire défiler et d’extraire des données du DOM rendu.

Étape 4 : Explorez-vous des milliers d’URL, enchaînez-vous des requêtes ou exécutez-vous des tâches de longue durée ? Passez à Scrapy. Sa boucle d’événements, ses pipelines d’éléments, ses contrôles de concurrence et ses tentatives de réessai intégrées surpassent n’importe quelle boucle développée en interne.

Il existe une cinquième branche pour les cibles difficiles. Si le site identifie agressivement les navigateurs, bloque les adresses IP des centres de données ou affiche des CAPTCHA dès la première requête, conservez votre code d’analyse mais confiez la couche de récupération à une API de scraping. Il s’agit d’une solution de secours, pas d’une solution par défaut.

Utilisez ce guide de référence rapide :

Signal sur le site cible

Meilleur outil

API publique disponible

L'API

Données dans le code HTML initial

Requêtes + Beautiful Soup

Contenu rendu en JavaScript

Playwright (ou Selenium)

Des milliers de pages, des tentatives de récupération, des pipelines

Scrapy

CAPTCHA, blocage d'adresses IP, empreintes TLS

API de scraping

La plupart des projets de scraping web en Python devraient commencer par l’outil le plus léger permettant de récupérer les données, et ne passer à un niveau supérieur que lorsque la couche actuelle atteint ses limites. Cela reflète les recommandations de la communauté : la meilleure configuration est la plus simple qui permet d’obtenir les données de manière fiable.

Configuration de l’environnement : Python 3, Virtualenv et bibliothèques requises

Utilisez Python 3.10 ou une version plus récente. Créez un virtualenv isolé pour chaque projet afin que les dépendances ne se répercutent pas sur votre installation Python système :

python3 -m venv .venv
source .venv/bin/activate        # Windows: .venv\Scripts\activate

pip install --upgrade pip
pip install requests beautifulsoup4 lxml pandas
pip install playwright
pip install scrapy
pip install selenium webdriver-manager

# Playwright ships as a Python package plus browser binaries.
# Install the Chromium binary once:
python -m playwright install chromium

lxml fait office à la fois de parseur Beautiful Soup le plus rapide et de moteur XPath que nous utiliserons plus tard. pandas est facultatif mais utile pour le nettoyage et l’exportation. webdriver-manager gère le téléchargement de ChromeDriver par Selenium, ce qui vous évite d’avoir à faire correspondre manuellement les versions de navigateur sur chaque exécuteur CI.

Quelques remarques pratiques avant de commencer :

  • Si vous utilisez Apple Silicon, utilisez la version ARM64 de Python. Le binaire Chromium de Playwright est bien plus rapide sur ARM natif que sous Rosetta.
  • Sous Windows, activez l’environnement venv avec .venv\Scripts\activate et privilégiez PowerShell plutôt que cmd pour obtenir des traces de pile lisibles.
  • Figez les versions une fois que votre scraper fonctionne : pip freeze > requirements.txt. Les scrapers sont par nature fragiles, et verrouiller un ensemble de dépendances dont le bon fonctionnement est avéré est le moyen le plus simple d’éviter que la tâche cron de demain ne tombe en panne à cause d’une mise à jour silencieuse d’une bibliothèque.
  • Si vous prévoyez de conteneuriser, ajoutez l'étape playwright install étape dans votre Dockerfile afin que les binaires du navigateur soient intégrés à l’image plutôt que d’être téléchargés à chaque démarrage à froid.

Inspectez la page cible avant d’écrire la moindre ligne de code

Chaque heure passée dans DevTools vous fait gagner dix heures de débogage. Ouvrez la page que vous souhaitez scraper dans Chrome ou Firefox, puis parcourez les trois panneaux avant d’ouvrir votre éditeur.

Le panneau « Éléments ». Cliquez avec le bouton droit sur une valeur qui vous intéresse et choisissez « Inspecter ». Notez la balise, la classe et tout attribut stable tel que data-testid ou itemprop. Les identifiants et les classes qui semblent générés automatiquement (css-1x9k2j) sont instables et rendront vos sélecteurs inopérants lors du prochain déploiement. Privilégiez les attributs sémantiques lorsqu’ils existent.

L'onglet « Réseau ». Rechargez la page avec l'onglet « Réseau » ouvert et filtrez par XHR ou Fetch. De nombreux sites modernes affichent des listes en appelant un point de terminaison JSON interne. Si vous en repérez un qui renvoie les champs dont vous avez besoin, il est plus rapide, plus fiable et moins sujet aux erreurs de s’adresser directement à ce point de terminaison avec Requests plutôt que d’analyser le code HTML. C’est l’astuce la plus efficace que la plupart des tutoriels de scraping omettent de mentionner.

Copier en tant que cURL. Cliquez avec le bouton droit sur n’importe quelle requête dans l’onglet Réseau et choisissez « Copier en tant que cURL ». Collez-la dans votre terminal pour vérifier qu’elle fonctionne, puis traduisez-la en Python. L’outil curlconverter.com le fera automatiquement, mais lire les en-têtes à la main vous apprend ce que le serveur attend réellement : un User-Agent, un Referer, un cookie de session ou un jeton CSRF.

Si le code HTML initial contient vos données, vous suivez la voie statique. Si vos données n’apparaissent qu’après le chargement de la page et l’exécution de JavaScript, vous suivez la voie du navigateur. S’il existe une API JSON cachée, privilégiez-la systématiquement. La suite de ce guide part du principe que vous avez effectué cette vérification avant d’écrire du code.

Créer un scraper statique avec Requests et Beautiful Soup

Pour une page statique, trois étapes suffisent : récupérer, analyser, extraire. Nous utiliserons books.toscrape.com, une cible de démonstration publique spécialement conçue pour que les tutoriels ne surchargent pas de vrais sites.

import requests
from bs4 import BeautifulSoup

URL = "https://books.toscrape.com/catalogue/page-1.html"
HEADERS = {"User-Agent": "Mozilla/5.0 (compatible; MyScraper/1.0)"}

resp = requests.get(URL, headers=HEADERS, timeout=15)
resp.raise_for_status()          # raises on 4xx / 5xx

Un resp.status_code « 200 signifie que la récupération a réussi. Tout autre résultat est un signal, pas une note de bas de page. raise_for_status() transforme cela en une exception que vous pouvez intercepter. Définissez une valeur explicite timeout à chaque requête. La valeur par défaut est « attendre indéfiniment », ce qui est exactement ce que vous ne voulez pas dans une tâche planifiée.

Analysez le code HTML avec lxml, qui est nettement plus rapide que la fonction intégrée de Python html.parser:

soup = BeautifulSoup(resp.text, "lxml")

Now select de Python. Beautiful Soup vous propose trois façons d’interroger l’arborescence. Les deux que vous utiliserez quotidiennement sont find_all les recherches basées sur les balises et select pour les sélecteurs CSS. Préférez select lorsque la cible possède des classes stables :

books = []
for card in soup.select("article.product_pod"):
    title = card.select_one("h3 a")["title"]
    price = card.select_one("p.price_color").get_text(strip=True)
    stock = card.select_one("p.instock").get_text(strip=True)
    books.append({"title": title, "price": price, "stock": stock})

print(books[:3])

Quelques bonnes habitudes vous faciliteront la vie par la suite :

  • Protégez chaque sélecteur. select_one renvoie None si aucune correspondance n’est trouvée, ce qui .get_text() le programme plantera lors de l'itération suivante. Utilisez if card.select_one(...) ou de l’opérateur « walrus » pour ignorer les lignes manquantes.
  • Extrayez le texte avec strip=True. Le code HTML regorge d’espaces blancs et d’espaces insécables. Il est plus économique de nettoyer les données au moment de l’extraction que de corriger chaque champ en aval par la suite.
  • Stockez les valeurs brutes, et non les valeurs « finales ». Enregistrez la chaîne de caractères du prix telle quelle "£51.77" et normalisez-la lors d’une étape distincte. Lorsque votre extraction échoue, vous souhaitez voir ce que le serveur a réellement renvoyé.

Pour suivre la pagination, recherchez le lien « suivant » et bouclez jusqu’à ce qu’il disparaisse :

def scrape_all_pages(start_url):
    url = start_url
    results = []
    while url:
        r = requests.get(url, headers=HEADERS, timeout=15)
        r.raise_for_status()
        s = BeautifulSoup(r.text, "lxml")
        for card in s.select("article.product_pod"):
            results.append({
                "title": card.select_one("h3 a")["title"],
                "price": card.select_one("p.price_color").get_text(strip=True),
            })
        next_link = s.select_one("li.next a")
        url = requests.compat.urljoin(url, next_link["href"]) if next_link else None
    return results

Voici un scraper web Python statique complet et fiable : récupération avec un en-tête valide, analyse avec lxml, sélectionnez avec du CSS, vérifiez les nœuds manquants, suivez le lien vers la page suivante et terminez proprement lorsque la pagination est épuisée. Pour une présentation plus approfondie de Beautiful Soup avec des tableaux, des formulaires et des éléments imbriqués, consultez notre tutoriel complémentaire sur Beautiful Soup.

Deux autres bonnes pratiques méritent d’être adoptées dès le premier jour. Tout d’abord, encapsulez la requête dans un try/except requests.RequestException et consignez l’URL qui a échoué. Les erreurs réseau sont inévitables à grande échelle, et un scraper qui plante dès la première ConnectionError ne terminera jamais un crawl nocturne. Deuxièmement, enregistrez vos résultats sur le disque toutes les quelques centaines d’enregistrements au lieu de tout conserver en mémoire :

import json

def checkpoint(records, path="checkpoint.jsonl"):
    with open(path, "a", encoding="utf-8") as f:
        for r in records:
            f.write(json.dumps(r, ensure_ascii=False) + "\n")

Ainsi, si le processus plante à la page 47 sur 100, vous disposez des 46 premières pages sur le disque et pouvez reprendre sans avoir à les récupérer à nouveau. Ces deux habitudes, la gestion des erreurs et la sauvegarde par points de contrôle, font la différence entre les scrapers de démonstration et ceux que vous pouvez réellement laisser tourner.

Sélecteurs CSS ou XPath : choisissez un langage de requête et restez-y fidèle

Beautiful Soup propose des sélecteurs CSS. lxml propose les deux, mais son principal atout réside dans XPath. Les deux résolvent le même problème avec des modes d’échec différents, et il est plus important d’en choisir un de manière délibérée que de choisir le « meilleur ».

Les sélecteurs CSS sont plus courts, plus familiers et se lisent comme du code front-end. Ils sont parfaits pour la sélection basée sur les classes : article.product_pod > h3 a. En revanche, ils peinent avec tout ce qui nécessite de remonter dans l’arborescence DOM, de faire correspondre du contenu textuel ou de naviguer par rapport à un élément frère.

XPath est plus expressif. Il gère la navigation par axe (ancestor::, following-sibling::), la correspondance de texte (//a[text()="Next"]) et la sélection positionnelle ((//tr)[3]). La même extraction avec lxml et XPath se présente ainsi :

from lxml import html

tree = html.fromstring(resp.text)
titles = tree.xpath("//article[contains(@class,'product_pod')]//h3/a/@title")
prices = tree.xpath("//article[contains(@class,'product_pod')]//p[@class='price_color']/text()")

La question de la maintenance prime sur celle de l’esthétique. Les sélecteurs qui s’appuient sur des noms de classes cessent de fonctionner dès que le concepteur du site renomme une classe. Les sélecteurs qui s’appuient sur la structure (div > div > span:nth-child(2)) cessent de fonctionner dès que quelqu’un ajoute un élément d’encapsulation. Les expressions XPath basées sur contains(@class, ...) ou sur des attributs stables tels que [@itemprop="price"] résistent aux deux cas de figure. En pratique, les équipes qui optent pour XPath ont tendance à écrire des scrapers plus résilients, au prix d’une courbe d’apprentissage légèrement plus raide.

Choisissez un seul langage de requête par projet et restez cohérent. Une base de code qui mélange soup.select(...) et tree.xpath(...) est une base de code où chaque nouvel ingénieur doit apprendre les deux. Notre guide XPath et notre comparaison entre XPath et les sélecteurs CSS abordent les modèles de navigation par axes qui justifient le passage à XPath lorsque le balisage évolue.

Une approche pragmatique par défaut pour les équipes de scraping web sous Python : commencez chaque nouveau spider avec Beautiful Soup et les sélecteurs CSS, car ils se lisent comme le front-end que vous scrapez. Optez pour XPath dès que vous rencontrez un cas où vous devez remonter l’arborescence, filtrer sur du contenu textuel ou naviguer par index. Cela couvre environ 90 % des tâches avec l’outil le plus simple et réserve l’outil le plus expressif aux situations où il est réellement rentable.

Nettoyez et normalisez les champs extraits

Les chaînes brutes extraites ne sont presque jamais utilisables. Les prix sont accompagnés de symboles monétaires, les dates se présentent sous une demi-douzaine de formats, les espaces sont omniprésents et il arrive parfois qu’un \u00a0 espace insécable se cachant au sein d’un texte par ailleurs propre. Séparez l’extraction de la normalisation afin que ces deux étapes restent débuggables.

import re
from datetime import datetime

def to_float_price(raw: str) -> float | None:
    if not raw:
        return None
    cleaned = re.sub(r"[^\d.,]", "", raw).replace(",", "")
    try:
        return float(cleaned)
    except ValueError:
        return None

def to_iso_date(raw: str, fmt: str) -> str | None:
    try:
        return datetime.strptime(raw.strip(), fmt).date().isoformat()
    except (ValueError, AttributeError):
        return None

def clean_text(raw: str) -> str:
    return " ".join(raw.replace("\xa0", " ").split()) if raw else ""

Appliquez ces fonctions d’aide en un seul passage sur vos enregistrements bruts :

def normalize(record):
    return {
        "title": clean_text(record["title"]),
        "price": to_float_price(record["price"]),
        "in_stock": "in stock" in clean_text(record["stock"]).lower(),
    }

normalized = [normalize(r) for r in raw_records]

Dédupliquez avant d’enregistrer. Une clé primaire stable basée sur l’URL ou l’identifiant du produit empêche votre cron quotidien de multiplier silencieusement les lignes. Si vous exportez vers pandas, df.drop_duplicates(subset=["url"]) cela se fait en une seule ligne.

Il convient de connaître deux pièges liés à l’encodage. Premièrement, requests devine l’encodage de la réponse à partir des en-têtes et peut se tromper ; définissez-le resp.encoding = "utf-8" explicitement si vous constatez des caractères illisibles. Deuxièmement, certains sites renvoient des caractères échappés sous forme d’entités HTML, comme ’ même dans les navigateurs modernes. Beautiful Soup les décode lors de l’analyse, mais si vous récupérez directement du JSON, utilisez html.unescape() avant de les stocker. C’est une normalisation cohérente qui distingue un scraper de démonstration d’un ensemble de données que vous pouvez réellement interroger.

Récupérer des pages rendues en JavaScript avec Playwright

Les outils statiques échouent dès qu’un site affiche son contenu dans le navigateur. Les applications modernes basées sur React, Vue et Svelte renvoient souvent une structure HTML presque vide, puis alimentent le DOM avec le JSON récupéré. Beautiful Soup verra cette structure, et non les données. Vous avez besoin d’un véritable contexte de navigateur, et Playwright est le moyen le plus simple d’en obtenir un sous Python.

Installez-le une seule fois (cela inclut également le binaire Chromium) :

pip install playwright
python -m playwright install chromium

Un scraper complet et exécutable pour une page rendue en JavaScript :

from playwright.sync_api import sync_playwright

def scrape_js_page(url: str):
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        context = browser.new_context(
            user_agent="Mozilla/5.0 (compatible; MyScraper/1.0)",
            viewport={"width": 1366, "height": 900},
        )
        page = context.new_page()
        page.goto(url, wait_until="domcontentloaded", timeout=30_000)

        # Wait for the element that only appears after JS runs.
        page.wait_for_selector("article.product_pod", timeout=15_000)

        cards = page.query_selector_all("article.product_pod")
        results = []
        for card in cards:
            title = card.query_selector("h3 a").get_attribute("title")
            price = card.query_selector("p.price_color").inner_text().strip()
            results.append({"title": title, "price": price})

        browser.close()
        return results

Deux détails déterminent si ce code est fiable ou instable.

La stratégie d’attente. wait_until="domcontentloaded" se déclenche lorsque le code HTML initial est analysé. "networkidle" attend que l'activité réseau s'arrête pendant 500 ms, ce qui est plus strict mais plus lent. Sur les applications monopages, privilégiez wait_for_selector sur un élément concret dont vous avez réellement besoin. C’est plus rapide que networkidle et moins fragile qu’un time.sleep.

Gestion du chargement dynamique. Pour le défilement infini, utilisez page.mouse.wheel(0, 2000) dans une boucle jusqu’à ce que le nombre d’enregistrements cesse de changer. Pour le contenu accessible par un clic, page.click("button.load-more") et resélectionnez. Pour les pages nécessitant une connexion, effectuez la connexion une seule fois avec context.storage_state(path="auth.json") et réutilisez le fichier d’état d’une exécution à l’autre afin de ne pas avoir à vous réauthentifier à chaque tâche.

Playwright prend également en charge l’asynchrone, l’interception de requêtes (bloquer les images et les polices pour accélérer le processus) et les cibles multi-navigateurs. Pour les explorations à haut volume, il est souvent associé à Scrapy via scrapy-playwright. Notre guide Playwright aborde ces modèles en détail.

Pour les pages à défilement infini, un modèle courant consiste à faire défiler la page jusqu’à ce que le nombre d’enregistrements cesse d’augmenter :

def scroll_until_stable(page, selector, max_rounds=20):
    prev = 0
    for _ in range(max_rounds):
        page.mouse.wheel(0, 4000)
        page.wait_for_timeout(1000)
        count = len(page.query_selector_all(selector))
        if count == prev:
            break
        prev = count
    return prev

Pour réduire encore davantage la charge du navigateur, bloquez les ressources lourdes dont vous n’avez pas besoin :

context.route("**/*.{png,jpg,jpeg,gif,svg,woff2,mp4}", lambda route: route.abort())

Cette simple ligne réduit souvent le temps de chargement des pages de 40 % ou plus sur les sites riches en médias, car vous évitez les téléchargements que vous ne feriez que supprimer. Combiné à wait_for_selector, cela rend Playwright véritablement compétitif face aux scrapers statiques en termes de vitesse pour les pages nécessitant un rendu JavaScript. Lorsque vous utilisez Playwright à grande échelle, lancez un contexte de navigateur persistant par worker au lieu d’un nouveau navigateur par URL, et réutilisez la même page pour des requêtes similaires. Le démarrage à froid de Chromium coûte environ une seconde par page, et cette surcharge domine tout crawling réel.

Selenium comme alternative pour les pages JavaScript

Selenium précède Playwright d’environ une décennie, et de nombreuses équipes l’utilisent encore en CI car leur infrastructure de test existante est déjà compatible avec WebDriver. C’est un choix tout à fait raisonnable pour le scraping si vous faites partie de ces équipes.

Un scraper Selenium minimal utilisant webdriver-manager afin que vous n’ayez pas à télécharger manuellement ChromeDriver :

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from webdriver_manager.chrome import ChromeDriverManager

opts = Options()
opts.add_argument("--headless=new")
opts.add_argument("--window-size=1366,900")

driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=opts)
driver.get("https://books.toscrape.com/")

WebDriverWait(driver, 15).until(
    EC.presence_of_element_located((By.CSS_SELECTOR, "article.product_pod"))
)

results = []
for card in driver.find_elements(By.CSS_SELECTOR, "article.product_pod"):
    title = card.find_element(By.CSS_SELECTOR, "h3 a").get_attribute("title")
    price = card.find_element(By.CSS_SELECTOR, "p.price_color").text.strip()
    results.append({"title": title, "price": price})

driver.quit()

Selenium s’appuie sur un WebDriver, qui sert de pont entre votre script Python et le processus du navigateur proprement dit. Chrome utilise ChromeDriver, Firefox utilise GeckoDriver, Edge utilise EdgeDriver, et chacun doit correspondre à la version majeure du navigateur.

Comparé à Playwright, Selenium présente un démarrage plus lent, une API légèrement plus verbeuse et ne dispose pas d’interception de requêtes intégrée. Ses atouts résident dans la maturité de son écosystème, une prise en charge de premier ordre chez la plupart des fournisseurs de CI, et le fait que vos ingénieurs de test le connaissent déjà. Si vous partez de zéro, Playwright est plus simple. Si vous étendez une suite de tests existante, Selenium convient parfaitement. Notre guide pratique sur Selenium aborde les modèles de mise à l’échelle et les techniques de contournement de Cloudflare lorsque le flux par défaut est bloqué.

Un détail spécifique à Selenium qu’il convient de bien assimiler : privilégiez toujours WebDriverWait avec des conditions explicites (presence_of_element_located, element_to_be_clickable) à time.sleep. Les attentes explicites s’achèvent dès que la condition est remplie ; les temps d’attente gaspillent du temps sur les pages rapides et échouent quand même sur les pages lentes.

Évoluer avec Scrapy : robots d’exploration, pipelines et exportations

Scrapy n’est pas une simple bibliothèque d’analyse syntaxique. Il s’agit d’un framework complet de crawling : un moteur asynchrone, une pile de middleware pour les en-têtes et les proxys, des pipelines d’éléments pour le nettoyage et la persistance, ainsi que des exportations intégrées vers JSON, JSONL et CSV. Optez pour cette solution lorsque votre tâche porte sur environ un millier d’URL, nécessite des tentatives de reprise ou comporte plusieurs étapes de sortie.

Mettre en place un projet :

scrapy startproject bookstore
cd bookstore
scrapy genspider books books.toscrape.com

Modifier bookstore/spiders/books.py:

import scrapy

class BooksSpider(scrapy.Spider):
    name = "books"
    start_urls = ["https://books.toscrape.com/catalogue/page-1.html"]
    custom_settings = {
        "DOWNLOAD_DELAY": 0.5,
        "CONCURRENT_REQUESTS": 8,
        "USER_AGENT": "Mozilla/5.0 (compatible; MyScraper/1.0)",
        "RETRY_TIMES": 3,
    }

    def parse(self, response):
        for card in response.css("article.product_pod"):
            yield {
                "title": card.css("h3 a::attr(title)").get(),
                "price": card.css("p.price_color::text").get(),
                "stock": card.css("p.instock::text").re_first(r"\S.*"),
            }
        next_page = response.css("li.next a::attr(href)").get()
        if next_page:
            yield response.follow(next_page, self.parse)

Exécuter et exporter :

scrapy crawl books -O books.jsonl

Cette commande unique explore chaque page, suit le lien vers la page suivante, applique vos paramètres de délai et de nouvelle tentative, puis transmet les résultats au format JSONL. L'option -O drapeau écrase le fichier ; -o ajoute les données à la fin du fichier.

Deux fonctionnalités de Scrapy méritent d’être maîtrisées dès le début :

les pipelines d’éléments font passer chaque élément extrait par une chaîne de classes Python. Utilisez-les pour la normalisation, la déduplication et la persistance. Un PriceCleanerPipeline peut supprimer les symboles monétaires ; une SQLitePipeline peut l'insérer dans une base de données. Connectez les pipelines dans settings.py sous ITEM_PIPELINES.

Les middlewares s’intègrent à chaque requête ou réponse. C’est là qu’interviennent la rotation des proxys, la rotation des en-têtes et la gestion de Cloudflare. Les middlewares communautaires comme scrapy-rotating-proxies et scrapy-user-agents s'intègrent en deux lignes de configuration.

Lorsque vous tombez sur des pages riches en JavaScript, n’abandonnez pas Scrapy. Ajoutez scrapy-playwright et marquez les requêtes spécifiques avec meta={"playwright": True}. Vous conservez ainsi la concurrence, les pipelines et les exportations de Scrapy, tout en ne payant le coût du navigateur que pour les pages qui en ont besoin. Notre guide pratique Scrapy et le guide d’intégration de scrapy-playwright traitent de ce modèle hybride.

Voici à quoi ressemble un pipeline minimal qui normalise les prix et supprime les doublons :

# bookstore/pipelines.py
import re

class BookstorePipeline:
    def __init__(self):
        self.seen = set()

    def process_item(self, item, spider):
        title = item.get("title")
        if title in self.seen:
            raise DropItem(f"duplicate: {title}")
        self.seen.add(title)
        raw_price = item.get("price") or ""
        item["price"] = float(re.sub(r"[^\d.]", "", raw_price) or 0)
        return item

Intégrez-le settings.py avec ITEM_PIPELINES = {"bookstore.pipelines.BookstorePipeline": 300}. L’entier correspond à la priorité ; les classes avec la priorité la plus faible s’exécutent en premier. Ajoutez d’autres classes pour la validation, le stockage et les alertes Slack, et toute la chaîne de post-traitement devient déclarative.

Quand ne pas utiliser Scrapy. Pour un scraping ponctuel d’une seule page, la structure de projet de Scrapy est surdimensionnée. Pour toute tâche récurrente, ou toute tâche impliquant plus de quelques centaines d’URL, les paramètres par défaut de concurrence et de réessai justifient immédiatement le code standard. Une règle empirique utile : si vous vous retrouvez à écrire votre propre boucle asynchrone, votre propre pool de threads ou votre propre décorateur de réessai autour de Requests, c’est que vous avez suffisamment réinventé Scrapy pour qu’il vaille mieux l’utiliser tel quel.

Utilisez une API de scraping Web pour les cibles anti-bot

Certains sites résisteront à tout ce que vous avez lu jusqu’à présent. Ils identifient votre poignée de main TLS, bloquent les adresses IP des centres de données, affichent des CAPTCHA dès le premier contact ou renvoient un code d’état 200 OK avec un corps indiquant « veuillez activer JavaScript ». Playwright peut contourner certains de ces obstacles ; les proxys résidentiels en contournent davantage. Lorsque cette combinaison échoue encore, une API de scraping hébergée est la solution la plus pragmatique.

Une API de scraping prend une URL et renvoie du code HTML. Elle gère la couche des requêtes : rendu du navigateur, rotation des proxys, tentatives de réessai et contournement des mesures anti-bot. Vous conservez votre code d’analyse Beautiful Soup ou lxml existant et remplacez simplement l’appel de récupération. Cela signifie que vous n’avez pas à gérer de pools de proxys, à actualiser les empreintes de navigateur, ni à déboguer les raisons pour lesquelles le solveur de CAPTCHA d’une région est plus lent aujourd’hui.

Un appel type ressemble à ceci :

import requests

API_ENDPOINT = "https://api.webscrapingapi.com/v2"
params = {
    "api_key": "YOUR_KEY",
    "url": "https://example.com/hard-target",
    "render_js": "true",
    "proxy_type": "residential",
    "country": "us",
}
resp = requests.get(API_ENDPOINT, params=params, timeout=60)
resp.raise_for_status()
html = resp.text

# Parse with the same code you would use on a static page.
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, "lxml")

Les paramètres sont importants. render_js=true exécute la cible via un navigateur sans interface utilisateur côté serveur. proxy_type=residential achemine la requête via des adresses IP qui ressemblent à du trafic domestique. country=us géolocalise la requête lorsque la cible propose un contenu différent selon les régions.

Deux limites importantes méritent d’être soulignées. Premièrement, vous payez à la requête ; le coût de l’API de scraping augmente donc linéairement avec le volume. Si votre page est statique et ne bloque rien, l’utilisation de Requests avec Beautiful Soup revient bien moins cher, de plusieurs ordres de grandeur. Deuxièmement, certains fournisseurs publient des chiffres très élevés concernant leur pool de proxys à des fins marketing ; considérez ces chiffres comme indicatifs jusqu’à ce que vous les ayez confirmés dans la documentation officielle du fournisseur. La bonne décision n’est pas « l’API par défaut », mais « l’API lorsque la couche de récupération est à l’origine du problème ».

Une règle empirique utile : si vous passez plus d’une journée par semaine à déboguer des blocages plutôt que des erreurs d’analyse, la couche de récupération n’est plus votre problème, mais celui de quelqu’un d’autre. Confiez-la à une API de scraping et libérez ainsi les heures d’ingénierie pour le travail d’extraction et de modélisation que seule votre équipe peut effectuer.

Gérer les erreurs HTTP, les nouvelles tentatives et les délais d’expiration

Un scraper sans politique de nouvelle tentative est un scraper qui échouera dès sa première nuit en production. Les réponses HTTP vous indiquent exactement quoi faire, si vous savez les écouter.

Statut

Signification

Réaction appropriée

200

Succès

Analyser et passer à la suite

301 / 302

Rediriger

Suivre (c'est le comportement par défaut des requêtes)

403

Accès interdit

Alternance d'adresse IP ou d'agent utilisateur, vérification anti-bot

404

Introuvable

Ignorer et consigner ; la ressource n'existe plus ou l'URL est incorrecte

429

Trop de requêtes

Respectez Retry-After, reculez, ralentissez

5xx

Erreur serveur

Réessayer avec un recul exponentiel, puis abandonner

Requests plus urllib3 vous permet de définir une politique de réessai adéquate en quelques lignes :

import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

session = requests.Session()
retry = Retry(
    total=5,
    backoff_factor=1.5,           # 1.5s, 3s, 6s, 12s, 24s
    status_forcelist=[429, 500, 502, 503, 504],
    allowed_methods=["GET", "HEAD"],
    respect_retry_after_header=True,
)
session.mount("https://", HTTPAdapter(max_retries=retry))
session.mount("http://", HTTPAdapter(max_retries=retry))

resp = session.get(url, timeout=(5, 30))    # (connect, read)

Définissez des timeout pour chaque requête. Un tuple de (connect, read) secondes est plus sûr qu’un simple nombre et évite l’erreur classique où « un seul serveur d’origine lent bloque l’ensemble de l’exploration ».

Dans Playwright, les délais d’expiration s’appliquent à chaque action : page.goto(url, timeout=30_000) et page.wait_for_selector(sel, timeout=15_000). Les tentatives de réessai doivent être explicites, car Playwright ne les effectue pas automatiquement. Enveloppez les navigations dans votre propre boucle ou utilisez tenacity pour un décorateur déclaratif :

from tenacity import retry, stop_after_attempt, wait_exponential

@retry(stop=stop_after_attempt(4), wait=wait_exponential(multiplier=1.5, min=2, max=30))
def goto_with_retry(page, url):
    page.goto(url, wait_until="domcontentloaded", timeout=30_000)

Enregistrez le code d’état, l’URL et le numéro de tentative à chaque nouvelle tentative. C’est grâce à ces tentatives silencieuses que les scrapers « fonctionnent » pendant des semaines tout en renvoyant des données obsolètes. Une autre bonne habitude : limitez le budget total de tentatives par URL, et pas seulement le nombre de tentatives. Une requête qui prend 90 secondes en quatre tentatives est pire qu’une requête qui échoue rapidement au bout de 15 secondes et passe à la suivante, car cet échec lent bloque toutes les autres URL partageant ce worker.

Éviter d’être bloqué : proxys, en-têtes et limites de débit

La plupart des blocages s’expliquent par trois signaux : vos en-têtes ne ressemblent pas à ceux d’un navigateur, vos requêtes sont trop rapides et votre adresse IP figure sur une liste noire de centres de données. Corrigez ces trois points.

Faites tourner un pool d’agents-utilisateurs réels et envoyez les en-têtes qu’un vrai navigateur envoie. Les sites identifient les Accept, Accept-Language, et Accept-Encoding aussi, pas seulement User-Agent.

import random, time

USER_AGENTS = [
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 ...",
    # ...more real UAs
]

def browser_headers():
    return {
        "User-Agent": random.choice(USER_AGENTS),
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
        "Accept-Language": "en-US,en;q=0.9",
        "Accept-Encoding": "gzip, deflate, br",
    }

Ajoutez de la gigue à la cadence de vos requêtes. Une cadence fixe time.sleep(1) est plus facile à détecter qu’un timing aléatoire typique d’un utilisateur humain.

time.sleep(random.uniform(1.2, 3.5))

Alternez les proxys, de préférence résidentiels. Les adresses IP de centres de données sont bloquées en masse car elles sont faciles à détecter. Les proxys résidentiels acheminent le trafic via de véritables appareils d’utilisateurs et se fondent dans la masse.

PROXIES = [
    "http://user:pass@proxy1.example.com:8000",
    "http://user:pass@proxy2.example.com:8000",
    # ...
]

def get(url):
    proxy = random.choice(PROXIES)
    return requests.get(
        url,
        headers=browser_headers(),
        proxies={"http": proxy, "https": proxy},
        timeout=(5, 30),
    )

Détectez rapidement les échecs. Une réponse avec un statut 200 et un corps contenant « Just a moment... », « Attention Required » ou « cf-chl-bypass » correspond à un défi Cloudflare, et non à du contenu réel.

def looks_blocked(resp):
    body = resp.text[:5000].lower()
    return any(k in body for k in [
        "just a moment", "attention required", "cf-chl-bypass",
        "captcha", "access denied",
    ])

En cas looks_blocked un incident survient, alternez les proxys, réduisez radicalement la fréquence des requêtes et envisagez de basculer vers une API de scraping. Nos guides sur la rotation des proxys et la stratégie « no-block » approfondissent les méthodes de récupération après un blocage d’IP.

Récupérez-vous des interdictions d’IP sans redémarrer le processus. Conservez les proxys opérationnels dans un collections.deque. Lorsqu’un proxy renvoie un 403 ou un CAPTCHA, reléguez-le en fin de liste et imposez-lui un délai de refroidissement avant de le remettre en première ligne. Si l’ensemble du pool est inactif, mettez le système en veille plutôt que de le faire planter :

from collections import deque
pool = deque(PROXIES)

def next_proxy():
    proxy = pool.popleft()
    pool.append(proxy)
    return proxy

Cela ne suffit pas pour les cibles les plus difficiles, mais cela permettra à un scraper bien conçu de fonctionner sur la plupart des sites pendant des mois sans intervention manuelle. Lorsque cela ne suffit plus, c’est le signal qu’il faut remplacer la couche de récupération par une API hébergée plutôt que de continuer à agrandir votre pool de proxys.

Stockage des données extraites : CSV, JSON, SQLite et formats compatibles avec les LLM

Votre choix de stockage dépend de la manière dont les données seront utilisées. Choisissez avec soin ; une conversion ultérieure s’avère fastidieuse.

CSV pour les feuilles de calcul et les transferts rapides :

import csv
with open("books.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(f, fieldnames=["title", "price", "in_stock"])
    writer.writeheader()
    writer.writerows(records)

JSON pour les enregistrements imbriqués et les sorties au format API :

import json
with open("books.json", "w", encoding="utf-8") as f:
    json.dump(records, f, ensure_ascii=False, indent=2)

SQLite pour les exécutions répétées avec déduplication et requêtes. Il s’agit d’un seul fichier, sans serveur, et fourni avec Python :

import sqlite3
conn = sqlite3.connect("books.db")
conn.execute(
    "CREATE TABLE IF NOT EXISTS books ("
    " url TEXT PRIMARY KEY,"
    " title TEXT, price REAL, scraped_at TEXT)"
)
conn.executemany(
    "INSERT OR REPLACE INTO books VALUES (?, ?, ?, ?)",
    [(r["url"], r["title"], r["price"], r["scraped_at"]) for r in records],
)
conn.commit()

Parquet pour les charges de travail analytiques destinées à DuckDB, Spark ou un entrepôt de données : pandas.DataFrame(records).to_parquet("books.parquet"). Format en colonnes et typé, ce qui accélère les requêtes en aval.

Sortie JSONL compatible avec les LLM

Si vos données extraites sont destinées à alimenter un LLM ou un pipeline d’embedding, fournissez-les au format JSONL : un objet JSON par ligne, un enregistrement par document. Chaque enregistrement doit comporter un identifiant stable, une URL source, un horodatage et un text champ que le modèle peut segmenter.

def to_llm_record(item):
    return {
        "id": item["url"],
        "source_url": item["url"],
        "scraped_at": item["scraped_at"],
        "title": item["title"],
        "text": f"{item['title']}\n\nPrice: {item['price']}\n\n{item.get('description', '')}",
        "metadata": {"category": item.get("category"), "in_stock": item["in_stock"]},
    }

with open("books.jsonl", "w", encoding="utf-8") as f:
    for r in records:
        f.write(json.dumps(to_llm_record(r), ensure_ascii=False) + "\n")

Limitez la taille des segments à environ 2 000 tokens, conservez les URL sources à des fins de citation et ne fusionnez jamais les enregistrements provenant de différentes sources. Ce sont ces métadonnées par enregistrement qui permettent à un pipeline RAG en aval de rester fiable.

Intégration des résultats dans pandas ou un entrepôt de données. Pour les workflows d’analyse, chargez votre JSONL ou SQLite directement dans un dataframe et poursuivez le traitement en mémoire :

import pandas as pd
df = pd.read_json("books.jsonl", lines=True)
df["price"] = df["price"].astype(float)
df.to_parquet("books.parquet")

Pour les volumes plus importants, duckdb.read_json("books.jsonl") vous permet d’exécuter des requêtes SQL sur le même fichier sans le charger en mémoire. Ces deux approches garantissent une séparation totale entre votre étape d’extraction et votre étape d’analyse, ce qui est indispensable lorsque le scraper change mais que l’analyse reste la même.

Planifier et surveiller les scrapers récurrents

Un scraper que vous exécutez manuellement relève du passe-temps. Un scraper que vous exécutez selon un calendrier relève de l’infrastructure de données, et nécessite la même rigueur que n’importe quelle autre tâche.

Cron est le moyen le plus rapide de planifier des tâches sous Linux. Modifiez le fichier crontab -e:

undefined0 6 * * * /path/to/.venv/bin/python /path/to/scraper.py >> /var/log/scraper.log 2>&1

Cela s’exécute quotidiennement à 06h00 et capture stdout et stderr. Si votre tâche dure plus de quelques minutes ou comporte des dépendances, privilégiez une minuterie systemd, qui vous offre une sémantique de démarrage/arrêt appropriée, des politiques de redémarrage et une intégration avec journalctl. Pour la planification en cours d’exécution pendant le développement, la schedule bibliothèque est légère et lisible :

import schedule, time
schedule.every().day.at("06:00").do(run_job)
while True:
    schedule.run_pending()
    time.sleep(30)

Consignez les journaux de manière structurée. Les journaux en texte brut ne sont pas consultables ; les journaux JSON peuvent être redirigés vers Loki, Datadog ou une table SQLite sans post-traitement :

import logging, json
logging.basicConfig(level=logging.INFO, format="%(message)s")

def log_event(event, **fields):
    logging.info(json.dumps({"event": event, **fields}))

log_event("page_scraped", url=url, status=resp.status_code, items=len(items))

Créez des alertes sur les métriques qui comptent : des erreurs 403 répétées, une baisse soudaine du nombre d’éléments (signe courant d’une modification du balisage du site) et une durée d’exécution totale dépassant un seuil. Un scraper silencieux renvoyant zéro ligne est pire qu’un scraper qui plante bruyamment, car vous risquez de ne pas vous en apercevoir pendant des semaines. Définissez une vérification du « nombre minimum de lignes attendu » et envoyez-vous une alerte si ce nombre passe en dessous :

MIN_EXPECTED = 400
if len(items) < MIN_EXPECTED:
    log_event("row_count_alert", got=len(items), expected=MIN_EXPECTED)
    raise SystemExit(2)     # non-zero exit code trips your cron alerter

Pour les tâches de scraping web en Python de longue durée, émettez également un signal de vie toutes les N pages afin qu’un outil de surveillance externe (Healthchecks.io, Uptime Kuma ou une simple sentinelle cron) puisse distinguer un processus bloqué d’un processus simplement lent. Le silence est le mode de défaillance contre lequel vous devez concevoir votre système.

Débogage des scrapers défaillants : une liste de contrôle reproductible

Les scrapers tombent en panne pour un petit nombre de raisons. Parcourez cette liste de contrôle dans l’ordre ; la solution se trouve presque toujours dans les trois premières étapes.

  1. Vérifiez la longueur du code HTML brut. print(len(resp.text)). Un corps de moins de quelques kilo-octets indique généralement une page de vérification, une limitation de débit ou une structure vide avant l’exécution du JavaScript. Si la longueur a diminué par rapport à une référence fonctionnelle, le problème provient de la couche de récupération, et non de l’analyseur syntaxique.
  2. Enregistrez la page rendue sur le disque et ouvrez-la dans un navigateur. Path("debug.html").write_bytes(resp.content). Neuf fois sur dix, cela permet de voir immédiatement si vous avez obtenu du contenu réel, une page de connexion ou le message « Veuillez activer JavaScript ».
  3. Comparez les sélecteurs avec un instantané dont vous savez qu’il fonctionne. Conservez un petit tests/fixtures/ répertoire contenant des exemples de pages réelles datant de l’époque où le scraper fonctionnait. En cas de dysfonctionnement, diff comparez le code HTML actuel à ce modèle et recherchez des classes renommées, de nouveaux conteneurs ou des éléments supprimés.
  4. Vérifiez la présence d’un chargement différé. Si le nombre d’éléments a diminué mais que la page semble normale dans le navigateur, le site est probablement passé à des listes virtualisées ou au défilement infini. Rouvrez l’onglet « Réseau » et recherchez un point de terminaison JSON que le frontend appelle désormais après le montage.
  5. Recherchez une API JSON cachée. Au fil du temps, les interfaces utilisateur sont refactorisées vers des API. Ce qui nécessitait auparavant Playwright se résume souvent à un simple fetch() appel à /api/products?page=2. Ce point de terminaison est stable, rapide et constitue presque toujours la bonne réponse dès lors qu’il existe.
  6. Vérifiez la sémantique HTTP. Obtenez-vous 200 mais un JSON vide ? Une 403 de manière intermittente ? Des cookies incorrects ? Ajoutez des journaux concernant les codes d’état, les en-têtes de réponse et les cookies avant de supposer que le parseur est en cause.

Cadre juridique et éthique du scraping en Python

Le scraping Web est légal dans de nombreuses juridictions lorsqu’il s’applique à des données publiques, mais « légal » ne signifie pas « responsable ». Veillez à respecter ces deux aspects.

Respectez robots.txt. Le protocole d’exclusion des robots (Robots Exclusion Protocol) est désormais une norme Internet officielle publiée sous le numéro IETF RFC 9309, et bien qu’il ne s’agisse pas en soi d’une loi, de nombreuses conditions d’utilisation y font référence. Analysez-le à l’aide de la fonction intégrée à Python urllib.robotparser et ignorez les chemins interdits :

from urllib.robotparser import RobotFileParser
rp = RobotFileParser()
rp.set_url("https://example.com/robots.txt")
rp.read()
if not rp.can_fetch("MyScraper/1.0", target_url):
    return

Lisez les conditions d’utilisation du site cible. Certains sites interdisent explicitement la collecte automatisée ; ignorer ces dispositions peut donner lieu à une plainte pour rupture de contrat, même si aucune loi n’a été enfreinte. Portez une attention particulière aux sites nécessitant une connexion, car le fait de franchir une barrière d’authentification modifie considérablement le cadre juridique.

Ne collectez pas de données à caractère personnel à la légère. Si les données contiennent des éléments permettant d’identifier une personne (nom, e-mail, adresse, adresse IP, cookies liés à un utilisateur), vous entrez probablement dans le champ d’application de lois telles que le RGPD, la loi britannique sur la protection des données ou le CCPA. Consultez un avocat avant de mettre en place un pipeline permettant de stocker ces données.

Privilégiez les API officielles lorsqu’elles existent, ne contournez pas les barrières de paiement ni l’authentification, et imposez-vous une limitation de débit même lorsque la cible ne vous y oblige pas. Un scraper lent et respectueux, capable d’évoluer de manière durable, vaut mieux qu’un scraper rapide qui vaudra à toute votre entreprise d’être bannie par adresse IP. Notre cadre de conformité juridique détaille la situation pays par pays.

Enfin, définissez une description claire et honnête User-Agent qui identifie votre projet et fournit une adresse de contact (MyProject/1.0 (+https://example.com/contact)). C’est une petite marque de courtoisie qui permet aux propriétaires de sites de contacter quelqu’un par e-mail avant de recourir à leur WAF, et qui, dans la pratique, réduit considérablement le risque de blocage d’adresse IP.

Trois projets pour débuter

La meilleure façon de consolider une pile de web scraping en Python consiste à développer le même scraper de base pour trois types de cibles différents. Chacun de ces projets est suffisamment court pour être réalisé en un après-midi et suffisamment réaliste pour vous enseigner les schémas que vous utiliserez à grande échelle.

Suivi des prix. Choisissez une page de démonstration de site e-commerce (books.toscrape.com est une option sûre et stable). Récupérez le nom du produit, son prix et son stock dans une base SQLite une fois par jour, conservez chaque instantané et tracez une courbe de prix par produit à l’aide de pandas. Cela vous permettra de vous familiariser avec les exécutions idempotentes, la déduplication par URL et le stockage de séries chronologiques. Dès que votre site de test ajoutera un champ « remise », vous pourrez également vous exercer à gérer la dérive de schéma. Notre guide sur les données produits propose des modèles similaires pour de véritables cibles de commerce électronique.

Agrégateur d’offres d’emploi. Récupérez des fiches d’emploi à partir d’un site d’offres d’emploi public (ou d’une démo comme realpython.github.io/fake-jobs/). Normalisez les intitulés, les lieux et les dates de publication, puis dédupliquez les données d’une page à l’autre. Ce projet valorise une normalisation rigoureuse et vous explique pourquoi l’extraction et le nettoyage doivent être des étapes distinctes. Notre guide sur les données d’emploi aborde les modèles de mise à l’échelle lorsque vous passez d’un site à cinq.

Tableau de bord des titres d’actualités. Récupérez les titres et les horodatages de publication à partir d’un flux RSS public ou d’un agrégateur d’actualités. Stockez-les au format JSONL avec title, url, published_at, source. Ce projet vous prépare aux travaux en aval avec les grands modèles de langage (LLM) : le format JSONL s’intègre directement dans un pipeline d’embedding, et les métadonnées par enregistrement (source, horodatage) prennent en charge le RAG, qui repose largement sur les citations. Notre guide sur les données d’actualité aborde la surveillance des modifications à partir du même scraper de base.

Même pipeline en trois étapes (récupération, analyse, extraction), trois formats de données différents. C’est ainsi que vous intégrez le modèle.

Liste de contrôle de production avant de déployer un scraper

Avant de faire passer votre scraper du statut de script local à celui de tâche de production, parcourez cette liste. Les éléments manquants sont la raison pour laquelle « ça marchait hier » se transforme en une alerte à 3 heures du matin.

Points clés à retenir

  • Commencez par l’outil le plus léger. Requests associé à Beautiful Soup gère la plupart des pages statiques ; n’utilisez Playwright que lorsque les données sont rendues par JavaScript, et Scrapy uniquement lorsque vous avez besoin d’évolutivité, de pipelines et de tentatives de récupération.
  • Inspectez la cible dans DevTools avant d’écrire du code. Les points de terminaison JSON cachés sont presque toujours plus fiables que le scraping de HTML.
  • Séparez l’extraction de la normalisation. Stockez les valeurs brutes, nettoyez-les lors d’un deuxième passage et dédupliquez-les à l’aide d’une clé stable.
  • Le blocage est un problème lié à la couche de récupération. Alternez les agents utilisateurs, ajoutez du jitter, utilisez des proxys résidentiels et détectez explicitement les pages de vérification.
  • Déployez vos scrapers comme de véritables tâches : journaux structurés, tentatives de récupération, délais d’expiration, alertes de dérive de schéma et liste de contrôle de production. L’échec silencieux est le mode de défaillance contre lequel vous devez concevoir votre système.

FAQ

En quoi le web scraping diffère-t-il de l’utilisation d’une API officielle ?

Une API officielle est un contrat pris en charge, versionné, avec des limites de débit, une authentification et des noms de champs stables. Le scraping consiste à lire une page conçue pour les humains et à déduire une structure à partir du code HTML, qui peut changer sans préavis. Les API sont plus rapides, moins coûteuses et juridiquement plus sûres lorsqu’elles existent. Ne recourez au scraping que lorsqu’aucune API ne couvre vos données, ou lorsque les conditions d’utilisation de l’API excluent votre cas d’utilisation.

Quel nombre de requêtes par seconde est considéré comme sûr lors du scraping d’un site public ?

Il n’existe pas de chiffre universel, mais un point de départ raisonnable est d’une requête toutes les 2 à 5 secondes par domaine, avec un espacement aléatoire. Si le site publie une limite de débit dans robots.txt ou dans sa documentation, respectez-la. Surveillez les réponses HTTP 429 et réduisez votre fréquence de manière exponentielle. Un crawling plus rapide n’est possible qu’avec l’autorisation explicite du site ou via un service hébergé qui négocie déjà la charge avec la cible.

Comment faire en sorte que mon scraper Python continue de fonctionner lorsque le site cible modifie son code HTML ?

Testez les sélecteurs sur un modèle HTML figé en CI, surveillez les taux de remplissage par champ à chaque exécution et déclenchez une alerte en cas de baisse. Privilégiez les sélecteurs basés sur des attributs sémantiques (data-testid, itemprop) plutôt que sur des noms de classes générés automatiquement. Maintenez l’extraction et la normalisation découplées afin qu’un sélecteur défectueux n’entraîne l’échec que d’un champ, et non de l’ensemble de la tâche. Gérez les versions de votre logique d’analyse afin que la restauration ne nécessite qu’un seul commit.

Quand dois-je passer d’un scraper Python auto-hébergé à une API de scraping gérée ?

Passez à l’API lorsque c’est la couche de récupération, et non l’analyseur, qui pose problème. Parmi les signes avant-coureurs : des erreurs 403 répétées malgré la rotation des proxys, des CAPTCHA dès le premier contact, des blocages dus aux empreintes TLS, et un temps de maintenance de l’infrastructure dépassant celui consacré à la logique de traitement des données. Conservez votre code d’analyse existant ; seule la couche de requêtes doit être remplacée. Si la cible est statique et non protégée, l’auto-hébergement reste nettement moins coûteux.

Comment dois-je stocker les données extraites si je prévois de les intégrer ultérieurement dans un LLM ?

Utilisez le format JSONL avec un enregistrement par ligne. Chaque enregistrement doit comporter un id, un source_url, un scraped_at horodatage, un champ en texte brut text pour l’intégration, ainsi qu’un metadata objet pour les filtres. Veillez à ce que chaque text en morceaux d’environ 2 000 tokens afin que les outils de découpage en aval n’aient pas à les fractionner de manière arbitraire. Ne fusionnez jamais les enregistrements provenant de différentes sources et conservez les URL afin qu’un pipeline RAG puisse les citer.

Conclusion

En 2026, le web scraping en Python ne consiste pas tant à apprendre une énième bibliothèque qu’à choisir celle qui convient à chaque cible, puis à l’intégrer dans l’infrastructure qui assure son fonctionnement. Une première approche avec Requests et Beautiful Soup couvre la plupart des pages statiques. Playwright gère le JavaScript. Scrapy vous permet de traiter des milliers d’URL. XPath, la rotation des proxys, la journalisation structurée et les alertes de dérive de schéma transforment un script en une tâche que vous pouvez réellement laisser tourner toute seule. Et lorsque la couche de récupération d’une cible est véritablement hostile, une API de scraping constitue une solution de secours pragmatique plutôt qu’une option par défaut.

L’habitude la plus importante est de commencer modestement. Ne lancez pas de navigateur sans interface graphique pour extraire une page que vous pourriez analyser avec lxml. Ne mettez pas en place un pool de proxys avant d’avoir écrit un scraper fonctionnel. Déployez d’abord la version simple, n’ajoutez des couches que lorsque la version actuelle atteint ses limites, et instrumentez tout pour que vous puissiez détecter les changements sur le site avant même que votre tableau de bord ne les signale.

Lorsque vous tombez sur une cible qui identifie les navigateurs, bloque les adresses IP de centres de données ou affiche des CAPTCHA dès le premier contact, notre équipe chez WebScrapingAPI gère la couche de requêtes (rotation de proxys, rendu JS, tentatives de reconnexion) afin que vous puissiez conserver votre code d’analyse Python et éviter la course aux armements anti-bots. Testez-le sur votre URL la plus difficile et voyez quelle partie du pipeline reste sous votre contrôle.

À propos de l'auteur

Raluca Penciuc, Développeur full-stack @ WebScrapingAPI

Raluca Penciuc

Développeur full-stack

Raluca Penciuc est développeuse Full Stack chez WebScrapingAPI ; elle conçoit des robots de collecte de données, améliore les techniques de contournement et recherche des moyens fiables de réduire le risque de détection sur les sites cibles.

Web scraping avec AWS Lambda : guide pour Python et Java 2026
Guides

Web scraping avec AWS Lambda : guide pour Python et Java 2026

En bref : le web scraping avec AWS Lambda fonctionne mieux lorsque chaque invocation est courte, bien délimitée et peut faire l'objet d'une nouvelle tentative de manière indépendante. Commencez par utiliser les requêtes HTTP directes, AWS SAM et S3, puis ajoutez SQS, des conteneurs, le rendu dans un navigateur, des proxys ou une couche de récupération gérée uniquement lorsque la charge de travail démontre qu'elle en a besoin.

Suciu Dan33 min read
Lire l'article
Comment utiliser GoSpider : exploration, nettoyage des URL et extraction de données
Guides

Comment utiliser GoSpider : exploration, nettoyage des URL et extraction de données

En bref : GoSpider est un robot d'exploration en ligne de commande destiné à découvrir des URL, et non un outil complet de collecte de données structurées. Ce guide d'utilisation de GoSpider présente comment effectuer une exploration limitée, gérer correctement les résultats, transférer les données de Colly vers un fichier CSV, ainsi qu'une procédure de diagnostic pour les réponses 403 ou les pages nécessitant un rendu JavaScript.

Suciu Dan24 min read
Lire l'article
Comment récupérer les données de Redfin : Guide Python des données immobilières
Guides

Comment récupérer les données de Redfin : Guide Python des données immobilières

TL;DR : Redfin expose des points d'extrémité d'API cachés qui renvoient du JSON structuré pour les listes de propriétés, ce qui permet d'ignorer complètement l'analyse HTML fragile. Ce guide vous accompagne dans la construction d'un scraper Python qui extrait les données de location et de vente, effectue des recherches par emplacement, surveille les nouvelles inscriptions via des sitemaps XML et exporte des résultats propres au format CSV ou JSON.

Suciu Dan15 min read
Lire l'article

Commencez à créer

Prêt à faire évoluer votre système de collecte de données ?

Rejoignez plus de 2 000 entreprises qui utilisent WebScrapingAPI pour extraire des données Web à l'échelle de l'entreprise, sans aucun coût d'infrastructure.