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
| Ursache | Was mit den Daten passiert |
|---|---|
| Redesign ändert das Markup | Ein 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 JavaScript | Der statische HTML-Code enthält es nicht mehr, es kommt leer zurück |
| Lokalisierung oder Geo-Variante | Für manche Seiten erscheint eine andere Währung, Sprache oder Einheit |
| A/B-Test | Ein Teil der Seiten nutzt ein neues Layout, wodurch ein Teil der Datensätze bricht |
| Block- oder Challenge-Seite | Die 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.
| Drift | Was passiert ist | Contract auf Record-Ebene | Batch-Profil |
|---|---|---|---|
| Formatierter Preis | Ein Redesign platzierte das Währungssymbol innerhalb des Preises bei 30% der Seiten | Erkannt: 300 Datensätze abgelehnt | Erkannt: neuer Typ str beim Preis |
| Kleinste Einheiten | Preise kamen als 6305 statt 63,05 an | Verpasst: jeder Datensatz bestand die Prüfung | Erkannt: Median 63,05 auf 6305, und ein neuer Typ int |
| Defekter Lagerbestand-Selektor | Das Feld in_stock kam bei 80% der Seiten leer zurück | Verpasst: das Feld ist optional | Erkannt: 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
- 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.
- Den Umfang eingrenzen. Den Alarm nach Site, Seitentyp und Extraktor-Version aufschlüsseln. Drift beginnt meist auf einer Site nach einer Änderung.
- Eine Stichprobe vergleichen. Eine Handvoll betroffener Datensätze neben den Seiten betrachten, von denen sie stammen. Die Ursache ist meist innerhalb weniger Minuten offensichtlich.
- Beheben und nachfüllen. Den Extraktor aktualisieren, dann den betroffenen Zeitraum aus gespeicherten Seiten neu extrahieren, sofern man sie aufbewahrt, oder andernfalls neu erfassen.
- 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.