Scraping

Schluss mit HTML-Parsing: Strukturierte Daten aus JSON-LD und eingebetteten Daten extrahieren

Die meisten Seiten enthalten bereits maschinenlesbare Daten. Wie man JSON-LD und eingebettetes JSON extrahiert statt sich auf brüchige Selektoren zu verlassen, und was zu tun ist, wenn die Daten unvollständig sind.

Matt Brown

Matt Brown

26. September 2026 · 9 Min. Lesezeit

Die meisten Scraper werden nach demselben Muster gebaut: Seite öffnen, das Element mit dem Preis finden, einen CSS-Selektor schreiben, das für jedes Feld wiederholen. Das funktioniert, bis die Seite ein Redesign bekommt, eine Klasse umbenannt wird oder der Preis in eine neue Komponente eingebettet wird. Dann liefert der Selektor nichts zurück, oder schlimmer, er liefert das Falsche, und die Pipeline läuft einfach weiter.

Viele Seiten veröffentlichen dieselben Daten bereits in einer Form, die für Maschinen gedacht ist. Suchmaschinen haben danach verlangt, Websites haben sie geliefert, und sie liegen die ganze Zeit schon im Seitenquelltext. Dieses Tutorial zeigt, wie man sie findet, mit ein paar Dutzend Zeilen Code extrahiert und mit den Fällen umgeht, in denen sie fehlen oder unvollständig sind, was, wie ein reales Beispiel unten zeigt, häufiger vorkommt, als die Dokumentation vermuten lässt.

Die wichtigsten Erkenntnisse

  • JSON-LD tauchte im Web Almanac 2024 auf 41% der Seiten auf, gegenüber 34% im Jahr 2022. Für Produkte, Artikel, Veranstaltungen und Organisationen ist es oft die stabilste Quelle auf der Seite.
  • Strukturierte Daten ändern sich weit seltener als das Seitenlayout, weil Websites für ihre Suchergebnisse darauf angewiesen sind. Selektoren brechen bei Redesigns, strukturierte Daten überstehen sie meist.
  • Sie sind nicht immer vollständig. Eine Produktseite kann vollständige Datensätze für die auf dem Bildschirm angezeigte Variante veröffentlichen und für alle anderen Varianten nur nackte Links. Validieren Sie jeden Datensatz.
  • Der robusteste Extraktor versucht zuerst strukturierte Daten, dann eingebettetes JSON und zuletzt CSS-Selektoren, und merkt sich, welche Methode verwendet wurde.

Was „strukturierte Daten” auf einer Webseite bedeutet

Es gibt vier Stellen, an denen maschinenlesbare Daten üblicherweise zu finden sind:

QuelleWie sie aussiehtTypische Inhalte
JSON-LD<script type="application/ld+json">-Blöckeschema.org-Objekte: Product, Offer, NewsArticle, Organization, Event, BreadcrumbList
Microdataitemprop-Attribute an sichtbaren ElementenDasselbe schema.org-Vokabular, verteilt über das Markup
Open Graph und Meta-Tags<meta property="og:...">Titel, Beschreibung, Bild, manchmal Preis
Eingebetteter AnwendungszustandEin großes JSON-Objekt, das das JavaScript der Seite ausliest, etwa __NEXT_DATA__Oft alles, was die Seite anzeigt, und mehr

Der Web Almanac 2024 des HTTP Archive hat ihre Verbreitung im Web gemessen: JSON-LD wuchs „von 34% im Jahr 2022 auf 41% im Jahr 2024”, Microdata blieb stabil bei 26%, und RDFa und Open Graph, zu denen die Social-Sharing-Tags gehören, die die meisten Websites hinzufügen, erschienen auf 66% bzw. 64% der Seiten. JSON-LD ist das, wonach man zuerst greifen sollte, weil es ein in sich geschlossener Datenblock ist und keine über das Layout verstreuten Attribute.

Warum es zuverlässiger ist als Selektoren

Ein CSS-Selektor hängt davon ab, wie eine Seite aussieht. Strukturierte Daten hängen davon ab, was eine Seite bedeutet. Websites ändern ständig, wie ihre Seiten aussehen. Sie ändern weit seltener, was ihre strukturierten Daten aussagen, weil diese Rich Results in Suchmaschinen speisen und ein Bruch für die Website spürbare Kosten hätte.

Sie versagen auch ehrlicher. Ein Selektor, der aufhört zu treffen, kann still ein anderes Element treffen und einen plausiblen, falschen Wert zurückgeben, die Art von Fehler, die in der stillen Fehlerrate beschrieben wird. Ein JSON-LD-Objekt enthält entweder ein price-Feld oder nicht, was die Validierung unkompliziert macht.

JSON-LD in Python extrahieren

Die Standardbibliothek reicht aus. Dieser Extraktor sammelt jeden JSON-LD-Block, toleriert defekte, und durchläuft die Container, die Websites zum Verschachteln von Objekten verwenden: Listen, @graph und hasVariant, das schema.org für Produktvarianten nutzt.

import json
from html.parser import HTMLParser


class _Collector(HTMLParser):
    """Collect JSON-LD blocks and embedded JSON state from an HTML page."""

    def __init__(self):
        super().__init__()
        self.blocks, self._buf, self._kind = [], None, None

    def handle_starttag(self, tag, attrs):
        a = dict(attrs)
        if tag == "script" and a.get("type", "").lower() == "application/ld+json":
            self._buf, self._kind = [], "json-ld"
        elif tag == "script" and a.get("id") == "__NEXT_DATA__":
            self._buf, self._kind = [], "next-data"

    def handle_data(self, data):
        if self._buf is not None:
            self._buf.append(data)

    def handle_endtag(self, tag):
        if tag == "script" and self._buf is not None:
            raw = "".join(self._buf).strip()
            try:
                self.blocks.append((self._kind, json.loads(raw)))
            except json.JSONDecodeError:
                self.blocks.append((self._kind + "-invalid", raw[:200]))
            self._buf = self._kind = None


def _walk(node):
    """Yield every JSON-LD object, flattening lists and nested containers."""
    if isinstance(node, list):
        for item in node:
            yield from _walk(item)
    elif isinstance(node, dict):
        yield node
        for key in ("@graph", "mainEntity", "itemListElement", "hasVariant"):
            if key in node:
                yield from _walk(node[key])


def _types(obj):
    t = obj.get("@type", [])
    return {t} if isinstance(t, str) else set(t)


def jsonld_objects(html, wanted_type=None):
    """Every JSON-LD object on the page, optionally filtered by schema.org type."""
    collector = _Collector()
    collector.feed(html)
    objs = [o for kind, data in collector.blocks if kind == "json-ld" for o in _walk(data)]
    return [o for o in objs if wanted_type is None or wanted_type in _types(o)]

Bei einem echten Guardian-Artikel liefert jsonld_objects(html, "NewsArticle") die Überschrift, die Zeitstempel für Veröffentlichung und Änderung sowie den Autor, ganz ohne Selektoren. Allein diese Zeitstempel sind den Aufwand wert: sie sind exakt, maschinenlesbar und über jeden Artikel der Website hinweg konsistent.

Produkte normalisieren

Produkte erfordern etwas mehr Sorgfalt, weil Preise in verschachtelten offers liegen, manchmal als einzelnes Offer und manchmal als AggregateOffer mit einer Preisspanne.

def products(html):
    """Return normalised product records found in a page's JSON-LD."""
    out = []
    for obj in jsonld_objects(html, "Product"):
        offers = obj.get("offers") or {}
        offer = offers[0] if isinstance(offers, list) and offers else offers
        if isinstance(offer, dict) and "AggregateOffer" in _types(offer):
            price = offer.get("lowPrice")
        else:
            price = offer.get("price") if isinstance(offer, dict) else None
        brand = obj.get("brand")
        out.append({
            "name": obj.get("name"),
            "sku": obj.get("sku") or obj.get("gtin13") or obj.get("mpn"),
            "brand": brand.get("name") if isinstance(brand, dict) else brand,
            "price": float(price) if price not in (None, "") else None,
            "currency": offer.get("priceCurrency") if isinstance(offer, dict) else None,
            "availability": (offer.get("availability") or "").rsplit("/", 1)[-1] if isinstance(offer, dict) else None,
        })
    return out

Ausgeführt gegen eine Seite mit einem Standardprodukt und -angebot liefert es saubere Datensätze wie {"name": "Trail Runner", "sku": "TR-01", "brand": "Acme", "price": 89.0, "currency": "EUR", "availability": "InStock"}, unabhängig davon, wie die Seite gestaltet ist.

Wenn strukturierte Daten unvollständig sind: ein reales Beispiel

Beispiele in der Dokumentation lassen das einfach aussehen. Reale Seiten sind unordentlicher, und es lohnt sich, eine davon zu zeigen.

Wir haben den Extraktor gegen die Produktseite eines beliebten Schuhs auf einem großen Shopify-Store laufen lassen. Das JSON-LD der Seite beschrieb eine ProductGroup, den schema.org-Typ für ein Produkt, das in Varianten verkauft wird, mit Produktname, Marke, Beschreibung und Bildern, sowie 49 Varianten. Nur 7 davon, die Größen der auf dem Bildschirm angezeigten Farbe, waren vollständige Produkte mit einem Preis von 100,00 $ und einem Verfügbarkeitsstatus. Die anderen 42, jede andere Farbe und Größe, waren nackte Referenzen: ein Typ und eine URL, sonst nichts. Die Bewertung befand sich in einem zweiten, separaten JSON-LD-Block.

Die strukturierten Daten beschrieben also die Seite, nicht den Katalog. Eine Pipeline, die annähme, „alle Varianten stehen im JSON-LD”, hätte still eine Farbe bepreist und für den Rest nichts erfasst.

Derselbe Store stellt auch eine öffentliche JSON-Darstellung jedes Produkts bereit, die passte: sieben Varianten für diese Farbe, jede mit Preis und Verfügbarkeitsflag. Aber dort kam der Preis als 10000, in kleinsten Einheiten. Eine Pipeline, die beide Quellen ohne Normalisierung mischte, hätte denselben Schuh einmal mit 100 $ und einmal mit zehntausend Dollar erfasst.

Daraus ergeben sich drei Lehren, die weit über Shopify hinaus gelten:

  • Validieren, nicht annehmen. Ein JSON-LD-Block, der sich parsen lässt, ist kein vollständiger Datensatz. Prüfen Sie, ob jedes benötigte Feld vorhanden ist, und vergleichen Sie, was Sie erhalten haben, mit dem, was Sie erwartet hatten.
  • Folgen Sie den Referenzen, wenn strukturierte Daten unvollständig sind. Varianten-URLs, eingebettetes JSON und öffentliche Produkt-Endpunkte füllen oft die Lücken und sind meist sauberer als die Seite selbst.
  • Einheiten explizit normalisieren. Kleinste Einheiten, Preisspannen, steuerinklusive und steuerexklusive Preise sowie Währungen müssen im Code behandelt werden, nicht durch Annahmen.

Eine Fallback-Kette, die ihre Quelle festhält

Fügen Sie die Teile zu einer Kette zusammen: zuerst strukturierte Daten versuchen, dann eingebettetes JSON, dann Selektoren, und notieren Sie, welche Methode jeden Datensatz geliefert hat.

REQUIRED = ("name", "price", "currency")


def extract_product(html, embedded=None, css_fallback=None):
    for source, candidates in (
        ("json-ld", products(html)),
        ("embedded-json", embedded(html) if embedded else []),
        ("css", css_fallback(html) if css_fallback else []),
    ):
        for record in candidates:
            if all(record.get(f) not in (None, "") for f in REQUIRED):
                return {**record, "source": source}
    return None

Das source-Feld macht sich schnell bezahlt. Wenn eine Website, die bislang stets json-ld-Datensätze lieferte, plötzlich css-Datensätze liefert, haben sich ihre strukturierten Daten geändert oder sind verschwunden, und das sollten Sie wissen, bevor auch der Selektor-Fallback bricht. Es ist zudem ein nützlicher Faktor für einen Target-Health-Score.

Mit der Web Scraping API umsetzen

Wenn Sie Seiten über Shifters Web Scraping API abrufen, funktioniert derselbe Ansatz, ohne selbst einen Browser laufen zu lassen. Der Parameter extract_rules der API bildet CSS-Selektoren auf JSON-Felder ab, und ihre html-Ausgabe liefert das innere HTML eines Elements zurück, sodass eine Regel, die das JSON-LD-Skript auswählt, den Rohblock zum Parsen zurückgibt:

{
  "jsonld": { "selector": "script[type='application/ld+json']", "output": "html" }
}

Eine einzelne Regel liefert das erste passende Element zurück, fordern Sie also für Seiten mit mehreren JSON-LD-Blöcken das vollständige HTML an und lassen Sie den obigen Extraktor darauf laufen. Kombinieren Sie beide Ansätze mit render_js=1 für Seiten, die ihre strukturierten Daten per JavaScript einfügen, und verwenden Sie auto_parser=1, wenn Sie direkt einen JSON-Endpunkt abrufen, etwa eine Produkt-JSON-URL, um den geparsten Inhalt zurückzubekommen. Fehlende Felder kommen als null zurück, statt die Anfrage scheitern zu lassen, was zum Validieren-dann-Fallback-Muster oben passt. Die vollständige Syntax findet sich in der Dokumentation zu Extraktionsregeln.

Was strukturierte Daten Ihnen nicht liefern

Strukturierte Daten beschreiben, was die Website für Suchmaschinen zu veröffentlichen entschieden hat. Sie können der sichtbaren Seite hinterherhinken, Felder auslassen, die die Website nicht preisgeben möchte, oder die Standardvariante statt der auf dem Bildschirm angezeigten beschreiben. Vergleichen Sie besonders bei Preisen die Daten stichprobenartig mit der sichtbaren Seite, denn ein veralteter JSON-LD-Preis und ein aktueller Preis auf der Seite sind aus unterschiedlichen Blickwinkeln beide „korrekt”. Und bei Websites, die Inhalte je nach Besucherstandort variieren, variieren auch die strukturierten Daten, erfassen Sie sie also aus dem Markt, der Sie interessiert, wie in Scraping von Flug- und Hotelpreisen beschrieben.

Fazit

Bevor Sie den nächsten Selektor schreiben, öffnen Sie den Seitenquelltext und suchen Sie nach application/ld+json. Bei einem großen Teil des Webs sind die gewünschten Daten bereits vorhanden, mit einem gemeinsamen Vokabular ausgezeichnet und deutlich weniger anfällig für Änderungen als das Layout drumherum.

Extrahieren Sie sie zuerst, validieren Sie sie, greifen Sie bei Unvollständigkeit auf eingebettetes JSON und dann auf Selektoren zurück, und notieren Sie, aus welcher Quelle jeder Datensatz stammt. Ihre Extraktoren werden seltener brechen, und wenn sie es tun, werden sie es Ihnen sagen.

Quellen und Referenzen

Bereit, loszulegen?

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

Jetzt starten