Base de connaissances

Dérive de schéma dans les données extraites : détecter les extracteurs cassés avant vos utilisateurs

Les scrapers plantent rarement lorsqu'un site change. Ils continuent de fonctionner et renvoient des données subtilement erronées. Comment les contrats de données et le profilage par lots détectent la dérive tôt.

Matt Brown

Matt Brown

29 septembre 2026 · 9 min de lecture

Les pires échecs de scraping ne ressemblent pas à des échecs. Le job s’exécute, les requêtes réussissent, les lignes arrivent dans l’entrepôt de données comme prévu. Puis quelqu’un en finance demande pourquoi le prix moyen a triplé du jour au lendemain, ou pourquoi la moitié du catalogue est soudainement en rupture de stock, et l’enquête remonte à une refonte du site web réalisée trois semaines plus tôt et passée inaperçue.

C’est le schema drift : les données continuent d’affluer, mais leur forme ou leur signification a discrètement changé. C’est le mode de défaillance normal des données web, car les sources changent sans préavis et les extracteurs continuent de produire des résultats. Ce guide couvre les deux couches qui le détectent : les contrats au niveau des enregistrements qui rejettent les valeurs incorrectes, et les profils au niveau des lots qui remarquent quand l’ensemble des données cesse de ressembler à lui-même. Il montre aussi, avec un petit test, pourquoi les deux sont nécessaires.

Points clés à retenir

  • Le drift est généralement silencieux. Une page modifiée casse rarement l’extracteur ; elle fait plutôt en sorte que l’extracteur renvoie quelque chose de plausible et de faux.
  • Les contrats au niveau des enregistrements vérifient chaque enregistrement selon des règles : champs obligatoires, types, plages et formats. Ils détectent les casses évidentes.
  • Les profils au niveau des lots comparent chaque exécution à une référence : taux de remplissage, répartition des types et médianes. Ils détectent ce que les contrats ne peuvent pas voir.
  • Dans notre test portant sur trois cas de drift réalistes, les contrats en ont détecté un seul. Les deux autres ont passé tous les contrôles au niveau des enregistrements et n’ont été détectés que par le profil de lot.
  • Alertez sur le drift avant l’envoi des données, mettez le lot en quarantaine, et enregistrez quel site et quelle version d’extracteur l’ont produit.

Comment le drift se produit réellement

CauseCe que les données font
La refonte change le balisageUn sélecteur correspond à un élément différent, ou à rien
Le format de prix change« 9.99 » devient « €9.99 », « 9,99 » ou 999 en unités mineures
Un champ se déplace derrière du JavaScriptLe HTML statique ne le contient plus, donc il revient vide
Localisation ou variante géographiqueUne devise, une langue ou une unité différente apparaît pour certaines pages
Test A/BUne fraction des pages utilise une nouvelle mise en page, donc une fraction des enregistrements est cassée
Page de blocage ou de challengeLa page se charge, l’extracteur s’exécute, et renvoie rien ou n’importe quoi

Le point commun est qu’aucun de ces cas ne déclenche d’exception. Le dernier, des pages qui se chargent avec succès mais qui ne sont pas la page voulue, est traité dans le taux d’échec silencieux. Le reste correspond à l’extracteur traitant fidèlement une page qui a changé sous ses pieds.

Couche 1 : les contrats au niveau des enregistrements

Un contrat de données définit à quoi ressemble un enregistrement valide : quels champs sont obligatoires, leurs types, leurs plages et formats autorisés. Chaque enregistrement est vérifié avant d’être accepté, et les violations sont comptabilisées et mises en quarantaine plutôt que stockées silencieusement.

import re
import statistics
from collections import Counter

CONTRACT = {
    "name":     {"type": str, "required": True},
    "price":    {"type": float, "required": True, "min": 0.01, "max": 100_000},
    "currency": {"type": str, "required": True, "pattern": r"^[A-Z]{3}$"},
    "in_stock": {"type": bool, "required": False},
}


def violations(record, contract=CONTRACT):
    """Record-level checks: presence, type, range, format."""
    problems = []
    for field, rule in contract.items():
        value = record.get(field)
        if value in (None, ""):
            if rule.get("required"):
                problems.append(f"{field}: missing")
            continue
        if rule["type"] is float and isinstance(value, int) and not isinstance(value, bool):
            value = float(value)
        if not isinstance(value, rule["type"]):
            problems.append(f"{field}: expected {rule['type'].__name__}, got {type(value).__name__}")
            continue
        if "min" in rule and value < rule["min"] or "max" in rule and value > rule["max"]:
            problems.append(f"{field}: {value} out of range")
        if "pattern" in rule and not re.match(rule["pattern"], value):
            problems.append(f"{field}: {value!r} bad format")
    return problems

Un enregistrement comme {"name": "x", "price": "€9.99", "currency": "eur"} échoue deux fois : le prix est une chaîne de caractères, et la devise n’est pas un code de trois lettres majuscules. Les contrats sont peu coûteux, explicites et faciles à raisonner. Leur limite est que chaque enregistrement est jugé isolément, et une bonne partie du drift produit des enregistrements individuellement valides.

Couche 2 : les profils au niveau des lots

Un profil résume un lot entier : pour chaque champ, à quelle fréquence il est renseigné, quels types apparaissent et, pour les nombres, la médiane. Comparer le profil de chaque exécution à une référence issue des exécutions saines récentes montre quand les données dans leur ensemble changent de forme, même si chaque enregistrement passe son contrat.

def profile(records, fields=CONTRACT):
    """Batch-level shape: how often each field is filled, its types, and numeric medians."""
    n = max(1, len(records))
    out = {}
    for field in fields:
        values = [r.get(field) for r in records]
        present = [v for v in values if v not in (None, "")]
        numbers = [float(v) for v in present if isinstance(v, (int, float)) and not isinstance(v, bool)]
        out[field] = {
            "fill_rate": len(present) / n,
            "types": Counter(type(v).__name__ for v in present),
            "median": statistics.median(numbers) if numbers else None,
            "distinct": len(set(map(str, present))),
        }
    return out


def drift(baseline, current, fill_drop=0.1, median_ratio=3.0):
    """Compare two batch profiles and describe what changed shape."""
    alerts = []
    for field, base in baseline.items():
        cur = current[field]
        if base["fill_rate"] - cur["fill_rate"] > fill_drop:
            alerts.append(f"{field}: filled {base['fill_rate']:.0%} -> {cur['fill_rate']:.0%}")
        if set(cur["types"]) - set(base["types"]):
            alerts.append(f"{field}: new types {sorted(set(cur['types']) - set(base['types']))}")
        if base["median"] and cur["median"]:
            ratio = cur["median"] / base["median"]
            if ratio > median_ratio or ratio < 1 / median_ratio:
                alerts.append(f"{field}: median {base['median']:g} -> {cur['median']:g}")
    return alerts

Les seuils sont des points de départ. Une baisse de 10 points du taux de remplissage ou un changement d’un facteur trois de la médiane relève rarement du fonctionnement normal ; ajustez les deux par champ une fois que vous disposez de quelques semaines d’historique.

Pourquoi il faut les deux : un petit test

Nous avons généré une référence saine de 1 000 enregistrements de produits, puis simulé trois casses réalistes et exécuté les deux couches sur chacune.

DriftCe qui s’est passéContrat au niveau des enregistrementsProfil de lot
Prix formatéUne refonte a placé le symbole de devise à l’intérieur du prix sur 30 % des pagesDétecté : 300 enregistrements rejetésDétecté : nouveau type str dans price
Unités mineuresLes prix ont commencé à arriver sous forme de 6305 au lieu de 63.05Non détecté : chaque enregistrement est passéDétecté : médiane de 63.05 à 6305, et un nouveau type int
Sélecteur de stock casséLe champ in-stock est revenu vide sur 80 % des pagesNon détecté : le champ est optionnelDétecté : rempli de 100 % à 20 %

Le contrat a détecté le seul drift qui produisait des valeurs manifestement mal formées. Les deux autres ont produit des enregistrements individuellement valides, un nombre positif dans la plage et un champ optionnel laissé vide, et seule la vue de lot a montré que quelque chose avait changé. Le cas des unités mineures n’est pas hypothétique : un véritable endpoint produit que nous avons examiné la semaine dernière renvoyait 10000 pour une chaussure à $100.00, comme décrit dans arrêtez de parser du HTML.

Que faire quand le drift se déclenche

  1. Mettez le lot en quarantaine. N’envoyez pas les données d’un lot avec des alertes de drift avant qu’une personne les ait examinées. Un jeu de données en retard vaut mieux qu’un jeu de données faux.
  2. Cernez la portée. Décomposez l’alerte par site, type de page et version d’extracteur. Le drift commence généralement sur un site après un changement donné.
  3. Comparez un échantillon. Examinez une poignée d’enregistrements affectés en les mettant en regard des pages dont ils proviennent. La cause est généralement évidente en quelques minutes.
  4. Corrigez et rattrapez. Mettez à jour l’extracteur, puis réextrayez la période affectée à partir des pages stockées si vous les conservez, ou recollectez si ce n’est pas le cas.
  5. Mettez à jour la référence délibérément. Quand un changement est légitime, par exemple un site changeant réellement de devise, réinitialisez la référence intentionnellement, jamais automatiquement.

Réduire le risque de drift en amont

Certaines sources dérivent moins que d’autres. Les données structurées intégrées pour les moteurs de recherche changent bien moins souvent que la mise en page, ce qui explique pourquoi les extraire en premier réduit la fréquence à laquelle les contrats se déclenchent. Surveiller les pages elles-mêmes aide aussi : un changement de template détecté par la détection de changement à grande échelle est un signal d’alerte précoce indiquant que l’extraction risque de se casser, et un taux croissant de violations de contrat sur un site est une donnée d’entrée solide pour un score de santé de cible. Collecter systématiquement un seul marché par job élimine aussi toute une catégorie de drift causée par les variantes géographiques et de devise.

En résumé

Les données web dérivent parce que le web change sans prévenir. Le danger n’est pas que les extracteurs se cassent, mais qu’ils continuent de fonctionner sur des pages qui ne sont plus ce pour quoi ils ont été conçus, et produisent des données qui semblent correctes enregistrement par enregistrement.

Vérifiez chaque enregistrement selon un contrat, et vérifiez chaque lot par rapport à son propre historique récent. Dans notre test, le contrat seul a détecté un drift sur trois ; le profil de lot les a tous détectés. Ensemble, ils transforment « le tableau de bord semblait bizarre depuis trois semaines » en une alerte dès le matin où cela a commencé.

Sources et références

  • Test réalisé par Shifter le 29 septembre 2026 avec le code ci-dessus, sur 1 000 enregistrements de produits générés et trois cas de drift simulés.

Prêt à commencer ?

Essayez les proxies résidentiels de Shifter, 205M+ IPs, 195+ pays, à partir de 0,75 $/GB.

Commencer