Zurück zum Blog
Anleitungen
Andrei OgiolanLast updated on May 7, 202611 min read

Web Scraping JavaScript-Tabellen in Python: Von versteckten APIs zu Playwright

Web Scraping JavaScript-Tabellen in Python: Von versteckten APIs zu Playwright
Kurz gesagt: Zum Web-Scraping von JavaScript-Tabellen in Python ist selten ein Headless-Browser erforderlich. Öffne die DevTools, suche den JSON-Endpunkt, der das Raster befüllt, spiele ihn mit requests, paginiere es und greife nur dann auf Playwright zurück, wenn der Netzwerkaufruf signiert, verschlüsselt oder anderweitig versiegelt ist.

Du hast den naheliegenden Code geschrieben. requests.get(url), übergeben Sie den HTML-Code an BeautifulSoup, extrahieren Sie die Zeilen aus dem <table>. Das Skript läuft, die Datei landet auf der Festplatte und die CSV-Datei ist leer. Willkommen beim Web-Scraping von JavaScript-Tabellen, bei dem die Zeilen, die du in deinem Browser siehst, in dem Dokument, das der Server tatsächlich zurückgegeben hat, gar nicht existieren.

Statische Tabellen liefern die Daten im ursprünglichen HTML. Dynamische Tabellen (auch AJAX- oder JavaScript-gerenderte Tabellen genannt) liefern eine fast leere Hülle; dann ruft ein Skript auf der Seite einen JSON-Endpunkt auf und fügt nach dem Laden Zeilen in das DOM ein. Wenn du dieses Skript nicht ausführst, siehst du diese Zeilen nicht. Einen vollständigen Browser zu starten, um dies zu beheben, ist eine schwerfällige Lösung für ein meist kleines Problem.

Dieser Leitfaden wählt den kürzeren Weg. Wir beginnen mit einer Entscheidungshilfe, damit du nicht mehr raten musst, ob du requests oder eine Browser-Engine nutzen soll, und gehen dann Schritt für Schritt durch, wie man den zugrunde liegenden JSON-Endpunkt in DevTools findet, ihn in Python mit Paginierung und Authentifizierung nachbildet, in saubere Zeilen parst und in CSV, JSON Lines oder SQLite exportiert. Playwright dient hier als echter Fallback für Websites, die den Netzwerkaufruf verbergen, nicht als Standardwerkzeug. Am Ende haben Sie ein Skript, das Sie im nächsten Quartal erneut ausführen können, ohne es von Grund auf neu schreiben zu müssen.

Warum JavaScript-Tabellen Standard-Scraper zum Scheitern bringen

Wenn Sie requests.get() auf einer Seite mit einer JavaScript-Tabelle auf, erhalten Sie das Dokument zurück, das der Server gesendet hat, bevor jeglicher Browser-Code ausgeführt wurde. Dieses Dokument enthält das Layout, die Navigation, den leeren Raster-Container und ein Bündel an JavaScript. Die Zeilen sind noch nicht vorhanden. Der Browser führt das Skript aus, das Skript ruft eine JSON-Nutzlast ab, und erst dann wird die Tabelle gefüllt.

BeautifulSoup parst getreu das, was ihm übergeben wurde, nämlich ein <table> ohne <tr> Kindern. Ihr Selektor findet keine Übereinstimmung, Ihre Schleife läuft null Mal, und der Writer erzeugt eine CSV-Datei mit Kopfzeilen und ohne Daten. Das Web-Scraping von JavaScript-Tabellen scheitert hier stillschweigend, da technisch gesehen jede Ebene funktioniert hat.

Wähle einen Extraktionspfad, bevor du Code schreibst

Bevor du einen Editor öffnest, führe eine einminütige Entscheidungsleiter durch. Die Rangfolge ist wichtig, da jeder Schritt mehr Wartungsaufwand erfordert als der darüberliegende.

  1. Offizielle API oder CSV-Export. Viele Dashboards bieten einen Download-Button oder einen dokumentierten Endpunkt. Nutze diesen. Du solltest nichts scrapen, was du einfach mit einem Schlüssel anfordern kannst.
  2. Verstecktes XHR oder Fetch JSON. Die meisten modernen Raster werden durch einen JSON-Aufruf gespeist, den du in DevTools sehen kannst. Dies sollte deine Standardmethode für das Web-Scraping von JavaScript-Tabellen sein. Die Nutzdaten sind strukturiert, das Schema ist stabil und du überspringst die gesamte Rendering-Ebene.
  3. Statisch <table> bereits in der Quelle. Wenn die Zeilen in view-source: (kein Skript erforderlich), parsen Sie den HTML-Code mit pandas.read_html() für einen schnellen Erfolg oder requests plus BeautifulSoup mit lxml für die Produktion.
  4. Headless-Browser-Rendering. Greifen Sie nur dann auf Playwright zurück, wenn der Netzwerkpfad signiert ist, GraphQL mit strengen Ursprungsprüfungen verwendet wird, WebSockets genutzt werden oder der Pfad anderweitig von einem einfachen HTTP-Client aus nicht erreichbar ist.

Die meisten Artikel behandeln Weg 4 zuerst. Das ist verkehrt herum. Ein versteckter JSON-Endpunkt, sofern vorhanden, liefert dir sauberere Daten und eine geringere Fehleranfälligkeit als es jeder Headless-Browser jemals könnte.

Finden Sie den versteckten JSON-Endpunkt mit DevTools

Der schnellste Weg, um zu bestätigen, dass eine Tabelle durch JavaScript gefüllt wird, ist die Überprüfung des rohen Seitenquellcodes, nicht des gerenderten DOM. Klicken Sie mit der rechten Maustaste auf die Seite, wählen Sie „Quelltext anzeigen“ und suchen Sie nach einem in der Tabelle sichtbaren Beispielwert (ein Name, ein Gehalt, eine eindeutige ID). Wenn die Suche nichts ergibt, wurde die Zeile nach dem Laden eingefügt und Sie sehen ein durch JavaScript gerendertes Raster.

Suchen Sie nun die Anfrage, die die Daten geliefert hat. Das in diesem Leitfaden durchgehend verwendete Referenzbeispiel ist die öffentliche DataTables-AJAX-Demo unter datatables.net/examples/data_sources/ajax.html. Öffnen Sie DevTools, wechseln Sie zur Registerkarte „Netzwerk“ und filtern Sie nach „Fetch/XHR“. Laden Sie die Seite neu, um den gesamten Datenverkehr zu erfassen, und lösen Sie dann eine Sortierung oder eine Änderung der Paginierung aus. Diese zweite Aktion ist der Trick: Die größte Datenmenge nach einer Sortierungsänderung ist fast immer diejenige, die die Zeilen enthält.

Klicken Sie auf den Aufruf, öffnen Sie „Response“ und überprüfen Sie, ob die JSON-Struktur Ihren Erwartungen entspricht. Überprüfen Sie die Header auf die Anfragemethode, Abfrageparameter, Cookies und eventuelle benutzerdefinierte Tokens (X-CSRF-Token, Authorization). Bei kniffligen Zielen klicken Sie mit der rechten Maustaste auf die Anfrage und wählen Sie „Als cURL kopieren“. Dadurch bleiben Header, Cookies und der genaue Body erhalten, sodass Sie ihn in einen Konverter einfügen und Ihren Python-Code starten können, ohne etwas von Hand eingeben zu müssen. Filtern Sie rigoros: Ein einziges Suchfeld kann zehn Autocomplete-Anfragen auslösen, bevor die eigentliche Anfrage erfolgt.

Spielen Sie die erfasste Anfrage in Python nach

Sobald Sie die URL und die Header haben, ist der Python-Teil überschaubar. Beginnen Sie mit dem absoluten Minimum und fügen Sie Header nur hinzu, wenn der Server Fehler meldet.

import requests

URL = "https://datatables.net/examples/ajax/data/objects.txt"

headers = {
    "User-Agent": "Mozilla/5.0 (compatible; tables-scraper/1.0)",
    "Accept": "application/json, text/javascript, */*; q=0.01",
}

response = requests.get(URL, headers=headers, timeout=15)
response.raise_for_status()
payload = response.json()

Zwei Dinge sind besonders wichtig. Erstens raise_for_status() ist unverzichtbar, da Anti-Bot-Systeme oft HTML mit HTTP 200 zurückgeben und eine fehlende Statusprüfung eine weiche Blockierung in beschädigte Daten verwandelt. Zweitens: Widerstehe dem Drang, dein persönliches Sitzungs-Cookie aus DevTools einzufügen. Dieses Cookie läuft ab, gibt persönlichen Kontext an dein Repo weiter und bindet das Skript an eine bestimmte Person. Bevorzuge öffentliche Header und füge dann einen echten Anmeldeablauf mit einem requests.Session , falls der Endpunkt tatsächlich eine Authentifizierung benötigt.

Für Workflows, bei denen Sie asynchrone Fan-Out-Verteilung über viele Endpunkte benötigen, ist HTTPX eine nahtlose Alternative mit einer nahezu identischen synchronen API und erstklassiger Async-Unterstützung. Betrachten Sie dies als Option und nicht als strenge Empfehlung; requests bleibt auch im Jahr 2026 eine vollkommen akzeptable Standardeinstellung.

Parsen Sie die JSON-Nutzdaten in saubere Zeilen

Das DataTables-Beispiel gibt ein Dict auf oberster Ebene zurück, das einen data Schlüssel, der eine Liste von Listen enthält. Echte APIs variieren: Einige geben eine Liste von Objekten zurück, andere verpacken die Zeilen unter results oder items, andere verbergen sie zwei Ebenen tief unter payload.table.rows. Überprüfen Sie die Struktur einmal und schreiben Sie dann defensiven Code.

rows = payload.get("data", [])
records = []
for r in rows:
    records.append({
        "name":       r[0],
        "position":   r[1],
        "office":     r[2],
        "extn":       r[3],
        "start_date": r[4],
        "salary":     r[5],
    })

Wenn der Endpunkt eine Liste von Objekten anstelle von positionellen Arrays zurückgibt, tauschen Sie die Indizes für r.get("name"), r.get("position")und so weiter. Verwenden Sie .get() anstelle von r["name"] erspart dir einen KeyError , falls das Backend eines Tages ein Feld hinzufügt oder umbenennt. Führen Sie diese Zuordnung einmalig an einer Stelle durch, damit der Rest der Pipeline mit einem stabilen internen Schema kommuniziert, anstatt mit dem, was die vorgelagerte API diese Woche gerade ausgibt.

Behandeln Sie Paginierung, Abfrageparameter und Authentifizierung

Echte Endpunkte liefern Ihnen selten jede Zeile in einem einzigen Aufruf. Das serverseitige DataTables-Protokoll verwendet draw, start, length, order[0][column], und search[value]; die kanonische Parameterliste finden Sie im Handbuch zur serverseitigen Verarbeitung von DataTables. Andere Backends verwenden Cursor-Paginierung (?cursor=eyJ...), Offset-Paginierung (?page=3&per_page=100) oder ein next_url in die Antwort eingebettetes Feld.

import time

session = requests.Session()
session.headers.update(headers)

start, length, rows = 0, 100, []
while True:
    r = session.get(URL, params={"draw": 1, "start": start, "length": length}, timeout=15)
    if r.status_code == 429:
        time.sleep(2 ** (start // length))  # crude exponential backoff
        continue
    r.raise_for_status()
    page = r.json().get("data", [])
    if not page:
        break
    rows.extend(page)
    start += length

Wenn der Endpunkt hinter einer Anmeldung liegt, melden Sie sich zuerst an mit session.post() und lassen Sie das Cookie-Jar die Sitzung verwalten. Bei CSRF-geschützten POST-Anfragen extrahieren Sie das Token aus einem versteckten Eingabefeld oder einem XSRF-TOKEN Cookie und leiten Sie es als Header weiter. Fügen Sie niemals eine statische Cookie-Zeichenkette ein. Diese läuft über Nacht ab und unterbricht danach jeden Cron-Lauf.

Exportieren Sie Zeilen in CSV, JSON Lines oder SQLite

Wählen Sie das Ausgabeformat, das Ihre nachgelagerten Tools tatsächlich verarbeiten. CSV eignet sich gut für Tabellenkalkulationen, JSON Lines ist besser für die Streaming-Erfassung und LLM- oder RAG-Pipelines geeignet, und SQLite ist die leichteste, analytikerfreundliche Option, die einen Neustart übersteht.

import csv, json, sqlite3

# CSV with named headers (clearer than raw csv.writer)
with open("rows.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(f, fieldnames=records[0].keys())
    writer.writeheader()
    writer.writerows(records)

# JSON Lines
with open("rows.jsonl", "w", encoding="utf-8") as f:
    for r in records:
        f.write(json.dumps(r, ensure_ascii=False) + "\n")

# SQLite
con = sqlite3.connect("rows.db")
con.execute("CREATE TABLE IF NOT EXISTS staff (name TEXT, position TEXT, office TEXT, extn TEXT, start_date TEXT, salary TEXT)")
con.executemany("INSERT INTO staff VALUES (:name, :position, :office, :extn, :start_date, :salary)", records)
con.commit(); con.close()

csv.DictWriter Die paar zusätzlichen Zeilen lohnen sich, da die Kopfzeile mit den Dict-Schlüsseln synchron bleibt; niemand muss sich merken, welche Spalte der Index war 3. Die gleiche records Liste versorgt alle drei Writer, sodass das Wechseln des Formats in der Produktion nur eine Änderung von einer Zeile erfordert.

Fallback: Rendern Sie die Tabelle mit Playwright, wenn das Netzwerk unterbrochen ist

Manche Websites lassen einen wirklich nicht an das JSON heran. Signierte URLs, die nach Sekunden ablaufen, GraphQL-Endpunkte mit strengen Origin Prüfungen, über WebSocket gespeiste Raster und eine Handvoll maßgeschneiderter Setups zwingen einen dazu, die Seite in einem echten Browser zu rendern. Playwright für Python ist hierfür eine starke, moderne Standardwahl, obwohl Selenium auf älteren Stacks immer noch eine vernünftige Wahl ist.

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=True)
    page = browser.new_page()
    page.goto("https://example.com/grid", wait_until="networkidle")
    page.wait_for_selector("table.grid tbody tr")
    rows = page.locator("table.grid tbody tr").all_text_contents()
    browser.close()

Eine Falle, auf die man bei jedem Fallback für das Web-Scraping von JavaScript-Tabellen achten sollte: Clientseitige Grid-Bibliotheken wie DataTables, AG Grid und TanStack Table virtualisieren häufig das Rendering, was bedeutet, dass zu jedem Zeitpunkt nur die Zeilen, die aktuell im Viewport sichtbar sind, im DOM geladen werden. Die genaue Zeilenanzahl hängt von der Viewport-Größe und der Bibliothekskonfiguration ab, vertrauen Sie also nicht auf naive tr , dass die Sammlung alles erfasst. Scrolle den Container in einer Schleife, höre auf neue Zeilen mit einem MutationObserveroder rufen Sie die bibliotheksinterne Paginierungs-API auf, bis die Gesamtzahl der Zeilen nicht mehr zunimmt.

Häufige Fallstricke beim Web-Scraping von JavaScript-Tabellen

Die meisten Fehler beim Web-Scraping von JavaScript-Tabellen treten unbemerkt auf. Das Skript läuft, die Datei wird geschrieben, und niemand bemerkt, dass die Daten falsch sind, bis ein Dashboard dies anzeigt. Achten Sie auf Folgendes:

  • Die Auswahl von Tabellen nach Index tables[2] bricht in dem Moment zusammen, in dem das Marketing ein Vergleichs-Widget über dem Raster hinzufügt. Verwenden Sie stattdessen den Text der Beschriftung, die ID oder eine eindeutige Kopfzeile als Abgleichkriterium.
  • Virtualisierte Raster. Ein naives Scraping von DataTables, AG Grid oder TanStack Table kann nur die sichtbaren Zeilen im Viewport erfassen, während Tausende unberücksichtigt bleiben. Überprüfen Sie die Zeilenzahlen anhand einer API-Zählung oder einer paginierten Anfrage.
  • Länderspezifisch formatierte Zahlen. 1.000,50 ist europäisch für 1000.50, aber Python float() liest es als 1.0. Normalisieren Sie die Zeichenkette vor der Umwandlung.
  • Zeitzonen in Datumsangaben. "2025-04-01" Werden ohne Zeitzone analysiert, wird stillschweigend Mitternacht UTC angenommen, wodurch tägliche Aggregate um eine Zeile verschoben werden.
  • Währungssymbole und Tausendertrennzeichen. "$1,234" werden nicht in einen Float umgewandelt. Entferne zuerst nicht-numerische Zeichen.
  • Abgelaufene Cookies. Ein eingefügtes Session-Cookie funktioniert einen Tag lang, gibt dann stillschweigend 401-Fehler zurück, die manche Server als HTTP-200-HTML tarnen.
  • Anti-Bot-200er. Eine WAF kann eine Captcha-Herausforderungsseite mit Status 200 zurückgeben. r.json() löst eine Ausnahme aus, aber nur, wenn Sie daran denken, sie aufzurufen.

Validieren und überwachen Sie die Extraktionspipeline

Ein Scrape ist nicht mit „CSV erstellt“ abgeschlossen. Er ist erst abgeschlossen, wenn du der Datei morgen vertraust. Fügen Sie nach dem Writer eine kleine Validierungsschicht ein: Stellen Sie sicher, dass die Zeilenanzahl innerhalb eines vernünftigen Bereichs des gestrigen Laufs liegt, melden Sie lautstark einen Fehler, wenn eine erforderliche Spalte eine Nullrate über einem Schwellenwert aufweist (1 bis 5 Prozent funktionieren), und vergleichen Sie den Spaltensatz mit einem gespeicherten Manifest, damit ein umbenanntes Feld eine Schemaabweichung kennzeichnet, anstatt eine nachgelagerte Verknüpfung zu beeinträchtigen. Melden Sie Läufe mit null Zeilen separat. Die meisten Web-Scraping-Pipelines für JavaScript-Tabellen scheitern an stiller Schrumpfung, nicht an lauten Abstürzen.

Wichtige Erkenntnisse

  • Der Standardpfad für das Web-Scraping von JavaScript-Tabellen ist der versteckte JSON-Endpunkt, nicht ein Headless-Browser. Verwenden Sie die Entscheidungsleiter, bevor Sie Code schreiben.
  • Der Tab „Netzwerk“ in DevTools in Kombination mit einer ausgelösten Sortier- oder Paginierungsaktion ist der schnellste Weg, um den Aufruf zu identifizieren, der tatsächlich die Zeilen überträgt.
  • Spielen Sie die Anfrage zustandslos nach: öffentliche Header, raise_for_status(), eine echte Sitzung für Anmeldungen und niemals ein manuell eingefügtes persönliches Cookie.
  • Paginierungsmuster variieren (DataTables draw/start/length, Cursor, Offsets); betrachten Sie die Schleife, nicht die einzelne Anfrage, als Arbeitseinheit.
  • Playwright ist das richtige Werkzeug, wenn der Netzwerkpfad signiert, verschlüsselt oder nicht vorhanden ist – und nur dann. Achten Sie auf virtualisierte Raster, die nur Viewport-Zeilen einbinden.
  • Eine Pipeline, die Sie im nächsten Quartal erneut ausführen können, verfügt über Assertions zur Zeilenanzahl, Schwellenwerte für die Null-Rate und ein Spaltenmanifest – nicht nur eine heute funktionierende CSV-Datei.

FAQ

Warum gibt requests.get() leere Zeilen für eine JavaScript-Tabelle zurück?

Weil requests JavaScript nicht ausführt. Es lädt das Dokument herunter, das der Server zuerst bereitstellt, welches die Seitenhülle und ein Skriptbündel enthält, aber keine Zeilen. Die Zeilen werden später durch clientseitigen Code hinzugefügt, der einen JSON-Endpunkt aufruft. Dein Parser sieht die leere <table> und gibt nichts zurück.

Brauche ich wirklich Selenium oder Playwright, um eine dynamische Tabelle zu scrapen?

Normalerweise nicht. Wenn DevTools eine JSON-Anfrage anzeigt, die das Raster füllt, reicht es aus, diese Anfrage mit requests oder httpx wiederzugeben, ist schneller, kostengünstiger und zuverlässiger als ein Browser. Greifen Sie nur dann auf Playwright zurück, wenn der Aufruf signiert ist, GraphQL mit strengen Ursprungsprüfungen verwendet, WebSocket-gesteuert ist oder anderweitig von einem einfachen HTTP-Client aus nicht erreichbar ist.

Wie scrape ich eine JavaScript-Tabelle, für die eine Anmeldung oder ein CSRF-Token erforderlich ist?

Verwenden Sie ein requests.Session , damit Cookies über Aufrufe hinweg erhalten bleiben. Sende deine Anmeldedaten an den Login-Endpunkt, lies dann den CSRF-Wert aus einem versteckten Eingabefeld oder dem XSRF-TOKEN Cookie aus und leiten Sie ihn als Header in der Datenanfrage weiter. Codieren Sie niemals ein aus Ihrem eigenen Browser kopiertes Sitzungs-Cookie fest ein.

Was ist, wenn die versteckte API jeweils nur eine Seite mit Zeilen zurückgibt?

Führen Sie eine Schleife durch. Überprüfen Sie die Anfrageparameter (start, length, cursor, page, offset) und inkrementiere sie, bis die Antwort null Zeilen oder ein has_more: false Flag zurückgibt. Füge bei HTTP 429 einen exponentiellen Backoff und eine feste Anforderungsbegrenzung hinzu, damit ein serverseitiger Fehler deinen Scraper nicht in eine Endlosschleife versetzt.

Fazit

Das Web-Scraping von JavaScript-Tabellen ist nicht mehr beängstigend, sobald du aufhörst, die gerenderte Seite als die Quelle der Wahrheit zu betrachten. Der Browser ist ein Renderer; der JSON-Endpunkt hinter dem Raster ist die eigentliche Datenquelle. Finde diesen Endpunkt in DevTools, spiele ihn mit requests, paginiere ihn ordnungsgemäß, validiere die Ausgabe, und du hast ein Skript, das das nächste Redesign übersteht, anstatt eines, das still und leise dein Warehouse mit leeren Zeilen füllt.

Heben Sie sich den Headless-Browser für die Fälle auf, in denen er wirklich benötigt wird. Websites mit signierten Netzwerkaufrufen, WebSocket-gespeisten Rastern oder aggressivem Bot-Schutz werden Sie dorthin treiben, und genau dort kommt es auf einen Fallback-Pfad an. Wenn Sie doch auf einen Browser zurückgreifen, gehen Sie bewusst mit virtualisiertem Rendering um, validieren Sie die Zeilensummen und behalten Sie Ihre Überwachungsebene bei.

Wenn Sie Proxy-Rotation, Browser-Fingerabdrücke und CAPTCHA-Handler lieber nicht selbst verwalten möchten, kann WebScrapingAPI vor Ihrem bestehenden requests Code schalten und sauberes HTML oder JSON von Websites zurückgeben, die sonst den direkten Zugriff blockieren, wobei die Parsing- und Paginierungslogik oben unverändert bleibt. Welchen Weg Sie auch wählen, das Spielbuch ist dasselbe: Wählen Sie den günstigsten Extraktionspfad, der funktioniert, und gestalten Sie das Skript so ehrlich, dass es Ihnen mitteilt, wenn es nicht mehr funktioniert.

Über den Autor
Andrei Ogiolan, Full-Stack-Entwickler @ WebScrapingAPI
Andrei OgiolanFull-Stack-Entwickler

Andrei Ogiolan ist Full-Stack-Entwickler bei WebScrapingAPI, wo er in verschiedenen Bereichen des Produkts mitwirkt und dabei hilft, zuverlässige Tools und Funktionen für die Plattform zu entwickeln.

Los geht’s

Sind Sie bereit, Ihre Datenerfassung zu erweitern?

Schließen Sie sich den über 2.000 Unternehmen an, die WebScrapingAPI nutzen, um Webdaten im Unternehmensmaßstab ohne zusätzlichen Infrastrukturaufwand zu extrahieren.