← Alle Artikel

Wie Du Schema.org- und JSON-LD-Daten aus Jeder Webseite Extrahierst

Du schreibst CSS-Selektoren für eine Produktseite. Sie funktionieren zwei Wochen lang. Dann bringt die Seite ein Redesign, der Preis wandert in ein neues <span>, und dein Scraper liefert still und leise für jede Zeile None.

Dabei hatte dieselbe Seite die ganze Zeit Name, Preis, Währung, Lagerstatus und Bewertung in einem sauberen JSON-Block. Er ist für Google gedacht, nicht für dich, aber nichts hindert dich daran, ihn zu lesen. Diese Anleitung zeigt, wie du diese Schema.org-Daten findest, mit Python extrahierst, in einen sauberen Datensatz verwandelst und mit den Fällen umgehst, in denen sie dich im Stich lassen.

Was Strukturierte Daten Sind und Warum Seiten Sie Veröffentlichen

Schema.org ist ein gemeinsames Vokabular, um Dinge im Web zu beschreiben: Product, Offer, JobPosting, Recipe, Article, Event, LocalBusiness und Hunderte mehr. Suchmaschinen lesen es, um Rich Results zu erzeugen, also die Sterne, Preise und Lagerhinweise, die du unter manchen Suchergebnissen siehst. Seiten, denen Suchtraffic wichtig ist, veröffentlichen es auf ihren wichtigsten Seiten.

Es gibt drei Syntaxen:

  • JSON-LD. Ein <script type="application/ld+json">-Block mit einem JSON-Dokument darin. Das ist heute das häufigste Format und das, das Google empfiehlt.
  • Microdata. Attribute wie itemscope, itemtype und itemprop, verteilt über das sichtbare HTML. Verbreitet bei älteren Shops und Templates.
  • RDFa. Dieselbe Idee mit den Attributen vocab, typeof und property. Seltener.

Viele Seiten enthalten zusätzlich Open Graph-Meta-Tags (og:title, og:image, og:price:amount). Sie sind kein Schema.org, aber ein praktischer Fallback für Titel und Bilder.

Warum ist das fürs Scraping wichtig? Strukturierte Daten sind der stabilste Teil einer Seite. Ein Redesign ändert Klassennamen, aber das SEO-Team bricht selten das Markup, das seine Rich Results speist. Ein Parser, der darauf aufbaut, übersteht Layoutänderungen, an denen selektorbasierte Scraper scheitern.

Wie Du Sie auf Einer Seite Findest

Öffne die Seite, sieh dir den Quelltext an (nicht den Elementbaum in den DevTools, der das DOM nach der Ausführung von JavaScript zeigt) und suche nach:

  • application/ld+json für JSON-LD-Blöcke
  • itemprop= für Microdata
  • property="og: für Open-Graph-Tags

Eine Seite hat oft mehrere JSON-LD-Blöcke: einen für die Organisation, einen für die Breadcrumbs, einen für das Produkt. Um alles, was eine Seite veröffentlicht, auf einen Blick zu sehen, füge die URL in den Schema Markup Validator ein. Er listet jedes gefundene Element auf, in jeder Syntax.

JSON-LD mit Python Extrahieren

Du brauchst requests und beautifulsoup4. Die Hilfsfunktion unten sammelt alle JSON-LD-Knoten einer Seite und behandelt zwei Formen, an denen erste Versuche scheitern: einen Block, der eine Liste von Objekten enthält, und einen Block, der alles in ein @graph-Array packt.

import json
import requests
from bs4 import BeautifulSoup

def jsonld_nodes(html):
    soup = BeautifulSoup(html, "html.parser")
    nodes = []
    for tag in soup.find_all("script", type="application/ld+json"):
        raw = tag.string or tag.get_text()
        if not raw or not raw.strip():
            continue
        try:
            # strict=False akzeptiert rohe Zeilenumbrüche in Strings, ein häufiger Fehler
            doc = json.loads(raw, strict=False)
        except json.JSONDecodeError:
            continue
        for item in doc if isinstance(doc, list) else [doc]:
            if isinstance(item, dict) and "@graph" in item:
                nodes.extend(item["@graph"])
            else:
                nodes.append(item)
    return [n for n in nodes if isinstance(n, dict)]

def has_type(node, wanted):
    types = node.get("@type", [])
    return wanted in (types if isinstance(types, list) else [types])

Beachte, dass @type ein String oder eine Liste sein kann. "@type": ["Product", "Car"] ist gültig, also prüfe immer die Zugehörigkeit, statt Strings zu vergleichen.

Oder Lies Alle Syntaxen auf Einmal mit extruct

Wenn du viele verschiedene Seiten scrapst, nutzen manche nur Microdata oder RDFa. Die Open-Source-Bibliothek extruct liest alle in einem Aufruf:

import extruct
from w3lib.html import get_base_url

def all_structured_data(html, url):
    return extruct.extract(
        html,
        base_url=get_base_url(html, url),
        syntaxes=["json-ld", "microdata", "rdfa", "opengraph"],
        uniform=True,  # Microdata und Open Graph in derselben Form wie JSON-LD
        errors="ignore",
    )

Mit uniform=True kommen Microdata-Elemente mit @type und denselben Property-Namen wie JSON-LD zurück, sodass derselbe nachgelagerte Code beides verarbeiten kann.

Ein Haken: extruct parst JSON-LD pro Seite, nicht pro Block. Ist ein Block auf der Seite kaputt, verwirft errors="ignore" das gesamte JSON-LD dieser Seite, einschließlich des gültigen Produktblocks. Eine gute Aufteilung ist die Hilfsfunktion jsonld_nodes von oben für JSON-LD und extruct für Microdata, RDFa und Open Graph.

Einen Product-Knoten in einen Sauberen Datensatz Verwandeln

Den Product-Knoten zu finden ist der einfache Teil. Die Schema.org-Spezifikation erlaubt viel Variation, und echte Seiten nutzen sie komplett aus:

  • offers kann ein einzelnes Offer sein, eine Liste von Angeboten oder ein AggregateOffer mit lowPrice und highPrice.
  • price kann eine Zahl sein, ein String wie "1299.00" oder ein String mit Tausendertrennzeichen. Manchmal versteckt er sich in einer verschachtelten priceSpecification.
  • availability ist meist eine URL wie https://schema.org/InStock, mal mit http, mal nur InStock.
  • brand kann ein einfacher String sein oder ein Objekt mit name.
  • image kann ein String sein, eine Liste oder ein ImageObject mit url.

Ein kleiner Normalisierer fängt all das ab:

import re

def first(value):
    if isinstance(value, list):
        return value[0] if value else None
    return value

def name_of(value):
    value = first(value)
    return value.get("name") if isinstance(value, dict) else value

def to_number(value):
    if isinstance(value, (int, float)):
        return float(value)
    text = re.sub(r"[^\d.,]", "", str(value or ""))
    if "," in text and "." in text:
        # Das zuletzt stehende Trennzeichen ist das Dezimaltrennzeichen
        if text.rfind(",") > text.rfind("."):
            text = text.replace(".", "").replace(",", ".")
        else:
            text = text.replace(",", "")
    elif "," in text:
        decimals = text.split(",")[-1]
        text = text.replace(",", ".") if len(decimals) <= 2 else text.replace(",", "")
    try:
        return float(text)
    except ValueError:
        return None

def product_record(node):
    offer = first(node.get("offers")) or {}
    spec = first(offer.get("priceSpecification")) or {}
    if has_type(offer, "AggregateOffer"):
        price = offer.get("lowPrice") or offer.get("price")
    else:
        price = offer.get("price") or spec.get("price")
    image = first(node.get("image"))
    rating = node.get("aggregateRating") or {}
    return {
        "name": node.get("name"),
        "sku": node.get("sku"),
        "brand": name_of(node.get("brand")),
        "price": to_number(price),
        "currency": offer.get("priceCurrency") or spec.get("priceCurrency"),
        "availability": str(offer.get("availability") or "").rstrip("/").split("/")[-1] or None,
        "image": image.get("url") if isinstance(image, dict) else image,
        "rating": to_number(rating.get("ratingValue")),
        "review_count": to_number(rating.get("reviewCount") or rating.get("ratingCount")),
    }

url = "https://www.example.com/product/123"
html = requests.get(url, timeout=30).text
products = [product_record(n) for n in jsonld_nodes(html) if has_type(n, "Product")]
print(products)

Dasselbe Muster funktioniert für andere Typen. Ein JobPosting hat title, hiringOrganization, jobLocation und baseSalary. Ein Recipe hat recipeIngredient und recipeInstructions. Ein RealEstateListing oder ein Car bringt eigene Properties mit. Schreib eine kleine Mapping-Funktion pro Typ, der dich interessiert, nicht pro Seite.

Wo Strukturierte Daten Dich im Stich Lassen

Strukturierte Daten sind der beste erste Schritt, aber keine vollständige Antwort. Plane für diese Fälle.

Das Datenblatt Steht Nicht im Markup

Seiten veröffentlichen, was Suchmaschinen belohnen: Name, Preis, Bewertung, Verfügbarkeit. Die detaillierten Spezifikationen, die ein Käufer liest, etwa Maße, Materialien, Motorleistung oder Wohnfläche, stehen meist nur in einer HTML-Tabelle oder einer Definitionsliste. Du kannst sie generisch auslesen, ohne Selektoren pro Seite, indem du Paare aus Bezeichnung und Wert sammelst:

def spec_table(html):
    soup = BeautifulSoup(html, "html.parser")
    specs = {}
    for row in soup.select("table tr"):
        cells = row.find_all(["th", "td"])
        if len(cells) == 2:
            specs[cells[0].get_text(" ", strip=True)] = cells[1].get_text(" ", strip=True)
    for dt in soup.find_all("dt"):
        dd = dt.find_next_sibling("dd")
        if dd:
            specs[dt.get_text(" ", strip=True)] = dd.get_text(" ", strip=True)
    return specs

Behalte die Bezeichnungen so, wie die Seite sie anzeigt. Wenn du dieselbe Seite in mehreren Sprachen scrapst, ordne die Bezeichnungen danach deinen eigenen Feldnamen zu (“Kilometerstand” und “Mileage” sind dasselbe Feld).

Das JSON-LD Wird per JavaScript Eingefügt

Manche Seiten fügen ihre strukturierten Daten clientseitig ein, zum Beispiel über einen Tag-Manager. Das rohe HTML von requests enthält dann überhaupt kein JSON-LD, während der Validator, der die Seite rendert, es problemlos anzeigt. Rendere die Seite mit einem Browser und lies die Skripte aus dem DOM:

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    page.goto(url, wait_until="networkidle")
    blocks = page.locator('script[type="application/ld+json"]').all_text_contents()
    browser.close()

Die Daten Sind Veraltet oder Gehören zu Einer Anderen Variante

Die Richtlinien von Google verlangen, dass das Markup dem sichtbaren Inhalt entspricht, aber Templates und Caches driften auseinander. Bei einem Produkt mit Größen oder Farben beschreibt das JSON-LD vielleicht die Standardvariante oder listet alle Varianten unter einer ProductGroup mit hasVariant. Prüfe eine Stichprobe von Seiten gegen das, was der Browser zeigt, bevor du dem Preisfeld im großen Maßstab vertraust.

Das JSON Ist Kaputt

Von Hand gebautes JSON-LD enthält abschließende Kommas, nicht escapte Anführungszeichen und HTML-Kommentare. strict=False erledigt rohe Zeilenumbrüche. Für den Rest rettet ein toleranter Parser wie chompjs viele Blöcke, die json.loads ablehnt. Scheitert ein Block trotzdem, logge ihn und geh zur nächsten Quelle über.

Es Gibt Nichts, Weil Du Blockiert Wurdest

Ein leeres Ergebnis heißt nicht immer, dass die Seite kein Markup hat. Eine Bot-Challenge oder eine “403 als 200 getarnt”-Seite hat ebenfalls kein JSON-LD. Prüfe auf Blockade-Signale, bevor du schließt, dass eine Seite keine strukturierten Daten veröffentlicht. Unser Leitfaden zum Erkennen von Soft Blocks behandelt die üblichen Anzeichen.

Eine Praktische Extraktionsreihenfolge

Probiere für jede Seite die Quellen von der stabilsten zur am wenigsten stabilen und hör auf, sobald du die nötigen Felder hast:

  1. JSON-LD
  2. Microdata und RDFa
  3. Eingebetteter Framework-Zustand wie __NEXT_DATA__ (siehe wie du das versteckte JSON hinter einer JavaScript-Seite findest)
  4. Open-Graph-Meta-Tags für Titel, Beschreibung und Bild
  5. Spezifikationstabellen und beschriftete Werte
  6. CSS- oder XPath-Selektoren, als letztes Mittel

Halte fest, aus welcher Quelle jedes Feld stammt. Wenn sich eine Seite ändert, weißt du, welche Ebene sich bewegt hat.

FAQ

Was ist der Unterschied zwischen JSON-LD, Microdata und RDFa?

Es sind drei Schreibweisen für dasselbe Schema.org-Vokabular. JSON-LD ist ein separater JSON-Block in einem Script-Tag. Microdata und RDFa sind Attribute an den sichtbaren HTML-Elementen. Das Datenmodell ist dasselbe, deshalb kann extruct alle drei in derselben Form zurückgeben.

Ist der Preis im JSON-LD immer der Preis, der auf der Seite steht?

Meistens, weil Suchmaschinen erwarten, dass das Markup dem sichtbaren Inhalt entspricht. Nach einer Preisänderung kann es hinterherhinken oder eine andere Variante beschreiben als die ausgewählte. Prüfe eine Stichprobe, bevor du dich darauf verlässt.

Brauche ich einen Headless-Browser, um strukturierte Daten zu lesen?

Nur wenn die Seite sie per JavaScript einfügt oder einfache HTTP-Clients blockiert. Das meiste JSON-LD steht im serverseitig gerenderten HTML, ein einfacher Request reicht also.

Kann ich strukturierte Daten mit eigenen Selektoren kombinieren?

Ja, und das ist das robusteste Setup. Nimm jedes Feld, das du bekommen kannst, aus den strukturierten Daten und nutze Selektoren nur für die wenigen Felder, die sie nicht abdecken.

Lass eine API das Parsen Übernehmen

Wenn du nicht für jeden Seitentyp eigenen Extraktionscode pflegen willst, kann ScrapeUnblocker statt HTML strukturiertes JSON zurückgeben. Hänge parsed_data=true an einen /getPageSource-Request an:

import requests

r = requests.post(
    "https://api.scrapeunblocker.com/getPageSource",
    params={"url": "https://www.example.com/product/123", "parsed_data": True},
    headers={"x-scrapeunblocker-key": "YOUR_API_KEY"},
    timeout=120,
)
print(r.json()["data"])

Die Antwort sagt, um welchen Seitentyp es sich handelt (product_detail, listing_detail, search_list und so weiter), und liefert englische Felder in snake_case, mit Preisen, die auf einen Wert und eine Währung normalisiert sind. Detailseiten enthalten jetzt auch das komplette Datenblatt der Seite als specifications, genau so, wie die Seite es anzeigt, neben den Schema.org-Daten. Hat eine Seite nichts zu extrahieren, bekommst du das gerenderte HTML zurück, sodass der Parser aus dieser Anleitung trotzdem etwas zum Arbeiten hat. Der Leitfaden zu Parsed Data zeigt die vollständige Struktur der Antwort.

ScrapeUnblocker kostenlos testen

Über 95 % Erfolgsquote · ab 0,55 € pro 1.000 Aufrufe (ggf. zzgl. MwSt.) · 500 kostenlose Anfragen bei der Registrierung.

Kostenlos testen → Preise ansehen