Aller au contenu
Retour au blog

Bonnes pratiques en matière de web scraping : un guide complet couvrant l'ensemble du cycle de vie, avec des conseils et du code fonctionnel

Anda MiuțescuDernière mise à jour le 36 min read
Bonnes pratiques en matière de web scraping : un guide complet couvrant l'ensemble du cycle de vie, avec des conseils et du code fonctionnel
En bref : Les bonnes pratiques du web scraping sont les habitudes qui rendent un scraper légal, respectueux, précis et peu coûteux à exploiter. Rédigez un contrat de données, préférez les API ou le JSON au HTML, respectez robots.txt tel que le définit la RFC 9309, espacez les requêtes vers chaque hôte, respectez Retry-After et validez chaque enregistrement avant de le sauvegarder. Ce guide les applique à chaque étape du cycle de vie d'un scraper, avec des extraits Python fonctionnels.

Les bonnes pratiques en matière de web scraping désignent les habitudes qui permettent à un scraper de rester légal, respectueux, précis et peu coûteux à exploiter : planifier les données dont vous avez besoin, respecter le fichier robots.txt et les conditions d’utilisation du site, limiter le nombre de requêtes par serveur, gérer les erreurs 429 avec un délai de réessai, analyser les données de manière défensive, valider chaque enregistrement, surveiller chaque exécution, vérifier la progression et suivre le coût par enregistrement valide.

Le guide ci-dessous applique ces bonnes pratiques de web scraping à chaque étape du cycle de vie d’un scraper, de la planification initiale à la facture mensuelle, avec un court extrait de code Python chaque fois que cela permet de clarifier le propos.

Les extraits de code nécessitent Python 3.10 ou une version plus récente, ainsi que deux bibliothèques :

pip install requests beautifulsoup4

Enregistrez chaque extrait sous le nom de fichier indiqué au-dessus et exécutez-le à partir de ce dossier, par exemple avec python polite_fetch.py.

Étape

Objectif

Pratique clé

Section

Plan

Savoir à quoi ressemble un travail « terminé »

Contrat de données et ordre des sources

Planification

Juridique et courtois

Rester bien accueilli

RFC 9309 robots.txt, User-Agent honnête

Scraping légal et respectueux

Fréquence

Ne surchargez pas

Limitation par hôte, Retry-After, backoff

Taux de requêtes

Requêtes

Moins d'échecs

Délais d'expiration, sessions, codes 304

Habitudes de requêtes

Blocages

Récupération en toute sérénité

Diagnostiquer, puis remonter d'un cran

Blocages

Rendu

Ne payez qu’en cas de besoin

JSON d'abord, navigateur ensuite

Navigateurs « headless »

Analyse

Résister aux refontes

Hooks stables, solutions de secours, octets en entrée

Analyseurs

Qualité des données

Faites confiance au résultat

Schéma, motifs de rejet, déduplication

Qualité des données

Surveillance

Priorité aux alertes

Exécuter les métriques, les canaris, les codes de sortie

Surveillance

Évolutivité

Se développer sans chaos

Frontier, points de contrôle transactionnels

Évolutivité

Coût

Rester rentable

Coût par enregistrement valide

Coût

Quelles sont les meilleures pratiques les plus importantes en matière de web scraping ?

Si vous ne devez adopter que dix bonnes pratiques, optez pour celles-ci : rédigez un contrat de données, privilégiez les API et le format JSON plutôt que le HTML, respectez scrupuleusement le fichier robots.txt, identifiez votre bot, réglez la fréquence d'accès à chaque serveur, respectez l'en-tête Retry-After, définissez des délais d'expiration, validez les enregistrements avant de les sauvegarder, signalez les pages vides ou peu fournies, et enregistrez des points de contrôle avec les données qu'ils décrivent.

Chacune de ces dix bonnes pratiques de web scraping renvoie vers la section qui l'explique.

  1. Rédigez un contrat de données qui définit les champs, les types, une clé unique et la date de mise à jour avant même que le code n’existe.
  2. Privilégiez une API officielle ou le format JSON propre à la page plutôt que l’analyse du code HTML.
  3. Appliquez le fichier robots.txt tel qu’il est défini par la RFC 9309, et non selon la manière dont votre bibliothèque le fait.
  4. Identifiez votre bot à l’aide d’un User-Agent honnête et d’une adresse de contact.
  5. Réglez le rythme d'accès pour chaque hôte séparément, en commençant lentement et en vous adaptant au serveur.
  6. Respectez le paramètre Retry-After et réduisez progressivement la fréquence de manière exponentielle en utilisant le jitter.
  7. Définissez des délais d'expiration de connexion et de lecture pour chaque requête.
  8. Vérifiez la validité des enregistrements avant de les sauvegarder et conservez les motifs de chaque rejet.
  9. Signalez les pages vides ou peu fournies, et pas seulement les plantages.
  10. Validez les points de contrôle en même temps que les données qu’ils décrivent, afin qu’un plantage ne laisse jamais une page à moitié enregistrée.

Comment planifier un projet de web scraping ?

Planifiez à rebours à partir des données. Notez les champs, les types, la fraîcheur et le volume dont vous avez besoin, puis choisissez la source fiable la moins chère. Classez les techniques de web scraping par ordre de coût : une API officielle en premier lieu, puis le JSON déjà chargé par la page, ensuite les plans de site pour la découverte, puis le HTML statique, et enfin un navigateur headless uniquement en dernier recours. Établissez un budget avant d’écrire la moindre ligne de code.

Si vous hésitez encore à vous lancer dans le web scraping, commencez par vous poser les questions auxquelles il faut répondre avant d’écrire un scraper.

Rédigez d’abord un contrat de données

Un contrat de données est un petit tableau indiquant précisément ce que le scraper doit fournir. En voici un pour un outil de suivi des prix des livres :

Champ

Type

Obligatoire

Règle

url

chaîne de caractères (absolue)

oui

clé unique

titre

chaîne

oui

non vide

prix

décimal

oui

> 0

devise

Code ISO 4217

oui

par ex. GBP

en_stock

booléen

oui

à partir du texte de disponibilité

note

entier 1–5

non

null autorisé

date_de_récupération

horodatage UTC

oui

défini par le programme de récupération

Le moniteur a besoin de données actualisées quotidiennement, à raison d’environ 1 000 lignes par exécution.

L'analyseur remplit cette exigence, la validation la fait respecter et la surveillance déclenche une alerte lorsqu'un cycle ne répond pas aux critères.

Choisissez la source fiable la moins chère

Parcourez cette liste et arrêtez-vous à la première source qui dispose de tous les champs de votre contrat :

  1. Une API officielle. Lorsqu’un site en propose une, extraire les données via une API est préférable à l’analyse du code HTML : vous bénéficiez de noms de champs stables, de limites documentées et de conditions d’utilisation explicites.
  2. Le JSON que la page charge déjà. Ouvrez les DevTools, filtrez l’onglet Réseau sur « Fetch/XHR », puis rechargez la page. De nombreux sites récupèrent leurs données au format JSON avant de les afficher à l’écran.
  3. Les plans de site, pour la découverte. Utilisez les plans de site XML du site pour découvrir les URL au lieu d’explorer chaque lien. Leurs lastmod dates indiquent également quelles pages ont été modifiées depuis votre dernière analyse.
  4. HTML statique. Récupérez et analysez la page lorsque les données se trouvent dans son code source.
  5. Une page rendue. N’utilisez un navigateur sans interface graphique que lorsque les données n’existent nulle part ailleurs.

Chaque étape supplémentaire entraîne davantage de requêtes, davantage de code d’analyse et davantage de dysfonctionnements lorsque le site change.

Voici l’étape 2 en pratique : la version JavaScript de quotes.toscrape.com charge ses citations à partir d’un point de terminaison JSON paginé, ce qui vous évite d’avoir à toucher au code HTML.

json_endpoint.py

"""Prefer the JSON a page already loads over parsing its HTML."""
import requests

url = "https://quotes.toscrape.com/api/quotes"
page, quotes = 1, []
while True:
    data = requests.get(url, params={"page": page}, timeout=(5, 30),
                        headers={"User-Agent": "acme-quotes-bot/0.1 (+mailto:data@example.com)"}).json()
    quotes += [(q["text"], q["author"]["name"]) for q in data["quotes"]]
    if not data["has_next"]:
        break
    page += 1
print(len(quotes), "quotes from", page, "JSON pages; first:", quotes[0][1])
  • /api/quotes?page=N correspond à la même requête que celle effectuée par la page dans le navigateur, visible dans l'onglet Réseau.
  • has_next C'est la condition d'arrêt, ce qui évite de devoir deviner la dernière page ou de récupérer des pages vides.
  • Il n’y a pas d’analyseur HTML, car les champs sont fournis avec leur nom et leur type.

Estimer le budget avant d’écrire le code

Un budget approximatif permet de déterminer si le projet est même réalisable : nombre de pages × fréquence ÷ taux autorisé = durée d’exécution.

Imaginons que vous ayez besoin de 10 000 pages de produits par jour et que le site tolère une requête toutes les deux secondes. Cela fait 10 000 × 2 = 20 000 secondes, soit environ 5,6 heures d’exploration par jour. Si cela ne convient pas, vous devez réduire le nombre de pages, opter pour un exploration incrémentielle ou utiliser une source contenant davantage d’enregistrements par requête. Accélérer l’exploration n’est pas une option, car c’est le site qui fixe le débit.

Conservez les réponses brutes pendant quelques jours

Stockez le code HTML ou JSON brut que vous récupérez pendant quelques jours, compressé et indexé par URL et heure de récupération. Un sélecteur défectueux ou un nouveau champ implique alors de réanalyser les copies stockées, et non de parcourir à nouveau le site.

Comment extraire légalement et respectueusement les données d’un site web ?

Extrayez uniquement les données publiques que vous avez une raison légitime de collecter, lisez les conditions d'utilisation du site et évitez les zones réservées aux utilisateurs connectés, sauf si vous y êtes autorisé. Récupérez le fichier robots.txt et appliquez-le conformément à la définition de la RFC 9309, identifiez votre bot à l’aide d’une URL de contact, évitez les données personnelles dont vous n’avez pas besoin et ne vous faites jamais passer pour un robot d’indexation de moteur de recherche.

Comment fonctionne concrètement la mise en correspondance du fichier robots.txt ?

Le fichier robots.txt est une norme officielle depuis 2022, date à laquelle l’IETF a publié la RFC 9309, le protocole d’exclusion des robots. Les règles à retenir :

  • Commencez par trouver votre groupe. Les groupes commencent par User-agent lignes. Votre robot d'indexation suit le groupe dont le jeton de produit est mentionné, et le * groupe ne s’applique que si aucun autre ne le fait.
  • La correspondance la plus longue l'emporte. Lorsque plusieurs règles correspondent à un chemin d'accès, c'est la plus spécifique (celle qui comporte le plus de caractères) qui prévaut.
  • En cas d’égalité, c’est « Allow » qui l’emporte. Si un Allow et une Disallow règle correspondent et ont la même longueur, le chemin est autorisé.
  • Deux caractères spéciaux. * correspond à n'importe quelle séquence de caractères et $ marque la fin du chemin, de sorte que Disallow: /*.pdf$ bloque toutes les URL dont le chemin se termine par .pdf.
  • Les codes d'état modifient la signification. Une erreur 4xx sur le fichier robots.txt signifie qu'un robot d'indexation peut accéder à tout. Une erreur 5xx ou une requête ayant échoué signifie qu'il doit considérer que l'accès est totalement interdit.
  • La mise en cache a une limite. Ne conservez pas une copie mise en cache pendant plus de 24 heures.
  • Le paramètre « crawl-delay » ne figure pas dans la norme, et Google l’ignore. Respectez-le néanmoins lorsqu’il est présent, car il indique la volonté du propriétaire du site.

La RFC précise également que ces règles ne constituent pas une forme d’autorisation d’accès : le fichier est une demande publiée par le propriétaire du site, et non un verrou.

Pourquoi votre analyseur de fichier robots.txt peut ne pas respecter la norme

urllib.robotparser, en Python, est le choix évident pour les scrapers Python, mais il vérifie les règles dans l’ordre où elles apparaissent dans le fichier et s’arrête à la première correspondance au lieu de retenir la plus longue. Il traite également les chemins d’accès des règles comme de simples préfixes, donc * et $ n’ont aucune signification particulière.

L'extrait de code ci-dessous analyse un petit fichier robots.txt à l'aide des deux approches et affiche leurs résultats côte à côte. Le « polite fetcher » importe ensuite son parse_robots() et rfc9309_allowed() .

robots_match.py

"""robots.txt per RFC 9309: pick the group for your user agent, then the
longest matching rule wins, Allow wins ties, '*' matches any characters and
'$' anchors the end of the path."""
import re
import urllib.robotparser


def parse_robots(text, user_agent):
    """Return (rules, crawl_delay) for the group that applies to user_agent."""
    groups, current, in_agent_lines = [], None, False
    for raw in text.splitlines():
        line = raw.split("#", 1)[0].strip()
        if ":" not in line:
            continue
        key, value = (part.strip() for part in line.split(":", 1))
        key = key.lower()
        if key == "user-agent":
            if not in_agent_lines:  # a new group starts
                current = {"agents": [], "rules": [], "delay": None}
                groups.append(current)
            current["agents"].append(value.lower())
            in_agent_lines = True
            continue
        in_agent_lines = False
        if current is None:
            continue
        if key in ("allow", "disallow"):
            current["rules"].append((key, value))
        elif key == "crawl-delay":  # not in RFC 9309, but a clear signal
            try:
                current["delay"] = float(value)
            except ValueError:
                pass
    token = user_agent.split("/")[0].lower()  # product token, e.g. "acme-price-monitor"
    matched = [g for g in groups if token in g["agents"]] or \
              [g for g in groups if "*" in g["agents"]]
    rules = [rule for g in matched for rule in g["rules"]]
    delay = next((g["delay"] for g in matched if g["delay"] is not None), None)
    return rules, delay


def _pattern(rule_path):
    regex = re.escape(rule_path).replace(r"\*", ".*")
    if regex.endswith(r"\$"):
        regex = regex[:-2] + "$"
    return re.compile(regex)


def rfc9309_allowed(rules, path):
    """rules: list of ("allow"|"disallow", path) for the group that applies to you."""
    best_len, verdict = -1, True  # no matching rule means allowed
    for kind, rule_path in rules:
        if not rule_path:
            continue  # an empty Disallow allows everything
        if _pattern(rule_path).match(path):
            length = len(rule_path)
            if length > best_len or (length == best_len and kind == "allow"):
                best_len, verdict = length, kind == "allow"
    return verdict


if __name__ == "__main__":
    robots_txt = """User-agent: *
Disallow: /catalogue/
Allow: /catalogue/category/
Disallow: /*.pdf$

User-agent: acme-price-monitor
Disallow: /checkout/
Crawl-delay: 2
"""
    stdlib = urllib.robotparser.RobotFileParser()
    stdlib.parse(robots_txt.splitlines())
    rules, _ = parse_robots(robots_txt, "mybot/1.0")
    for path in ["/catalogue/category/books_1/index.html", "/catalogue/item_1.html",
                 "/files/report.pdf", "/files/report.pdf?v=2", "/index.html"]:
        print(f"{path:42} RFC 9309: {str(rfc9309_allowed(rules, path)):5}  "
              f"urllib.robotparser: {stdlib.can_fetch('mybot', path)}")
    rules, delay = parse_robots(robots_txt, "acme-price-monitor/1.2 (+https://example.com/bot)")
    print("acme-price-monitor group:", rules, "crawl-delay:", delay)
    print("/catalogue/item_1.html for acme-price-monitor:", rfc9309_allowed(rules, "/catalogue/item_1.html"))

Ce que montre la comparaison :

  • /files/report.pdf est autorisé par urllib.robotparser, bien que Disallow: /*.pdf$ cela soit interdit par la norme, car l'analyseur de la bibliothèque standard lit le * littéralement.
  • /catalogue/category/books_1/index.html est bloqué par urllib.robotparser, car Disallow: /catalogue/ apparaît en premier dans le fichier. Selon la norme, c’est le plus long Allow: /catalogue/category/ l’emporte et la page est autorisée.
  • acme-price-monitor dispose de son propre groupe, qui remplace * groupe dans son intégralité. Pour ce robot, /catalogue/item_1.html est autorisé avec un délai d’exploration de deux secondes, car aucune des * règles ne s’appliquent pas.

Utilisez un analyseur syntaxique conforme aux normes, ou au moins testez votre analyseur par rapport aux règles spécifiques dont vous dépendez.

Conditions d’utilisation, identifiants de connexion et données personnelles

Quelques règles couvrent la plupart des situations :

  • Lisez d’abord les conditions d’utilisation. Se connecter revient généralement à les accepter, et beaucoup d’entre elles interdisent l’accès automatisé ; considérez donc les zones authentifiées comme interdites d’accès sans autorisation.
  • Collectez le minimum de données personnelles, voire aucune. Ignorez les noms, adresses e-mail ou détails de profil qui ne figurent pas dans votre contrat de données ; les lois sur la protection des données, telles que le RGPD, s’appliquent aux données personnelles, que vous les ayez extraites ou non.
  • Ne contournez pas les barrières payantes, les identifiants de connexion ou les CAPTCHA. Ils indiquent que le propriétaire souhaite limiter l’accès.

Il s’agit ici de conseils pratiques, et non de conseils juridiques. Pour plus de détails juridiques, consultez notre guide sur la légalité du web scraping.

Identifiez votre outil de scraping de manière honnête

Utilisez un User-Agent stable qui indique le nom de votre produit, sa version et un moyen de vous contacter :

acme-price-monitor/1.2 (+https://example.com/bot; data@example.com)

Un propriétaire de site qui constate un trafic inhabituel provenant de cette chaîne de caractères peut vous contacter au lieu de bloquer une plage d’adresses IP, et les auteurs du fichier robots.txt disposent d’un identifiant de produit à cibler.

N’envoyez jamais un User-Agent « Googlebot ». Google explique comment les propriétaires de sites peuvent vérifier ses robots d’indexation à l’aide de recherches DNS directes et inversées ou de ses plages d’adresses IP publiées ; un User-Agent falsifié échouera donc à la vérification et paraîtra trompeur une fois détecté.

À quelle vitesse pouvez-vous effectuer un scraping sans surcharger un site web ?

Il n’existe pas de rythme universel sans risque. Commencez par une ou deux requêtes simultanées par hôte avec une pause d’environ une seconde, puis laissez le serveur dicter le rythme : ralentissez lorsque la latence augmente, arrêtez-vous et attendez si vous obtenez un code d’erreur 429 ou 503, respectez l’en-tête Retry-After et réduisez progressivement le rythme de manière exponentielle en cas de jitter.

Définissez votre budget par hôte, et non par scraper

Dix workers qui s’interrompent poliment les uns après les autres peuvent tout de même envoyer dix requêtes parallèles vers un même domaine. Réglez le rythme au niveau par lequel passe chaque requête vers un hôte : un objet « fetcher » ou un limiteur partagé entre les machines.

Laissez le serveur réguler le rythme

Une latence croissante signifie que le serveur est sollicité davantage ; augmentez donc le délai avant que les erreurs ne commencent à se produire. Planifiez les gros crawls pendant les heures creuses du site, généralement la nuit selon le fuseau horaire de son marché principal.

Gérez correctement les codes 429 et Retry-After

Le code 429 « Too Many Requests » (RFC 6585) indique que vous avez envoyé trop de requêtes dans un laps de temps donné, tandis qu’un code 503 signifie que le serveur est surchargé ou hors service pour maintenance. Ces deux codes peuvent comporter l’en-tête « Retry-After », et la sémantique HTTP (RFC 9110) autorise deux formats pour sa valeur :

Retry-After: 120
Retry-After: Fri, 09 Oct 2026 07:30:00 GMT

le premier en secondes et le second sous forme de date HTTP ; un scraper qui ne gère qu’un seul de ces formats plantera ou ignorera l’en-tête s’il rencontre l’autre. Considérez cette valeur comme un délai d’attente minimum.

Appliquez un délai d’attente exponentiel, avec une variation aléatoire

En l’absence d’en-tête « Retry-After », ou si une nouvelle tentative échoue, prolongez le délai d’attente à chaque fois :

wait = min(cap, base × 2^attempt) × random(0.5 to 1.5)

Utilisez la plus grande des deux valeurs : ce délai d’attente ou la valeur de « Retry-After ». Le facteur aléatoire est la variation : sans elle, les workers ayant échoué simultanément réessaient ensemble et sollicitent le serveur en une seule vague synchronisée.

Ne réessayez que ce qui peut l’être

Réessayez les réponses 429, 500, 502, 503 et 504 ainsi que les délais d’attente réseau sur les requêtes GET idempotentes. Ne réessayez pas en cas de 404, puisque la page ne reviendra pas, ni ne martelez une 403, puisque la même requête obtient la même réponse. Limitez le nombre de tentatives et consignez chaque abandon afin que les URL bloquées apparaissent dans vos métriques.

Un gestionnaire de requêtes « poli » dans une seule classe

La PoliteFetcher classe ci-dessous combine cette section et la suivante, en important parse_robots() et rfc9309_allowed() de l'extrait de robots.txt ; veillez donc à conserver ces deux fichiers dans un même dossier.

polite_fetch.py

"""A polite, resilient fetcher: robots.txt, per-host pacing, timeouts,
retries with backoff and jitter, Retry-After, and conditional requests."""
import email.utils
import random
import time
from urllib.parse import urlsplit

import requests

from robots_match import parse_robots, rfc9309_allowed  # Snippet 2

USER_AGENT = "acme-price-monitor/1.2 (+https://example.com/bot; data@example.com)"
RETRYABLE = {429, 500, 502, 503, 504}
ROBOTS_TTL = 24 * 3600  # RFC 9309: don't trust a cached copy for more than 24 h


class PoliteFetcher:
    def __init__(self, min_delay=1.0, max_retries=4, timeout=(5, 30)):
        self.session = requests.Session()
        self.session.headers["User-Agent"] = USER_AGENT
        self.min_delay, self.max_retries, self.timeout = min_delay, max_retries, timeout
        self.robots, self.last_hit, self.validators = {}, {}, {}

    def _robots(self, host):
        cached = self.robots.get(host)
        if cached and time.monotonic() - cached[0] < ROBOTS_TTL:
            return cached[1], cached[2]
        try:
            resp = self.session.get(host + "/robots.txt", timeout=self.timeout)
        except requests.RequestException:
            resp = None
        if resp is None or resp.status_code >= 500:
            rules, delay = [("disallow", "/")], None  # unreachable: assume full disallow
        elif resp.status_code >= 400:
            rules, delay = [], None  # unavailable (e.g. 404): no restrictions
        else:
            rules, delay = parse_robots(resp.text, USER_AGENT)
        self.robots[host] = (time.monotonic(), rules, delay)
        return rules, delay

    def allowed(self, url):
        parts = urlsplit(url)
        rules, _ = self._robots(f"{parts.scheme}://{parts.netloc}")
        path = (parts.path or "/") + (f"?{parts.query}" if parts.query else "")
        return rfc9309_allowed(rules, path)

    def _pace(self, host):
        _, crawl_delay = self._robots(host)
        delay = max(self.min_delay, crawl_delay or 0)
        wait = self.last_hit.get(host, 0) + delay - time.monotonic()
        if wait > 0:
            time.sleep(wait)
        self.last_hit[host] = time.monotonic()

    @staticmethod
    def _retry_after(value):
        if not value:
            return None
        if value.isdigit():
            return int(value)
        when = email.utils.parsedate_to_datetime(value)  # HTTP-date form
        return max(0, when.timestamp() - time.time())

    def get(self, url):
        if not self.allowed(url):
            raise PermissionError(f"robots.txt disallows {url}")
        parts = urlsplit(url)
        host = f"{parts.scheme}://{parts.netloc}"
        headers = {}
        etag, modified = self.validators.get(url, (None, None))
        if etag:
            headers["If-None-Match"] = etag
        if modified:
            headers["If-Modified-Since"] = modified
        for attempt in range(self.max_retries + 1):
            self._pace(host)
            try:
                resp = self.session.get(url, headers=headers, timeout=self.timeout)
            except (requests.ConnectionError, requests.Timeout):
                resp = None
            if resp is not None and resp.status_code not in RETRYABLE:
                if resp.status_code == 200:
                    self.validators[url] = (resp.headers.get("ETag"),
                                            resp.headers.get("Last-Modified"))
                return resp  # 200, 304, or a non-retryable error for the caller
            if attempt == self.max_retries:
                break
            backoff = min(60, 2 ** attempt) * random.uniform(0.5, 1.5)
            server_hint = self._retry_after(resp.headers.get("Retry-After")) if resp is not None else None
            time.sleep(max(backoff, server_hint or 0))
        raise RuntimeError(f"gave up on {url} after {self.max_retries + 1} attempts")


if __name__ == "__main__":
    f = PoliteFetcher(min_delay=1.0, max_retries=2)
    for url in ["https://books.toscrape.com/", "https://httpbin.org/etag/abc123",
                "https://httpbin.org/etag/abc123", "https://httpbin.org/status/404"]:
        r = f.get(url)
        print(r.status_code, len(r.content), "bytes", url)
    for url in ["https://httpbin.org/deny", "https://httpbin.org/status/503"]:
        try:
            f.get(url)
        except (PermissionError, RuntimeError) as exc:
            print("expected:", exc)

Les lignes clés :

  • _robots() récupère le fichier robots.txt avec votre propre User-Agent, considère un code 4xx comme « aucune restriction » et un code 5xx ou une erreur réseau comme « accès refusé », puis effectue une nouvelle requête dès que la copie mise en cache date de plus de 24 heures.
  • _pace() applique le délai par hôte et utilise le Crawl-delay du site lorsqu’il est supérieur à votre délai minimum.
  • RETRYABLE limite les tentatives en fonction des limites de débit et des erreurs serveur. Une erreur 404 est directement renvoyée à l’appelant.
  • _retry_after() gère à la fois les formats en secondes et en date HTTP, et la boucle attend la durée la plus longue entre le délai de repli et l’indication du serveur.

La démo ci-dessous obtient un code 304 lors d’une requête répétée, renvoie un 404 sans nouvelle tentative, refuse un chemin interdit par le fichier robots.txt avant de l’envoyer, et abandonne face à un 503 permanent après avoir atteint le nombre maximal de tentatives.

Quelles habitudes de requête permettent d'éviter la plupart des échecs de scraping ?

La plupart des échecs proviennent de requêtes mal conçues, et non de défenses sophistiquées. Définissez toujours des délais d’expiration pour la connexion et la lecture, réutilisez une seule session par hôte, envoyez un ensemble d’en-têtes cohérent et honnête, acceptez la compression, ignorez les images et les polices dont vous n’avez pas besoin, normalisez les URL pour éviter les doublons, et utilisez des requêtes conditionnelles afin que les pages inchangées renvoient un code 304 vide.

  • Définissez des délais d’expiration distincts pour la connexion et la lecture sur chaque requête, par exemple 5 secondes et 30 secondes. Dans requests, un appel sans délai d’attente peut rester en attente indéfiniment et bloquer un worker.
  • Utilisez un requests.Session par hôte. Cela permet de réutiliser les connexions, d’éviter les poignées de main TLS répétées et d’alléger la charge sur le serveur.
  • Envoyez le même ensemble d’en-têtes, réaliste et honnête, à chaque requête, avec un User-Agent qui vous identifie. Notre guide des en-têtes HTTP pour le web scraping explique quels en-têtes sont importants et dans quel ordre.
  • Laissez la compression activée. requests demande le format gzip par défaut ; si vous créez vos en-têtes manuellement, conservez Accept-Encoding, car le HTML compressé ne représente qu’une fraction de la taille transmise sur le réseau.
  • Nettoyez les URL avant qu’elles n’entrent dans votre file d’attente : supprimez les paramètres de suivi et #fragments, suivez rel="canonical" liens, et dédupliquez.
  • Ignorez les pixels de suivi et les vues de pagination en double ; ne récupérez que ce que vous allez analyser.

Mettez les pages en cache et utilisez des requêtes conditionnelles

Lorsqu’une réponse contient un ETag ou Last-Modified , enregistrez-le et renvoyez-le la prochaine fois sous la forme If-None-Match ou If-Modified-Since. Grâce à ces requêtes conditionnelles, une page inchangée reçoit un code d’état 304 Not Modified sans corps, ce qui vous permet d’économiser de la bande passante et allège la charge de travail du site.

PoliteFetcher.get() le fait déjà : son validators dictionnaire stocke l’en-tête ETag et Last-Modified des 200 dernières réponses et les renvoie lors de la prochaine requête. Une réponse 304 est renvoyée telle quelle, ce qui permet à votre code de réutiliser la copie stockée.

Que faire lorsqu’un site web commence à bloquer votre robot de collecte de données ?

Effectuez un diagnostic avant d’intensifier vos efforts. Analysez le code d’erreur (403, 429, 503, ou un 200 qui renvoie un CAPTCHA ou une page vide), vérifiez d’abord votre propre débit et vos en-têtes, puis procédez étape par étape : ralentissez, corrigez les en-têtes, ajoutez des proxys testés, affichez la page, et enfin confiez les cibles difficiles à une API de scraping gérée.

Code d'erreur

Cause probable

Première réponse

429 Trop de requêtes

Limite de débit

Respecter le paramètre « Retry-After » ; réduire le nombre de requêtes simultanées par hôte

503 avec « Retry-After »

Surcharge ou maintenance

Attendre le délai indiqué ; réduire la fréquence des requêtes

403 Accès interdit

Adresse IP bloquée, en-tête non valide ou connexion requise

Vérifiez les en-têtes et les conditions ; n'enchaînez pas les tentatives

200 avec une page de vérification ou un CAPTCHA

Défense contre les bots

Considérez cela comme un échec de validation ; ralentissez, puis remontez le problème

Code 200 avec une coque de données vide

Contenu rendu par JavaScript

Rechercher l'appel JSON ; afficher uniquement si nécessaire

Augmentation des délais d'expiration

Surcharge du serveur ou limitation du débit

Allongez les délais ; réduisez la concurrence

Vous ne comprenez pas la signification d'un code ? Consultez notre liste des erreurs de proxy et des codes d'état HTTP.

Les blocages « mous » sont les plus dangereux : une réponse 200 renvoyant une page de vérification ou ne contenant aucune donnée peut sembler indiquer une réussite, et seules les vérifications des données décrites plus loin dans ce guide permettent de les détecter.

Les sites bloquent les robots de scraping car un trafic automatisé intense sollicite excessivement les serveurs et peut nuire à l’activité. La liste des menaces automatisées de l’OWASP mentionne le scraping comme une menace à part entière (OAT-011), et la plupart des défenses ciblent le volume et les schémas d’abus ; ainsi, un trafic courtois et bien identifié déclenche moins de ces mécanismes.

Procédez dans l’ordre suivant :

  1. Ralentissez et réduisez la concurrence sur l’hôte concerné.
  2. Corrigez vos en-têtes afin qu’ils soient cohérents et complets.
  3. Ajoutez des proxys, en commençant par choisir le type de proxy adapté à la cible. Testez vos proxys avant de les déployer à grande échelle, puis répartissez la charge à l’aide de proxys rotatifs.
  4. Affichez la page dans un navigateur sans interface graphique si les données n’apparaissent qu’après l’exécution de JavaScript.
  5. Confiez les cibles les plus difficiles à une API de scraping gérée.

Pour la reconnaissance d'empreintes numériques, les CAPTCHA et les navigateurs furtifs, utilisez notre guide pratique pour effectuer du scraping sans être bloqué.

Quand avez-vous réellement besoin d’un navigateur sans interface utilisateur ?

Uniquement lorsque les données ne se trouvent pas dans le code HTML ou dans un appel JSON effectué par la page. Comparez le code source de la page avec ce qu’affiche DevTools ; si les données arrivent via une requête XHR, appelez directement ce point de terminaison. N’affichez que les pages qui nécessitent réellement JavaScript, bloquez les images et les polices, et réutilisez les contextes de navigateur, car l’affichage est bien plus lent et coûteux.

Un navigateur sans interface utilisateur exécute le JavaScript de la page sans fenêtre visible : c’est la méthode la plus complète pour récupérer une page, mais aussi la plus coûteuse. Deux tests rapides vous permettront de déterminer si vous en avez besoin :

  • Comparez la vue « Source » avec le panneau « Éléments ». Recherchez dans le code source de la page (Ctrl+U ou Cmd+Option+U) une valeur qui vous intéresse, comme un prix. Si elle s’y trouve, une simple requête HTTP suffit ; si seul le panneau « Éléments » de DevTools l’affiche, c’est que JavaScript l’a générée.
  • Vérifiez dans l’onglet « Réseau » s’il existe une requête Fetch/XHR transportant les données, comme l’exemple de point de terminaison JSON ci-dessus, et appelez-la directement.

Si vous avez besoin du rendu, envoyez uniquement au navigateur les URL qui échouent à ces tests, et bloquez les images, les polices et les médias. Avec Playwright ou un outil similaire, lancez un seul processus de navigateur avec un nouveau contexte par tâche, plutôt que d’ouvrir un nouveau navigateur par page.

Les utilisateurs de Scrapy peuvent n’exécuter le JavaScript que pour les requêtes qui en ont besoin et laisser le reste de l’exploration se faire via le téléchargeur HTTP rapide de Scrapy.

Comment écrire des analyseurs syntaxiques qui résistent aux changements de mise en page ?

Ancrer les sélecteurs à des éléments qui changent rarement (identifiants, attributs de données, structure sémantique, JSON-LD intégré) plutôt qu’à des classes de style, conserver une liste ordonnée de sélecteurs de secours par champ, décoder correctement les octets bruts et signaler clairement l’échec en cas de champs obligatoires manquants. Un analyseur qui renvoie silencieusement des champs vides est pire qu’un analyseur qui plante.

Choisissez des sélecteurs qui résisteront à une refonte

Les classes de style telles que .text-lg.font-bold changent dès qu’un graphiste modifie la page. Les identifiants, data-* les attributs, itemprop les microdonnées, les balises sémantiques et les blocs JSON-LD changent beaucoup moins souvent, car d’autres parties du code en dépendent. Veillez également à ce que les sélecteurs restent courts, car un chemin complet partant de <body> ne fonctionne plus dès qu’une balise div contenante est déplacée.

Les sélecteurs CSS sont plus faciles à lire, tandis que l’XPath permet de sélectionner par texte et de remonter l’arborescence. L’article « XPath vs sélecteurs CSS » présente les avantages et les inconvénients de chaque approche.

Prévoyez des solutions de repli classées par ordre de priorité pour chaque champ

Attribuez à chaque champ une liste de sélecteurs, en commençant par le plus spécifique, et retenez le premier qui correspond. Le SELECTORS dictionnaire de l’extrait ci-dessous fonctionne de cette manière.

Décodez les octets, ne vous fiez pas au texte deviné

Faites attention au bug d’encodage « £ ». Si un serveur envoie Content-Type: text/html sans spécifier de jeu de caractères, requests il se rabat sur ISO-8859-1 lors de la génération resp.text, ce qui fait qu’une page en UTF-8 est mal décodée : « £ » devient « £ », et aucun prix ne peut être analysé. Transmettez resp.contentles octets bruts à votre analyseur ; BeautifulSoup lira le <meta charset>.

Testez sur des pages enregistrées

Enregistrez quelques pages réelles comme cas de test. Lorsqu’un sélecteur change, analysez à nouveau les réponses brutes stockées et comparez les résultats avant et après.

Cet extrait de code transforme les fiches de livres d’une page du catalogue books.toscrape.com en enregistrements typés, valide chacune d’entre elles et affiche un rapport de qualité, puis transmet à l’analyseur une fiche dont la classe de prix a été renommée afin de montrer qu’un changement de mise en page a été détecté.

parse_validate.py

"""Parse defensively, normalize, validate, and report data quality."""
import json
from dataclasses import dataclass, asdict
from datetime import datetime, timezone
from decimal import Decimal, InvalidOperation
from urllib.parse import urljoin

import requests
from bs4 import BeautifulSoup

# Ordered fallbacks: the first selector that matches wins.
SELECTORS = {
    "title": ["h3 a[title]", "h3 a"],
    "price": ["p.price_color", "[itemprop=price]"],
    "availability": ["p.availability", ".instock"],
    "url": ["h3 a[href]"],
}
RATINGS = {"One": 1, "Two": 2, "Three": 3, "Four": 4, "Five": 5}


@dataclass
class Book:
    url: str
    title: str
    price: Decimal
    currency: str
    in_stock: bool
    rating: int | None
    fetched_at: str  # provenance: when this record was fetched (UTC)


def first(card, field):
    for css in SELECTORS[field]:
        node = card.select_one(css)
        if node is not None:
            return node
    return None


def parse_card(card, page_url, fetched_at=""):
    errors = []
    link, title_node = first(card, "url"), first(card, "title")
    price_node, stock_node = first(card, "price"), first(card, "availability")
    title = (title_node.get("title") or title_node.get_text(strip=True)) if title_node else ""
    if not title:
        errors.append("title missing")
    raw_price = price_node.get_text(strip=True) if price_node else ""
    currency = "GBP" if "£" in raw_price else "UNKNOWN"
    try:
        price = Decimal(raw_price.replace("£", "").replace(",", ""))
        if price <= 0:
            errors.append("price not positive")
    except InvalidOperation:
        price = Decimal("NaN")
        errors.append(f"price unparseable: {raw_price!r}")
    rating_node = card.select_one("p.star-rating")
    rating = next((RATINGS[c] for c in (rating_node.get("class", []) if rating_node else [])
                   if c in RATINGS), None)
    if link is None:
        errors.append("url missing")
    book = Book(url=urljoin(page_url, link["href"]) if link else "", title=title, price=price,
                currency=currency, in_stock="in stock" in (stock_node.get_text(" ", strip=True).lower()
                                                          if stock_node else ""),
                rating=rating, fetched_at=fetched_at)
    return book, errors


def scrape(page_url):
    resp = requests.get(page_url, timeout=(5, 30),
                        headers={"User-Agent": "acme-catalogue-bot/0.1 (+mailto:data@example.com)"})
    resp.raise_for_status()
    fetched_at = datetime.now(timezone.utc).isoformat(timespec="seconds")
    # Pass bytes, not resp.text: with no charset in Content-Type, requests guesses
    # ISO-8859-1 and "£" becomes "£". BeautifulSoup reads the page's <meta charset>.
    soup = BeautifulSoup(resp.content, "html.parser")
    cards = soup.select("article.product_pod")
    seen, good, rejected = set(), [], []
    for card in cards:
        book, errors = parse_card(card, page_url, fetched_at)
        if book.url in seen:
            errors.append("duplicate url")
        seen.add(book.url)
        (rejected if errors else good).append((book, errors))
    report = {
        "page": page_url,
        "cards_found": len(cards),
        "valid": len(good),
        "rejected": len(rejected),
        "missing_rating": sum(1 for b, _ in good if b.rating is None),
        "sample": {k: str(v) for k, v in asdict(good[0][0]).items()} if good else None,
    }
    return good, rejected, report


if __name__ == "__main__":
    _, rejected, report = scrape("https://books.toscrape.com/catalogue/page-1.html")
    print(json.dumps(report, indent=2))
    for book, errors in rejected:
        print("REJECTED", book.url, errors)
    # Simulate a layout change: the price class is renamed.
    html = '<article class="product_pod"><h3><a href="x.html" title="T">T</a></h3><p class="cost">£1.00</p></article>'
    card = BeautifulSoup(html, "html.parser").select_one("article")
    print("after layout change:", parse_card(card, "https://books.toscrape.com/")[1])
  • first() parcourt la liste de repli de chaque champ, afin qu’une classe renommée ne vide pas une colonne.
  • BeautifulSoup(resp.content, ...) transmet des octets, ce qui évite le bug d’encodage mentionné ci-dessus.
  • parse_card() collecte les erreurs au lieu de les lever, afin qu’une fiche incorrecte n’interrompe pas le traitement de la page et que chaque rejet soit justifié.
  • Le changement de mise en page simulé renvoie « prix non analysable » au lieu d’un prix vide, ce qui correspond au signal que vous souhaitez voir apparaître dans vos journaux.

Pour en savoir plus sur la conversion de pages brutes en champs, consultez notre guide d’analyse des données.

Comment vérifier la qualité des données extraites ?

Considérez chaque enregistrement comme non fiable jusqu’à ce qu’il respecte un schéma : champs obligatoires présents, types corrects, plages valides et clé unique. Utilisez le type « Decimal » pour les montants, normalisez les devises et les dates, dédupliquez sur la clé, stockez l’URL source et l’heure de récupération, et générez un rapport par exécution répertoriant les valeurs valides, rejetées et manquantes.

La qualité des données est le point faible des scrapers : la récupération réussit, l’analyseur s’exécute, et des valeurs erronées se glissent dans les rapports pendant des semaines. Ces bonnes pratiques de scraping de données permettent d’y remédier :

  • Définissez le schéma dans le code. La Book dataclass dans l’extrait de code d’analyse constitue le contrat de données en Python ; pydantic ajoute des vérifications supplémentaires, mais une dataclass ne nécessite aucune dépendance supplémentaire.
  • Préférez le rejet à la conversion forcée. Lorsqu’un prix ne peut pas être analysé, rejetez l’enregistrement en indiquant le motif plutôt que de stocker un zéro ; les motifs de rejet constituent votre premier signal d’alerte en cas de modification de la structure.
  • Stockez les montants en Decimal, jamais sous forme de nombre à virgule flottante, car ceux-ci ne peuvent pas représenter la plupart des prix avec exactitude. Normalisez les devises selon les codes ISO 4217 et les horodatages selon l’UTC.
  • Surveillez les taux de valeurs nulles au niveau des champs pour détecter les écarts par rapport à la dernière exécution.
  • Dédupliquez en fonction de l’URL canonique ou d’une clé métier telle qu’un SKU. Les doublons indiquent généralement une boucle de pagination ou le même article répertorié dans deux catégories.
  • Attribuez à chaque ligne un source_url et fetched_at, afin que chaque valeur puisse être rattachée à sa page et à son instant.
  • Générez un rapport de qualité par exécution : cartes trouvées, valides, rejetées et champs facultatifs manquants. L'extrait de code d'analyse affiche exactement ces informations, et la section suivante les surveille.

Vérifiez

Exemple de règle

Détecte

Champs obligatoires

présence du titre et du prix

Modifications de mise en page

Types

Le prix est interprété comme une valeur décimale

Erreurs d'encodage, sélecteur incorrect

Plages

0 < prix < 10 000

Confusion entre unités ou devises

Unique

une ligne par URL

Boucles de pagination, doublons

Taux de valeurs nulles

les notes nulles ne doivent pas dépasser 5 points au-dessus du dernier cycle

Défauts partiels

Provenance

Les champs source_url et fetched_at sont définis

Enregistrements introuvables

Comment surveiller un scraper web en production ?

Suivez quelques indicateurs de fonctionnement et déclenchez des alertes en cas de changement, pas seulement en cas de plantage : taux de réussite par code d’état, nombre d’enregistrements par page, pages vides, taux de champs nuls, tentatives de réessai, latence, actualité des données et coût par enregistrement valide. Configurez la sortie de la tâche pour qu’elle ne soit pas nulle lorsque les seuils sont dépassés, afin que ce soit votre planificateur, et non un client, qui s’en aperçoive en premier.

Un scraper qui plante est facile à repérer ; celui qui collecte discrètement des données erronées ne l’est pas. Considérez les seuils ci-dessous comme des points de départ pour l’optimisation, et non comme des références absolues.

Indicateur

Pourquoi c’est important

Exemple d’alerte

Taux de réussite par code d’état

Détecte les blocages et les interruptions de service

La part des codes 2xx passe en dessous de 95 %

Nombre d'enregistrements par page

Détecte les modifications de mise en page

Inférieure à 50 % de la médiane sur 7 jours

Pages vides

Erreurs silencieuses

Toute page comportant 0 enregistrement

Taux de champs nuls

Erreur partielle

Hausse de 10 points par rapport à la dernière exécution

Nombre de tentatives par requête

Limitation

Moyenne supérieure à 0,3

Latence p95

Charge du serveur

Double par rapport à la valeur de référence, donc ralentissement

Actualité

Sessions manquées

Données plus anciennes que votre cible

Coût par enregistrement valide

Dérive budgétaire

Hausse de 25 % d'une semaine sur l'autre

Deux habitudes permettent de tirer davantage parti de ces indicateurs :

  • Récupérez à chaque exécution quelques URL « canari » dont les valeurs sont connues, comme un produit dont le titre est fixe. Un « canari » erroné signifie que l’analyseur syntaxique a échoué, et non que les données sont erronées.
  • Définissez des alertes sur les écarts plutôt que sur des chiffres fixes. Une baisse du nombre d’enregistrements par page par rapport à la médiane sur 7 jours constitue un meilleur indicateur de changement de mise en page que n’importe quel nombre absolu.

L'extrait de code d'exploration de la section suivante se termine par une health() fonction qui signale les erreurs de récupération, les pages vides et les pages trop légères, puis se termine avec le statut 1. Les planificateurs tels que cron, systemd, Airflow et les exécuteurs CI traitent cela comme un échec ; l’alerte provient donc des outils que vous utilisez déjà.

Pour la planification, la livraison et la relecture, découvrez comment créer des tâches de scraping que vous pouvez restaurer.

Comment faire évoluer le scraping web sans le perturber ?

Faites évoluer la gestion administrative avant les travailleurs, car les techniques avancées de scraping Web, comme l’exploration distribuée, en dépendent. Conservez une liste d’URL dédupliquée, validez les enregistrements de chaque page et son indicateur « terminé » en une seule transaction, effectuez des écritures idempotentes afin que les réexécutions ne dupliquent pas les lignes, appliquez des limites de débit par domaine à tous les workers, et effectuez l’exploration de manière incrémentielle à l’aide de plans de site et de requêtes conditionnelles plutôt que de récupérer à nouveau l’intégralité du contenu.

Les bonnes pratiques en matière de web scraping évolutif concernent principalement la gestion de l’état, car un plus grand nombre de workers amplifie les problèmes liés au suivi de la progression.

  • Utilisez une « frontière » plutôt qu’une liste : une file d’attente d’URL dédupliquées qui permet à l’exploration de découvrir de nouvelles pages au fur et à mesure. Vous découvrez les frontières ? Commencez par l’exploration Web en Python.
  • Rendez les points de contrôle transactionnels. Validez les enregistrements d’une page, les liens découverts et le drapeau « terminé » en une seule transaction, de sorte qu’en cas de plantage, soit tout le travail sur cette page soit conservé, soit rien ne soit conservé.
  • Écrivez de manière idempotente. Les clés primaires ou les « upserts » font en sorte qu’une écriture en double n’ait aucun effet, ce qui rend les réexécutions et les nouvelles tentatives sûres.
  • Reprenez plutôt que de redémarrer. L’exécution suivante reprend à partir de la première URL inachevée.

Pour voir cela en action, explorez quotes.toscrape.com à l’aide de l’extrait de code ci-dessous, qui gère une frontière SQLite et valide le travail de chaque page en une seule transaction. Exécutez python resumable_crawl.py 3 pour vous arrêter après trois pages, puis relancez python resumable_crawl.py à nouveau : la deuxième exécution reprend là où la première s’est arrêtée, sans lignes en double.

resumable_crawl.py

"""Resumable pagination crawl with a SQLite checkpoint and a run health check."""
import json
import sqlite3
import sys
import time
from urllib.parse import urljoin

import requests
from bs4 import BeautifulSoup

START = "https://quotes.toscrape.com/page/1/"
HEADERS = {"User-Agent": "acme-quotes-bot/0.1 (+mailto:data@example.com)"}


def open_db(path="crawl.db"):
    db = sqlite3.connect(path)
    db.executescript("""
        CREATE TABLE IF NOT EXISTS frontier (url TEXT PRIMARY KEY, done INTEGER DEFAULT 0);
        CREATE TABLE IF NOT EXISTS quotes (text TEXT, author TEXT, tags TEXT,
                                           page TEXT, PRIMARY KEY (text, author));
    """)
    db.execute("INSERT OR IGNORE INTO frontier(url) VALUES (?)", (START,))
    db.commit()
    return db


def crawl(db, max_pages):
    stats = {"pages": 0, "errors": 0, "rows": 0, "empty_pages": 0, "rejected": 0}
    while stats["pages"] < max_pages:
        row = db.execute("SELECT url FROM frontier WHERE done = 0 LIMIT 1").fetchone()
        if row is None:
            break  # frontier exhausted
        url = row[0]
        try:
            resp = requests.get(url, headers=HEADERS, timeout=(5, 30))
            resp.raise_for_status()
        except requests.RequestException:
            stats["errors"] += 1
            break  # leave it undone; the next run resumes here
        soup = BeautifulSoup(resp.content, "html.parser")
        quotes = soup.select("div.quote")
        stats["empty_pages"] += not quotes
        with db:  # rows, link discovery and the checkpoint commit together
            for q in quotes:
                text, author = q.select_one("span.text"), q.select_one("small.author")
                if text is None or author is None:
                    stats["rejected"] += 1  # malformed card: reject it, don't coerce
                    continue
                cur = db.execute(
                    "INSERT OR IGNORE INTO quotes VALUES (?, ?, ?, ?)",
                    (text.get_text(strip=True), author.get_text(strip=True),
                     ",".join(t.get_text(strip=True) for t in q.select("a.tag")), url))
                stats["rows"] += cur.rowcount
            nxt = soup.select_one("li.next a[href]")
            if nxt:
                db.execute("INSERT OR IGNORE INTO frontier(url) VALUES (?)",
                           (urljoin(url, nxt["href"]),))
            db.execute("UPDATE frontier SET done = 1 WHERE url = ?", (url,))
        stats["pages"] += 1
        time.sleep(1.0)  # pace requests to one host
    return stats


def health(stats, db, min_rows_per_page=5):
    problems = []
    if stats["errors"]:
        problems.append(f"{stats['errors']} fetch errors")
    if stats["empty_pages"]:
        problems.append(f"{stats['empty_pages']} pages had no records (layout change?)")
    if stats["rejected"]:
        problems.append(f"{stats['rejected']} cards missing text or author")
    if stats["pages"] and stats["rows"] / stats["pages"] < min_rows_per_page:
        problems.append("rows per page below threshold")
    total = db.execute("SELECT COUNT(*) FROM quotes").fetchone()[0]
    pending = db.execute("SELECT COUNT(*) FROM frontier WHERE done = 0").fetchone()[0]
    return {**stats, "total_rows": total, "pending_urls": pending, "problems": problems}


if __name__ == "__main__":
    max_pages = int(sys.argv[1]) if len(sys.argv) > 1 else 100
    db = open_db()
    report = health(crawl(db, max_pages), db)
    print(json.dumps(report))
    sys.exit(1 if report["problems"] else 0)
  • with db: encapsule les citations insérées, le lien vers la page suivante nouvellement trouvé et la done = 1 mise à jour dans une seule transaction.
  • INSERT OR IGNORE sur les clés primaires rend chaque écriture idempotente, tant pour les URL de frontier que pour les citations.
  • Un échec de récupération laisse son URL non traitée et interrompt l’exécution ; ainsi, l’exécution suivante réessaie en premier lieu cette URL.
  • Les fiches auxquelles il manque une citation ou un auteur sont ignorées et comptabilisées, plutôt que d’être enregistrées à moitié vides.
  • health() transforme les compteurs de l’exécution en une liste de problèmes, et sys.exit(1) rend ces problèmes visibles pour tout ce qui planifie la tâche.

Une fois la gestion comptable solidement établie, trois autres pratiques permettent d’augmenter la capacité en toute sécurité :

  • Partagez les limites de débit par domaine. Sur plusieurs machines, la régulation locale ne protège plus le site ; conservez donc un budget par domaine dans un emplacement partagé, tel qu’un « token bucket » dans Redis.
  • Effectuez l’exploration de manière incrémentielle. Utilisez les dates du plan du site lastmod pour ne mettre en file d'attente que les URL nouvelles ou modifiées, et recourir à des requêtes conditionnelles pour transformer les pages inchangées en codes d'état 304 vides.
  • Passez à l’horizontalisation en dernier. N’ajoutez des workers qu’une fois que les limites par hôte sont appliquées de manière centralisée ; ainsi, un plus grand nombre de workers signifie davantage d’hôtes en parallèle, et non une pression accrue sur un seul.

Comment maîtriser les coûts liés au web scraping ?

Mesurez le coût par enregistrement valide, et non le coût par requête. La plupart des économies proviennent d’une réduction du nombre de requêtes : mettez en cache et utilisez des requêtes conditionnelles, privilégiez les points de terminaison JSON, effectuez le rendu uniquement lorsque cela est nécessaire, limitez les tentatives de connexion, n’utilisez des proxys que pour les hôtes qui en ont besoin, et effectuez un crawling incrémental. Comparez ensuite honnêtement ce chiffre à celui d’une API de scraping gérée avant de développer davantage.

La formule est simple :

cost per valid record = (infrastructure + proxies + engineering hours) ÷ valid records delivered

Les heures d’ingénierie vont au numérateur, car un scraper qui nécessite l’intervention d’un développeur chaque semaine n’est pas bon marché. Le dénominateur ne compte que les enregistrements ayant passé la validation.

Levier

Effet de levier

Voir

Requêtes conditionnelles et mise en cache

Les pages inchangées renvoient un code 304 vide

Habitudes de requête

Points de terminaison JSON plutôt que HTML

Moins de requêtes, analyse plus simple

Planification

Affichage uniquement lorsque cela est nécessaire

Évite les coûts liés au navigateur sur les pages statiques

Navigateurs « headless »

Limites de tentatives et délai d'attente

Empêche les « tempêtes de tentatives »

Taux de requêtes

Proxys uniquement lorsque cela est nécessaire

Ne payer les adresses IP que pour les hôtes qui bloquent

Blocages

Exploration incrémentielle

Récupération des URL nouvelles ou modifiées uniquement

Évolutivité

Validation avant stockage

Ne payez pas pour stocker ou transmettre des lignes erronées

Qualité des données

Développer ou acheter

Développer en interne est judicieux pour quelques cibles stables et peu protégées. Lorsque les proxys, les tentatives de reconnexion, le rendu et les blocages nécessitent plus de temps de développement que la valeur des données, comparez le coût par enregistrement valide parmi les options présentées dans notre tour d’horizon des scrapers Web et des API de scraping. Si vous préférez ne pas gérer cette infrastructure du tout, l’API Scraper de WebScrapingAPI est une API de scraping gérée qui prend en charge les proxys, les tentatives de reconnexion et le rendu JavaScript en option. Le rendu JavaScript et le ciblage géographique n’entraînent aucun coût supplémentaire, et les requêtes ayant échoué ne sont pas facturées.

Quels conseils courants sur le web scraping devriez-vous ignorer ?

Ignorez les conseils qui privilégient un accès à court terme au détriment de la sécurité : falsifier l’User-Agent de Googlebot, faire tourner les User-Agents à chaque requête avec des en-têtes non correspondants, simuler par script des mouvements de souris « aléatoires » par défaut, utiliser des délais fixes qui ignorent les signaux du serveur, réessayer indéfiniment à chaque erreur et extraire toutes les données « au cas où ». Chacune de ces pratiques entraîne des coûts supplémentaires, des risques ou des données erronées.

« Utilisez un User-Agent Googlebot pour que les sites vous laissent entrer. » Les propriétaires de sites peuvent démasquer un faux Googlebot grâce à une requête DNS. Utilisez un User-Agent honnête avec des coordonnées, comme décrit dans la section sur l’identification.

« Alternez les User-Agents à chaque requête. » Un User-Agent de navigateur dont les en-têtes ne correspondent à ceux d’aucun navigateur réel se fait davantage remarquer qu’une identité cohérente ; veillez donc à conserver un ensemble d’en-têtes cohérent par session.

« Ajoutez des mouvements de souris et des clics aléatoires pour paraître humain. » Pour la plupart des cibles, cela ajoute de la complexité et ralentit chaque page sans résoudre le problème habituel, à savoir le volume de requêtes ; réglez donc d’abord votre débit et vos en-têtes.

« Attendez 5 à 10 secondes entre chaque requête et vous ne risquerez rien. » Un délai fixe est trop long pour un site qui fonctionne correctement et trop court pour un site renvoyant des codes 429. Utilisez un rythme par hôte qui s’adapte à la latence et au paramètre Retry-After.

« Réessayez jusqu’à ce que ça marche. » Un nombre illimité de tentatives transforme une simple panne en une avalanche de tentatives. Ne réessayez que les erreurs pouvant faire l’objet d’une nouvelle tentative, utilisez la technique du « backoff » avec variation de temps (jitter) et limitez le nombre de tentatives.

« Récupérez tout, au cas où. » Les pages supplémentaires coûtent de la bande passante, de l’espace de stockage et du temps serveur, et les champs supplémentaires augmentent votre exposition s’ils contiennent des données personnelles. Collectez uniquement ce qui est répertorié dans votre contrat de données.

« Le paramètre `crawl-delay` est une norme que tout le monde respecte. » Il ne fait pas partie de la RFC 9309, et Google l’ignore. Respectez-le lorsqu’il est présent, mais ne vous attendez pas à ce que tous les robots d’indexation ou analyseurs syntaxiques l’interprètent de la même manière.

« Ma bibliothèque gère le fichier robots.txt à ma place. » requests et BeautifulSoup ne récupèrent ni n’appliquent pas du tout le fichier robots.txt. Scrapy le peut, grâce à son ROBOTSTXT_OBEY paramètre, que les projets créés avec scrapy startproject . Vérifiez vos paramètres et testez le matcher par rapport aux règles dont vous dépendez.

Liste de contrôle des bonnes pratiques en matière de web scraping

Avant de mettre un scraper en production, assurez-vous qu’il dispose d’un contrat de données écrit, d’une vérification du fichier robots.txt conforme aux normes, d’un User-Agent honnête, d’un contrôle du débit par hôte avec prise en charge de Retry-After, de délais d’expiration, de requêtes conditionnelles, de sélecteurs de secours, d’une validation de schéma, d’alertes de santé d’exécution, de points de contrôle transactionnels et d’un suivi du coût par enregistrement valide.

Chaque ligne comporte un test « prêt lorsque », ce qui vous permet de déterminer si un scraper est prêt pour la mise en production.

Phase de test

Vérification

Terminé lorsque

Plan

Contrat de données rédigé

Champs, types, clé et actualité validés

Aspects juridiques

Conditions générales et fichier robots.txt examinés

Matcher conforme aux normes dans le chemin de récupération

Identité

User-Agent valide

Nom, version et URL de contact inclus

Fréquence

Limitation du débit par hôte

Respect du délai « Retry-After » ; recul avec jitter ; limite de tentatives définie

Requêtes

Délais d'expiration et sessions

Délais d'expiration de connexion et de lecture définis partout

Mise en cache

Requêtes conditionnelles

ETag et Last-Modified enregistrés et envoyés

Analyse

Sélecteurs de repli

Octets transmis à l'analyseur ; fixtures dans les tests

Qualité

Validation du schéma

Motifs de rejet consignés ; déduplication par clé

Surveillance

État de fonctionnement

Sortie non nulle en cas de dépassement du seuil ; « canaris » en place

Évolutivité

Points de contrôle

Validation des données et de la progression en une seule transaction

Coût

Économie unitaire

Coût par enregistrement valide suivi chaque semaine

Vous souhaitez acquérir ces compétences en partant de zéro ? Choisissez parmi nos meilleurs cours sur le web scraping.

FAQ

Qu'est-ce que les bonnes pratiques en matière de web scraping ?

Les bonnes pratiques en matière de web scraping désignent les habitudes qui permettent aux scrapers de rester légaux, respectueux, précis et abordables. Elles consistent notamment à ne collecter que les données dont vous avez besoin, à respecter le fichier robots.txt et les conditions d'utilisation du site, à identifier votre bot, à espacer les requêtes vers chaque serveur, à respecter l'en-tête Retry-After, à valider les enregistrements, à surveiller les exécutions, à vérifier la progression et à suivre le coût par enregistrement valide.

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

Il n’existe pas de chiffre universel garantissant la sécurité. Commencez par une ou deux requêtes simultanées par hôte, espacées d’environ une seconde, puis adaptez-vous : ralentissez lorsque les temps de réponse augmentent, marquez une pause en cas de réponses 429 ou 503, respectez l’en-tête Retry-After et effectuez les crawls de grande envergure en dehors des heures de pointe.

Dois-je respecter le fichier robots.txt lors du scraping ?

Rien ne vous en empêche techniquement, mais le fichier robots.txt constitue la règle publiée par le propriétaire à l’intention des clients automatisés, et l’ignorer expose à un blocage. Appliquez-le conformément à la définition de la RFC 9309, en utilisant le groupe propre à votre bot s’il en existe un, et lisez également les conditions d’utilisation du site.

Le robotparser de Python respecte-t-il la norme robots.txt ?

Pas entièrement. Le module urllib.robotparser de Python applique les règles dans l’ordre où elles apparaissent dans le fichier au lieu de retenir la correspondance la plus longue ; il peut donc autoriser des URL qu’un site interdit ou bloquer des URL qu’il autorise. Il ignore également les caractères génériques * et $ définis dans la RFC 9309.

Quel User-Agent dois-je utiliser pour le web scraping ?

Utilisez un User-Agent honnête et stable qui identifie votre bot et fournit une URL de contact ou une adresse e-mail, afin que les propriétaires de sites puissent vous contacter au lieu de vous bloquer. Ne vous faites jamais passer pour Googlebot, car Google publie des informations sur la manière de vérifier ses robots d’indexation.

Comment un scraper doit-il gérer une réponse 429 « Too Many Requests » ?

Cessez d’envoyer des requêtes à cet hôte, lisez l’en-tête « Retry-After » et attendez au moins ce délai ; la valeur est exprimée soit en secondes, soit sous forme de date HTTP. En l’absence de cet en-tête, réduisez progressivement le rythme de manière exponentielle en appliquant un jitter, en limitant le nombre de tentatives et en diminuant la concurrence par hôte avant de reprendre.

Comment éviter que mon scraper ne tombe en panne lorsqu’un site web modifie sa mise en page ?

Vous ne pouvez pas empêcher les refontes, mais vous pouvez y faire face : ancrez les sélecteurs à des repères stables tels que les identifiants, les attributs de données ou le JSON-LD, prévoyez des solutions de secours par champ, signalez clairement les échecs en cas de champs obligatoires manquants et alertez lorsque le nombre d’enregistrements par page diminue.

Comment vérifier la qualité des données extraites ?

Vérifiez chaque enregistrement par rapport à un schéma avant de le stocker : champs obligatoires, types corrects, plages plausibles et clé unique. Utilisez le type « Decimal » pour les prix, effectuez une déduplication sur la clé, consignez les motifs de rejet et comparez les taux de valeurs nulles et le nombre de lignes entre les différentes exécutions.

Ai-je besoin d’un navigateur headless pour le scraping Web ?

En général, non. Si les données se trouvent dans le code source de la page ou dans une requête JSON dans l’onglet Réseau, récupérez-les directement. N’utilisez un navigateur sans interface utilisateur que pour les pages dont le contenu est généré par JavaScript, et bloquez les images et les polices.

Comment accélérer le web scraping sans se faire bloquer ?

Récupérez moins de données plutôt que d’intensifier vos requêtes : privilégiez les points de terminaison JSON plutôt que les pages rendues, utilisez des requêtes conditionnelles pour que les pages inchangées renvoient un code 304, effectuez des explorations incrémentielles basées sur le plan du site et réutilisez les connexions. Parallélisez ensuite les requêtes entre les hôtes, et non au sein d’un même hôte, en respectant les limites par hôte.

Peut-on extraire des données derrière une page de connexion ?

Soyez prudent. Se connecter implique généralement d’accepter les conditions d’utilisation du site, dont beaucoup interdisent l’accès automatisé ; considérez donc les zones authentifiées comme interdites d’accès sans autorisation ou sans contrat. Si vous disposez d’une autorisation, conservez les identifiants dans un gestionnaire de secrets, limitez les données personnelles et respectez les limites de débit habituelles.

Quand faut-il utiliser une API de scraping Web plutôt que de développer son propre scraper ?

Développez en interne lorsque les cibles sont peu nombreuses, stables et peu protégées. Utilisez une API de scraping gérée lorsque les proxys, les tentatives de connexion, le rendu et les blocages mobilisent plus de temps d’ingénierie que la valeur des données ne le justifie, ou lorsque vous avez besoin de couvrir de nombreux pays, et comparez le coût par enregistrement valide, heures d’ingénierie comprises.

Consacrez votre temps aux données, pas à l’infrastructure

Toutes les pratiques décrites ci-dessus constituent un travail que vous pouvez garder en interne, et pour de nombreuses équipes, c’est la bonne décision. Si ce n’est pas le cas, vous pouvez essayer l’API Scraper avec 5 000 requêtes gratuites et la laisser gérer les proxys, les tentatives de reconnexion et le rendu JavaScript (en option), exécuter des pages dynamiques via l’API Browser avec une seule requête REST, ajouter des proxys résidentiels facturés à l’utilisation à partir de 8,50 $ par Go, ou encore récupérer des enregistrements structurés provenant de 116 sources prises en charge grâce à l’API Data.

À propos de l'auteur

Anda Miuțescu, Rédacteur de contenu technique @ WebScrapingAPI

Anda Miuțescu

Rédacteur de contenu technique

Anda Miuțescu est rédactrice de contenu technique chez WebScrapingAPI ; elle rédige des articles clairs et utiles qui aident les développeurs à comprendre le produit et ses fonctionnalités.

Tutoriel Python Nodriver : extraction de données de navigateur en mode furtif, du premier script à la mise en production
Les techniques de web scraping

Tutoriel Python Nodriver : extraction de données de navigateur en mode furtif, du premier script à la mise en production

TL;DR: En bref : Nodriver est une bibliothèque Python asynchrone qui pilote Chromium via le Chrome DevTools Protocol, sans Selenium ni binaire chromedriver. Utilisez-la lorsqu'un navigateur furtif auto-hébergé, plus proche d'une vraie session Chrome, convient mieux qu'une pile WebDriver, mais sachez qu'elle ne supprime pas à elle seule les risques liés à l'IP, au TLS ou au comportement. Ce tutoriel va d'un premier script à la production en conteneur et à une Browser API hébergée.

Suciu Dan16 min read
Lire l'article
Comment gérer les URL de redirection Google /goto et les jetons CAES dans un outil de collecte de données SERP
Les techniques de web scraping

Comment gérer les URL de redirection Google /goto et les jetons CAES dans un outil de collecte de données SERP

TL;DR: En bref : Les URL Google /goto avec un jeton CAES, ainsi que les anciens wrappers /url, sont des couches de redirection, pas des destinations : un scraper de SERP qui traite chaque href comme final fausse le suivi de positions et les bases de liens. Préférez une API SERP structurée lorsqu'elle renvoie les liens de destination. Si vous devez analyser le HTML, classez chaque href, dédoublonnez les redirections et résolvez-les avec un simple GET. Ne déchiffrez pas CAES.

Suciu Dan14 min read
Lire l'article
En-têtes de réponse HTTP dans cURL : Tous les drapeaux, techniques et recettes de script
Les techniques de web scraping

En-têtes de réponse HTTP dans cURL : Tous les drapeaux, techniques et recettes de script

TL;DR: cURL cache les en-têtes de réponse par défaut. Utilisez -i pour voir les en-têtes avec le corps, -I pour une requête HEAD qui ne renvoie que les en-têtes, -v pour un débogage complet de la requête/réponse, et -D pour enregistrer les en-têtes dans un fichier. Pour les scripts modernes, cURL 7.83+ vous permet d'extraire des en-têtes individuels ou de les récupérer tous en JSON avec l'option -w write-out.

Suciu Dan13 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.