Wissen

Schema Drift in gescrapten Daten: Defekte Extraktoren erkennen, bevor es Ihre Nutzer tun

Scraper stürzen selten ab, wenn sich eine Website ändert. Sie laufen weiter und liefern subtil falsche Daten. Wie Data Contracts und Batch-Profiling Drift frühzeitig erkennen.

Matt Brown

Matt Brown

29. September 2026 · 8 Min. Lesezeit

Die schlimmsten Scraper-Fehler sehen nicht wie Fehler aus. Der Job läuft, die Requests sind erfolgreich, Zeilen landen planmäßig im Warehouse. Dann fragt jemand aus der Finanzabteilung, warum sich der Durchschnittspreis über Nacht verdreifacht hat, oder warum plötzlich der halbe Katalog als nicht vorrätig gilt, und die Untersuchung führt zurück zu einem Website-Redesign vor drei Wochen, das niemandem aufgefallen ist.

Das ist Schema Drift: Die Daten fließen weiter, aber ihre Form oder Bedeutung hat sich still verändert. Es ist der normale Fehlermodus von Webdaten, denn die Quellen ändern sich ohne Ankündigung, und die Extraktoren produzieren weiterhin Output. Dieser Leitfaden behandelt die zwei Schichten, die das erkennen: Contracts auf Record-Ebene, die schlechte Werte zurückweisen, und Profile auf Batch-Ebene, die bemerken, wenn die Daten als Ganzes nicht mehr wie sie selbst aussehen. Anhand eines kleinen Tests wird auch gezeigt, warum man beides braucht.

Key takeaways

  • Drift ist meist unauffällig. Eine veränderte Seite bringt den Extraktor selten zum Absturz; sie sorgt dafür, dass der Extraktor etwas Plausibles und Falsches zurückgibt.
  • Contracts auf Record-Ebene prüfen jeden Datensatz gegen Regeln: Pflichtfelder, Typen, Wertebereiche und Formate. Sie erfassen offensichtliche Brüche.
  • Profile auf Batch-Ebene vergleichen jeden Lauf mit einer Baseline: Fülldichte, Typenmischung und Mediane. Sie erfassen die Brüche, die Contracts nicht sehen können.
  • In unserem Test mit drei realistischen Drifts erkannten die Contracts einen davon. Die anderen zwei bestanden jede Prüfung auf Record-Ebene und wurden nur vom Batch-Profil erkannt.
  • Alarm bei Drift auslösen, bevor Daten ausgeliefert werden, den Batch unter Quarantäne stellen und festhalten, welche Site und welche Extraktor-Version ihn erzeugt hat.

Wie Drift tatsächlich entsteht

UrsacheWas mit den Daten passiert
Redesign ändert das MarkupEin Selektor trifft ein anderes Element oder gar keines
Preisformat ändert sich”9.99” wird zu “€9.99”, “9,99” oder 999 in kleinsten Einheiten
Ein Feld wandert hinter JavaScriptDer statische HTML-Code enthält es nicht mehr, es kommt leer zurück
Lokalisierung oder Geo-VarianteFür manche Seiten erscheint eine andere Währung, Sprache oder Einheit
A/B-TestEin Teil der Seiten nutzt ein neues Layout, wodurch ein Teil der Datensätze bricht
Block- oder Challenge-SeiteDie Seite lädt, der Extraktor läuft und gibt nichts oder Unsinn zurück

Das gemeinsame Merkmal ist, dass keiner dieser Fälle eine Exception auslöst. Der letzte Fall, Seiten, die erfolgreich laden, aber nicht die gewünschte Seite sind, wird in the silent failure rate behandelt. Der Rest ist der Extraktor, der brav eine Seite verarbeitet, die sich unter ihm verändert hat.

Schicht 1: Contracts auf Record-Ebene

Ein Data Contract legt fest, wie ein gültiger Datensatz aussieht: welche Felder Pflicht sind, ihre Typen, ihre erlaubten Wertebereiche und Formate. Jeder Datensatz wird vor der Annahme geprüft, und Verstöße werden gezählt und unter Quarantäne gestellt, statt sie stillschweigend zu speichern.

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

Ein Datensatz wie {"name": "x", "price": "€9.99", "currency": "eur"} scheitert zweifach: Der Preis ist ein String, und die Währung ist kein dreistelliger Großbuchstaben-Code. Contracts sind günstig, explizit und leicht nachvollziehbar. Ihre Grenze besteht darin, dass jeder Datensatz für sich beurteilt wird, und viel Drift erzeugt Datensätze, die einzeln betrachtet gültig sind.

Schicht 2: Profile auf Batch-Ebene

Ein Profil fasst einen ganzen Batch zusammen: für jedes Feld, wie oft es gefüllt ist, welche Typen vorkommen und, bei Zahlen, den Median. Vergleicht man das Profil jedes Laufs mit einer Baseline aus zuletzt gesunden Läufen, zeigt sich, wann sich die Daten als Ganzes in ihrer Form verändern, selbst wenn jeder einzelne Datensatz seinen Contract besteht.

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

Die Schwellenwerte sind Ausgangspunkte. Ein Rückgang der Fülldichte um 10 Punkte oder eine Verdreifachung eines Medians ist selten geschäftlicher Alltag; beide sollten pro Feld feinjustiert werden, sobald ein paar Wochen Historie vorliegen.

Warum man beides braucht: ein kleiner Test

Wir erzeugten eine gesunde Baseline aus 1.000 Produktdatensätzen, simulierten anschließend drei realistische Brüche und ließen beide Schichten auf jeden davon laufen.

DriftWas passiert istContract auf Record-EbeneBatch-Profil
Formatierter PreisEin Redesign platzierte das Währungssymbol innerhalb des Preises bei 30% der SeitenErkannt: 300 Datensätze abgelehntErkannt: neuer Typ str beim Preis
Kleinste EinheitenPreise kamen als 6305 statt 63,05 anVerpasst: jeder Datensatz bestand die PrüfungErkannt: Median 63,05 auf 6305, und ein neuer Typ int
Defekter Lagerbestand-SelektorDas Feld in_stock kam bei 80% der Seiten leer zurückVerpasst: das Feld ist optionalErkannt: Fülldichte 100% auf 20%

Der Contract erkannte genau den einen Drift, der offensichtlich fehlerhafte Werte erzeugte. Die anderen beiden erzeugten Datensätze, die einzeln betrachtet gültig waren, eine positive Zahl im erlaubten Bereich und ein optionales Feld, das leer blieb, und nur die Batch-Sicht zeigte, dass sich etwas verändert hatte. Der Fall der kleinsten Einheiten ist nicht hypothetisch: ein echter Produkt-Endpoint, den wir letzte Woche untersucht haben, lieferte 10000 für einen Schuh im Wert von $100.00, wie in stop parsing HTML beschrieben.

Was tun, wenn Drift ausgelöst wird

  1. Den Batch unter Quarantäne stellen. Keine Daten aus einem Batch mit Drift-Alarm ausliefern, bevor jemand einen Blick darauf geworfen hat. Ein verspäteter Datensatz ist besser als ein falscher.
  2. Den Umfang eingrenzen. Den Alarm nach Site, Seitentyp und Extraktor-Version aufschlüsseln. Drift beginnt meist auf einer Site nach einer Änderung.
  3. Eine Stichprobe vergleichen. Eine Handvoll betroffener Datensätze neben den Seiten betrachten, von denen sie stammen. Die Ursache ist meist innerhalb weniger Minuten offensichtlich.
  4. Beheben und nachfüllen. Den Extraktor aktualisieren, dann den betroffenen Zeitraum aus gespeicherten Seiten neu extrahieren, sofern man sie aufbewahrt, oder andernfalls neu erfassen.
  5. Die Baseline bewusst aktualisieren. Wenn eine Änderung legitim ist, etwa weil eine Site tatsächlich die Währung wechselt, die Baseline absichtlich zurücksetzen, niemals automatisch.

Drift von vornherein unwahrscheinlicher machen

Manche Quellen driften weniger als andere. Für Suchmaschinen eingebettete strukturierte Daten ändern sich weit seltener als das Seitenlayout, weshalb es die Häufigkeit, mit der Contracts überhaupt auslösen, reduziert, sie zuerst zu extrahieren. Auch das Beobachten der Seiten selbst hilft: eine Template-Änderung, die von change detection at scale erkannt wird, ist eine Frühwarnung, dass die Extraktion bald brechen könnte, und eine steigende Rate an Contract-Verstößen auf einer Site ist ein starker Input für a target health score. Konsistentes Sammeln aus einem Markt pro Job entfernt zudem eine ganze Klasse von Drift, die durch Geo- und Währungsvarianten verursacht wird.

Fazit

Webdaten driften, weil sich das Web ändert, ohne zu fragen. Die Gefahr ist nicht, dass Extraktoren kaputtgehen, sondern dass sie weiter auf Seiten arbeiten, die nicht mehr das sind, wofür sie gebaut wurden, und Daten erzeugen, die Datensatz für Datensatz betrachtet in Ordnung aussehen.

Jeden Datensatz gegen einen Contract prüfen, und jeden Batch gegen seine eigene jüngste Historie prüfen. In unserem Test erkannte der Contract allein einen von drei Drifts; das Batch-Profil erkannte alle drei. Zusammen verwandeln sie „das Dashboard sah drei Wochen lang seltsam aus” in einen Alarm am Morgen, an dem es begann.

Quellen und Referenzen

  • Test durchgeführt von Shifter am 29. September 2026 mit dem obigen Code, auf 1.000 generierten Produktdatensätzen und drei simulierten Drifts.

Bereit, loszulegen?

Testen Sie Shifters Residential-Proxys, 205M+ IPs, 195+ Länder, ab 0,75 $/GB.

Jetzt starten