Scraping

Web-Scraping-API-Daten im großen Maßstab in SQL-Datenbanken überführen

Das Abrufen ist die einfache Hälfte. So bringen Sie Scraping-API-Ausgaben mit idempotenten Ladevorgängen, typisierten Feldern, Schema-Drift-Warnungen und intakter Herkunft in SQL.

Chris Collins

Chris Collins

14. September 2026 · 10 Min. Lesezeit

A web scraping API übernimmt den schwierigsten Teil der Datensammlung: Proxies, Rendering, Wiederholungsversuche und Blockaden. Was sie nicht übernimmt, ist alles, was nach dem Eintreffen der Antwort passiert, und genau dort scheitern die meisten Pipelines tatsächlich.

Die Fehler sind leise. Doppelte Zeilen durch einen Retry, den niemand dedupliziert hat. Eine Preisspalte voller Strings wie "$16.99", die jemand in einem Dashboard in eine Zahl umgewandelt hat. Ein Website-Redesign, das ein Feld vor drei Wochen zu null gemacht hat. Ein Extraktor-Bug, der nicht behoben werden kann, ohne erneut für das komplette Abrufen zu bezahlen.

Dieser Leitfaden befasst sich mit der Ladeseite: dem zuverlässigen Einspeisen von Scraping-API-Output in eine SQL-Datenbank, in großem Umfang, ohne die Fähigkeit zu verlieren, zu erklären oder erneut nachzuvollziehen, was gespeichert wurde. Die Beispiele verwenden PostgreSQL und die Shifter Web Scraping API, die Muster lassen sich aber auf jede SQL-Datenbank übertragen.

Beginnen Sie bei der Antwort, die Sie tatsächlich erhalten

Bei der Shifter Web Scraping API haben Sie die Wahl zwischen zwei Antwortformaten.

Rohes HTML, das Sie selbst parsen. Oder strukturiertes JSON, indem Sie extract_rules übergeben, die CSS-Selektoren auf Felder abbilden:

curl "https://scrape.shifter.io/v1?api_key=YOUR_API_KEY&url=https://shop.example.com/p/42&render_js=1&extract_rules=%7B%22title%22%3A%7B%22selector%22%3A%22h1%22%2C%22output%22%3A%22text%22%7D%2C%22price%22%3A%7B%22selector%22%3A%22.price%22%2C%22output%22%3A%22text%22%7D%7D"

# {"title": "Example Product", "price": "$19.99"}

Zwei Eigenschaften dieser Ausgabeform prägen Ihr Schema. Ein Feld, dessen Selektor auf nichts passt, kommt als null zurück statt die Anfrage scheitern zu lassen, sodass ein fehlendes Element und ein defekter Selektor in der Antwort identisch aussehen. Und Text-Outputs sind Anzeige-Strings, sodass Preise, Bewertungen und Daten für Menschen formatiert ankommen, nicht typisiert für eine Datenbank. Die Syntax der Regeln, einschließlich der Listenextraktion für Ergebnisseiten, findet sich in den Dokumenten zu den Extraktionsregeln.

Drei Schichten, nicht eine Tabelle

Das Design, das den Kontakt mit der Produktion übersteht, trennt das, was Sie empfangen haben, von dem, was Sie daraus geschlossen haben.

SchichtInhaltWarum sie existiert
Raw LandingJede erfolgreiche Antwort, wie erhalten, mit Fetch-MetadatenExtraktion wiederholen, ohne erneut abzurufen
Typisierte BeobachtungenGeparste, typisierte, validierte Werte mit einem Parse-StatusWas Analysten und Anwendungen abfragen
Aktueller ZustandDer neueste Wert pro Entität, abgeleitet aus BeobachtungenSchnelle Lesezugriffe für Produkte und Dashboards

Die Raw-Schicht ist diejenige, die Teams überspringen und später bereuen. Credits werden für erfolgreiche Anfragen ausgegeben, sodass ein Bug in Ihrem Parsing, der nur durch erneutes Abrufen behoben werden kann, den gesamten Crawl doppelt kostet. Landen Sie die Antwort zuerst, parsen Sie danach, und ein Parser-Fix wird zu einer Replay-Abfrage.

Die Landing-Tabelle

CREATE TABLE scrape_raw (
  job_id            text        PRIMARY KEY,
  source_url        text        NOT NULL,
  market            text        NOT NULL,
  fetched_at        timestamptz NOT NULL,
  http_status       smallint    NOT NULL,
  body              jsonb       NOT NULL,
  body_hash         text        NOT NULL,
  extractor_version text        NOT NULL
);

CREATE INDEX scrape_raw_url_time ON scrape_raw (source_url, fetched_at DESC);

Ein paar bewusste Entscheidungen dabei.

job_id ist der Idempotenzschlüssel, berechnet bevor die Anfrage aus der URL und dem Scheduling-Zeitfenster erstellt wird, sodass ein Retry desselben logischen Jobs auf demselben Schlüssel landet, statt eine zweite Zeile zu erzeugen. extractor_version erfasst, welcher Satz an Extraktionsregeln den Body erzeugt hat, was es später ermöglicht, eine Website-Änderung von einer Regeländerung zu unterscheiden. market erfasst, wo die Beobachtung gemacht wurde, denn dieselbe URL kann pro Land unterschiedlichen Inhalt liefern. Und der API-Schlüssel wird nirgendwo in den Anfrage-Metadaten gespeichert, die Sie persistieren, denn Zugangsdaten in einer Datenbank sind Zugangsdaten in jedem Backup.

Idempotentes Laden

import hashlib
import json
import os

import psycopg
import requests
from psycopg.types.json import Jsonb

API = "https://scrape.shifter.io/v1"
RULES = {
    "title": {"selector": "h1", "output": "text"},
    "price": {"selector": ".price", "output": "text"},
}
EXTRACTOR_VERSION = "product-v3"

INSERT_RAW = """
INSERT INTO scrape_raw
  (job_id, source_url, market, fetched_at, http_status, body, body_hash, extractor_version)
VALUES (%s, %s, %s, now(), %s, %s, %s, %s)
ON CONFLICT (job_id) DO NOTHING
"""


def job_id(url: str, market: str, window: str) -> str:
    return hashlib.sha256(f"{url}|{market}|{window}".encode()).hexdigest()


def fetch(url: str, market: str) -> requests.Response:
    params = {
        "api_key": os.environ["SHIFTER_API_KEY"],
        "url": url,
        "render_js": 1,
        "country": market,
        "extract_rules": json.dumps(RULES),
    }
    return requests.get(API, params=params, timeout=90)


def land(conn: psycopg.Connection, url: str, market: str, window: str) -> None:
    resp = fetch(url, market)
    if resp.status_code != 200:
        raise RuntimeError(f"{resp.status_code} for {url}")
    body = resp.text
    with conn.cursor() as cur:
        cur.execute(
            INSERT_RAW,
            (
                job_id(url, market, window),
                url,
                market,
                resp.status_code,
                Jsonb(json.loads(body)),
                hashlib.sha256(body.encode()).hexdigest(),
                EXTRACTOR_VERSION,
            ),
        )

ON CONFLICT (job_id) DO NOTHING ist das, was das Laden sicher wiederholbar macht. Egal ob der Fehler beim Netzwerk, Ihrem Worker oder der Datenbank lag: den Job erneut auszuführen kann kein Duplikat erzeugen. Bei MySQL ist das Äquivalent ein Unique Key mit INSERT IGNORE oder ON DUPLICATE KEY UPDATE.

Durchsatz: Abrufen und Laden entkoppeln

Die Abrufseite hat eine harte Obergrenze, die durch das Concurrency-Limit Ihres Plans festgelegt ist, und Anfragen darüber hinaus liefern 429. Die Datenbank hat ihre eigene Obergrenze, und meist ist es diejenige, die Teams zuerst erreichen, indem sie pro gescrapter Seite eine Verbindung und eine Transaktion öffnen.

Setzen Sie eine Queue dazwischen. Fetch-Worker, dimensioniert nach Ihrem Concurrency-Limit, schreiben Antworten in die Queue. Eine kleine Anzahl von Loader-Workern leert sie in Batches. Für gleichmäßige Volumina reicht executemany von psycopg in Batches von einigen hundert Zeilen. Für große Backfills kopieren Sie den Batch per COPY in eine ungeloggte Staging-Tabelle und mergen ihn in einem Statement:

INSERT INTO scrape_raw
SELECT * FROM scrape_raw_staging
ON CONFLICT (job_id) DO NOTHING;

Das macht aus Tausenden Roundtrips einen einzigen und hält die Idempotenzgarantie intakt.

Bei langen Renderings kann die API asynchron liefern: übergeben Sie webhook=<URL>, und die Antwort wird an Ihren Endpunkt gepostet, sobald sie fertig ist. Machen Sie auch diesen Empfänger idempotent bezüglich job_id, denn jede HTTP-Zustellung kann von der einen oder anderen Seite erneut versucht werden.

API-Fehler auf Pipeline-Verhalten abbilden

Nicht alle Statuscodes sind Retry-Kandidaten, und ein Loader, der sie einheitlich behandelt, spammt entweder eine defekte Konfiguration zu oder gibt bei vorübergehenden Fehlern auf. Die vollständige Tabelle finden Sie unter Fehler und Limits.

StatusPipeline-Verhalten
408, 422, 500Erneut versuchen mit exponentiellem Backoff
429Zurückfahren und Worker-Concurrency reduzieren
400, 401, 403Konfigurationsfehler: an eine Dead-Letter-Queue senden und alarmieren, niemals erneut versuchen
509Credits erschöpft: Fetch-Stufe stoppen und alarmieren, erneutes Versuchen hilft nicht

Fehlgeschlagene Anfragen und Ziel-4xx- oder 5xx-Antworten werden nicht berechnet, und die API versucht vorübergehende Fehler bereits bis zu dreimal erneut, bevor sie antwortet, sodass Ihre eigenen Retries Zeit kosten, nicht Credits. Sie kosten trotzdem Zeit, weshalb der Backoff wichtig ist.

Die Beobachtungen typisieren

Genau hier werden Anzeige-Strings zu Daten, und genau hier entstehen die meisten stillen Fehler.

CREATE TABLE price_observation (
  source_url    text          NOT NULL,
  market        text          NOT NULL,
  observed_at   timestamptz   NOT NULL,
  price_amount  numeric(12,2),
  currency      char(3),
  raw_price     text,
  parse_status  text          NOT NULL,
  job_id        text          NOT NULL REFERENCES scrape_raw (job_id),
  PRIMARY KEY (source_url, market, observed_at)
);

Drei Regeln halten das sauber.

Bewahren Sie den rohen String neben dem geparsten Wert auf. raw_price ist das, was Ihnen erlaubt, eine verdächtige Zahl zu prüfen, ohne erneut abzurufen.

Parsen Sie pro Markt, nicht global. "1.299,00" und "1,299.00" sind derselbe Preis unter unterschiedlichen Konventionen, und ein Währungssymbol ist keine Währung: $ steht je nach Shop für US-, kanadische oder australische Dollar. Ermitteln Sie den ISO-Code aus Symbol und Markt gemeinsam.

Erfassen Sie, warum ein Wert null ist. Ein parse_status von missing, unparseable oder ok unterscheidet “die Seite hatte keinen Preis” von “unser Parser ist fehlgeschlagen”, was Ihnen das null der API allein nicht sagen kann.

Für den aktuellen Zustand leiten Sie ab, statt zu pflegen. In PostgreSQL:

CREATE VIEW price_current AS
SELECT DISTINCT ON (source_url, market) *
FROM price_observation
WHERE parse_status = 'ok'
ORDER BY source_url, market, observed_at DESC;

Eine abgeleitete View kann nicht aus dem Gleichgewicht mit der Historie geraten, die sie zusammenfasst.

Schema-Drift erkennen, bevor Ihre Nutzer es tun

Websites ändern ihr Markup, und ein geänderter Selektor wirft keinen Fehler. Er liefert null, die Anfrage ist erfolgreich, ein Credit wird ausgegeben, und die Zeile landet scheinbar gültig.

Die Verteidigung ist ein Null-Rate-Monitor pro Feld, pro Quelle, pro Extraktor-Version. Berechnen Sie den Anteil der missing-Parse-Status für jedes Feld über ein rollierendes Zeitfenster und alarmieren Sie, wenn er sich stark von seiner Basislinie entfernt. Ein Preisfeld, das über Nacht von 2% fehlend auf 60% fehlend springt, ist ein Redesign, und es am selben Tag zu erkennen macht den Unterschied zwischen einem gepatchten Selektor und drei Wochen unbrauchbarer Historie.

Wenn Sie es beheben, erhöhen Sie extractor_version und wiederholen Sie die betroffenen Raw-Zeilen mit dem neuen Parser. Das ist der Lohn dafür, rohe Antworten zu landen.

Aufbewahrung und Partitionierung

Raw-Landing-Tabellen wachsen am schnellsten und werden am wenigsten gelesen. Partitionieren Sie sie nach Abrufdatum, behalten Sie sie lange genug, um Ihr realistisches Replay-Fenster abzudecken, und löschen Sie alte Partitionen, statt Zeilen zu löschen. Beobachtungstabellen sind der historische Datensatz und verdienen sich üblicherweise eine längere Aufbewahrung, ebenso partitioniert.

Was Sie überwachen sollten

MetrikWas sie aufdeckt
Erfolgreiche Fetches gegenüber gelandeten ZeilenLoader-Verluste zwischen der API und der Datenbank
Doppelte job_id-KonflikteRetry-Stürme oder Scheduling-Überlappungen
Nullrate pro Feld und Extraktor-VersionMarkup-Änderungen und defekte Selektoren
Queue-Tiefe und LadeverzögerungEin Loader, der hinter die Fetch-Stufe zurückfällt
Verbrauchte Credits gegenüber Zeilen, die ok geparst wurdenGeld, das für Antworten ausgegeben wurde, die Sie nicht nutzen konnten

Diese letzte Metrik ist die Kostensicht, die zählt: Credits pro nutzbarer Zeile, nicht Credits pro Anfrage. Nutzung und Fehlerrate der API selbst sind im Panel unter Web Scraping API sichtbar.

FAQ

Sollte ich HTML oder extrahiertes JSON in der Raw-Schicht speichern?

Extrahiertes JSON ist deutlich kleiner und meist ausreichend. Speichern Sie HTML nur für Quellen, bei denen Sie erwarten, die Extraktionslogik häufig zu ändern, und geben Sie dieser Tabelle eine kurze Aufbewahrungsdauer.

Ist JSONB gut genug, um direkt abzufragen?

Für Exploration ja. Für alles, wovon ein Produkt oder Dashboard abhängt, heben Sie Felder in typisierte Spalten, wo die Datenbank Typen erzwingen und gewöhnliche Indizes nutzen kann.

Wie vermeide ich, für Seiten zu bezahlen, die sich nicht geändert haben?

Jede erfolgreiche Anfrage kostet einen Credit, also muss die Ersparnis daher kommen, weniger abzurufen, nicht weniger zu schreiben. Nutzen Sie günstige Signale wie eine Listing- oder Sitemap-Seite, um zu entscheiden, welche Detailseiten überhaupt abgerufen werden müssen.

Funktioniert das auch mit der Amazon API?

Ja. Ihre Antworten sind bereits strukturiertes JSON, sodass Extraktionsregeln entfallen, aber Preise kommen weiterhin als Anzeige-Strings an, und die Muster für Landing, Typisierung und Drift gelten unverändert.

Fazit

Eine Scraping-API löst die Datensammlung. Ihr Datenbankdesign entscheidet, ob das Gesammelte vertrauenswürdig bleibt. Landen Sie jede erfolgreiche Antwort mit einem Idempotenzschlüssel und einer Extraktor-Version, typisieren Sie Werte pro Markt, während Sie den rohen String aufbewahren, erfassen Sie, warum ein Wert null ist, leiten Sie den aktuellen Zustand ab, statt ihn zu pflegen, und beobachten Sie Nullraten pro Feld, damit Markup-Änderungen am selben Tag auffallen.

Speziell für Amazon werden die Optionen in den besten Web-Scraping-APIs für Amazon-Monitoring verglichen, und eine Immobilien-Version derselben Pipeline findet sich unter wie Immobilienunternehmen Web-Scraping-APIs nutzen. Das Produkt finden Sie auf der Seite Web Scraping API, mit Plänen auf der Preisseite.

Bereit, loszulegen?

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

Jetzt starten