Sie schreiben einige Zeilen Python, richten requests auf eine Produktliste oder eine Suchergebnisseite, übergeben die Antwort an BeautifulSoup und erhalten fast nichts zurück. Der Titel ist da, das Layout ist da, aber die Daten, die Sie eigentlich wollten, fehlen. Das ist die häufigste Wand, gegen die Menschen stoßen, wenn sie versuchen, JavaScript-Seiten mit Python zu scrapen: Die Seite rendert ihren Inhalt im Browser, nachdem das initiale HTML angekommen ist, sodass ein einfacher HTTP-Abruf immer nur die leere Hülle sieht.

Dieser Leitfaden erklärt, warum das passiert, geht die drei echten Wege durch, um die gerenderten Daten zu erhalten (Headless-Browser, die zugrunde liegende JSON-API und eine Rendering-API), und zeigt ein sauberes, lauffähiges Beispiel, das eine fertige Seite über die Crawling API abruft und sie mit BeautifulSoup parst. Am Ende wissen Sie, welcher Ansatz zu welchem Job passt und wie Sie einen Lauf vor dem Blockieren schützen.

Warum requests plus BeautifulSoup eine leere Hülle zurückgibt

Um das Problem zu sehen statt nur darüber zu lesen, rufen Sie eine clientseitig gerenderte Seite auf die naive Weise ab und schauen Sie, was zurückkommt.

python
import requests
from bs4 import BeautifulSoup

url = "https://example-shop.com/search?q=smartwatch"
html = requests.get(url).text
soup = BeautifulSoup(html, "html.parser")

products = soup.select("[data-product-title]")
print(f"found {len(products)} products")
# found 0 products

Status-Code 200, ein vollständig aussehendes HTML-Dokument und null Produkte. Der Grund liegt im Lebenszyklus der Seite. Der Server sendet ein leichtes HTML-Skelett: einige div-Mount-Punkte, ein paar <script>-Tags, vielleicht einen Lade-Spinner. Erst wenn diese Skripte ausgeführt werden, ruft der Browser eine API auf, empfängt die Produktdaten als JSON und erstellt die DOM-Knoten, die diese enthalten. Die requests-Bibliothek führt kein JavaScript aus. Sie lädt das Skelett herunter und hört auf, sodass die Produkt-Knoten für BeautifulSoup nie existieren.

Die Lösung für jeden der folgenden Ansätze ist im Prinzip dieselbe: die Seite in einen Zustand bringen, in dem JavaScript bereits ausgeführt wurde, und dann diesen Zustand parsen. Die Ansätze unterscheiden sich nur darin, wie sie diesen gerenderten Zustand erreichen und was es Sie an Geschwindigkeit, Infrastruktur und den Chancen des Blockiert-Werdens kostet.

Schnell erkennen

Klicken Sie mit der rechten Maustaste auf die Seite und wählen Sie "Seitenquelltext anzeigen", um den rohen HTML-Code zu sehen, den der Server gesendet hat, den genau requests empfängt. Öffnen Sie dann die Entwicklertools und schauen Sie sich das Elements-Panel an, das das Live-DOM nach dem Ausführen der Skripte zeigt. Wenn Ihre Zieldaten im Elements-Panel erscheinen, aber nicht im Quelltext, ist die Seite clientseitig gerendert und ein einfacher Abruf funktioniert nicht.

Ansatz 1: einen echten Browser mit Selenium oder Playwright steuern

Die direkteste Lösung ist die Verwendung eines Tools, das tatsächlich einen Browser ausführt. Selenium und Playwright starten beide Chromium (Headless oder sichtbar), laden die URL, warten auf das Beenden der Skripte und ermöglichen das Lesen des gerenderten DOM. Da ein echtes Browser-Engine JavaScript ausführt, sind die Daten, die bei einem einfachen Abruf fehlten, jetzt vorhanden.

Ein minimales Playwright-Beispiel sieht so aus:

python
from playwright.sync_api import sync_playwright
from bs4 import BeautifulSoup

with sync_playwright() as p:
    browser = p.chromium.launch(headless=True)
    page = browser.new_page()
    page.goto("https://example-shop.com/search?q=smartwatch")
    page.wait_for_selector("[data-product-title]")
    html = page.content()
    browser.close()

soup = BeautifulSoup(html, "html.parser")
titles = [t.get_text(strip=True) for t in soup.select("[data-product-title]")]
print(titles)

Die entscheidende Zeile ist wait_for_selector. Statt mit einem festen sleep zu raten, weisen Sie den Browser an zu warten, bis das gesuchte Element tatsächlich existiert, was sowohl schneller als auch zuverlässiger ist. Selenium bietet dieselbe Idee durch seine WebDriverWait- und Expected-Conditions-Helfer.

Dieser Ansatz funktioniert und ist das richtige Werkzeug, wenn Sie klicken, scrollen, Formulare ausfüllen oder mehrseitige Abläufe durchgehen müssen. Er bringt jedoch echte Kosten mit sich. Jede Browser-Instanz frisst Hunderte von Megabyte RAM und einen ganzen CPU-Kern, sodass das parallele Ausführen vieler teuer ist. Die Einrichtung ist umständlich: Sie verwalten Browser-Binärdateien, Treiber-Versionen und eine fragile Abhängigkeitskette. Und Rendering allein macht Sie nicht unsichtbar. Ein Headless-Browser von einer Rechenzentrum-IP mit einem Standard-Automatisierungs-Fingerabdruck wird von ernsthaften Anti-Bot-Systemen genauso schnell markiert und blockiert wie eine rohe Anfrage. Rendering löst das JavaScript-Problem; es tut nichts für das Erkennungsproblem. Für einen umfassenderen Vergleich der Browser-Engines lesen Sie einen Headless-Browser für Web-Scraping auswählen und diese Anleitung zum Scrapen dynamischer Inhalte mit Selenium und BeautifulSoup.

Ansatz 2: den Browser überspringen und die zugrunde liegende API aufrufen

Hier ist die Erkenntnis, die die meisten Tutorials verpassen. Wenn eine clientseitig gerenderte Seite sich selbst aufbaut, holt sie ihre Daten fast immer von einem Backend-JSON-Endpunkt. Wenn Sie diesen Endpunkt finden können, können Sie ihn direkt aufrufen und das Rendering vollständig überspringen, und erhalten sauberes strukturiertes JSON ohne Browser.

Um ihn zu finden, öffnen Sie die Entwicklertools, gehen Sie zum Netzwerk-Tab, filtern Sie nach Fetch/XHR und laden Sie die Seite neu. Sie suchen nach einer Anfrage, deren Antwort Ihre Daten enthält, typischerweise eine URL mit /api/, /graphql oder einem abfrageintensiven Pfad. Sobald Sie ihn gefunden haben, replizieren Sie ihn in Python.

python
import requests

api = "https://example-shop.com/api/search"
params = {"q": "smartwatch", "page": 1}
headers = {"Accept": "application/json"}

data = requests.get(api, params=params, headers=headers).json()
for item in data["results"]:
    print(item["title"], item["price"])

Wenn das funktioniert, ist es bei weitem die effizienteste Option: kein Browser-Overhead, strukturierte Daten statt HTML, das Sie parsen müssen, und eingebaute Paginierung durch die eigenen Parameter der API. Es ist immer zehn Minuten im Netzwerk-Tab wert, bevor Sie zu etwas Schwergewichtigerem greifen.

Der Haken ist, dass es nicht immer funktioniert. Der Endpunkt kann ein signiertes Token, ein Session-Cookie oder einen spezifischen Header-Satz erfordern, den die Seite dynamisch generiert. Er kann durch dieselbe Anti-Bot-Schicht wie die Seite selbst geschützt sein. Und er kann sich ohne Vorankündigung ändern, da eine interne API keine Stabilitätsgarantie trägt. Wenn die API erreichbar ist, nehmen Sie sie. Wenn sie gesperrt ist, sind Sie wieder auf eine gerenderte Seite angewiesen, was uns zum dritten Ansatz bringt.

Ansatz 3: durch die Crawling API rendern und das Ergebnis parsen

Die beiden vorherigen Ansätze lösen jeweils die Hälfte des Problems. Ein Headless-Browser rendert, aber versteckt Sie nicht. Ein direkter API-Aufruf ist sauber, wird aber oft blockiert. Was Sie normalerweise wollen, ist beides gleichzeitig: ein echter Browser, der das JavaScript der Seite ausführt, hinter einer IP, die die Seite als echten Besucher erkennt, der fertiges HTML in einem einzigen Aufruf zurückgibt, sodass Ihr Python einfach bleibt.

Das ist es, was die Crawling API tut. Sie senden ihr eine URL mit einem JavaScript-Token, sie lädt die Seite in einem echten Browser auf ihrer Seite, rotiert serverseitig durch Residential-IPs und gibt das vollständig gerenderte HTML zurück. Sie betreiben nie einen Browser-Fleet oder pflegen einen Proxy-Pool; Sie stellen eine HTTP-Anfrage und parsen die Antwort mit demselben BeautifulSoup, das Sie bereits kennen.

Warum das JS-Token

Crawlbase bietet zwei Token-Typen. Das normale Token ruft statisches HTML ab; das JavaScript-Token (JS-Token) rendert die Seite zuerst in einem echten Browser. Für ein clientseitig gerendertes Ziel benötigen Sie das JS-Token, sonst erhalten Sie dieselbe leere Hülle zurück wie bei einer einfachen Anfrage und es gibt nichts zu parsen.

Installieren Sie den offiziellen Client und BeautifulSoup, dann rufen Sie die gerenderte Seite ab.

bash
python -m venv scraper_env
source scraper_env/bin/activate

pip install crawlbase beautifulsoup4

Unter Windows aktivieren Sie die Umgebung mit scraper_env\Scripts\activate statt der source-Zeile. Rufen Sie nun die Seite mit dem JS-Token und den zwei Wait-Optionen ab, die für clientseitig gerendernte Inhalte wichtig sind.

python
from crawlbase import CrawlingAPI

api = CrawlingAPI({"token": "YOUR_CRAWLBASE_JS_TOKEN"})

def crawl(page_url):
    options = {"ajax_wait": "true", "page_wait": 5000}
    response = api.get(page_url, options)
    if response["status_code"] == 200:
        return response["body"].decode("utf-8")
    print(f"Request failed: {response['status_code']}")
    return None

if __name__ == "__main__":
    page_url = "https://example-shop.com/search?q=smartwatch"
    html = crawl(page_url)
    print(html[:500] if html else "No HTML returned")

Die zwei Wait-Optionen erledigen die Arbeit für ein clientseitig gerendertes Ziel. ajax_wait weist die API an, auf das Abklingen der asynchronen Anfragen zu warten, bevor die Seite erfasst wird, und page_wait hält für eine feste Anzahl von Millisekunden nach dem Laden an, damit spät gerenderte Elemente erscheinen. Fünf Sekunden sind ein vernünftiger Ausgangswert; erhöhen Sie ihn, wenn Felder leer zurückkommen. Führen Sie das aus und Sie sollten in den ersten 500 Zeichen echtes Markup sehen, nicht das Skelett, das ein einfacher Abruf zurückgibt. Das bestätigt, dass das Rendering funktioniert, bevor Sie einen einzigen Selektor schreiben.

Crawlbase Crawling API

Eine JavaScript-Seite hinter einer vertrauenswürdigen IP zu rendern, in einem Aufruf, ist genau das, wofür die Crawling API gemacht ist. Übergeben Sie ein JS-Token, sie führt die Seite in einem echten Browser aus, rotiert serverseitig durch Residential-IPs und gibt fertiges HTML zurück, sodass Sie keinen eigenen Headless-Fleet und keinen Proxy-Pool betreiben müssen. Probieren Sie es zunächst mit einer echten Seite im kostenlosen Kontingent.

Das gerenderte HTML mit BeautifulSoup parsen

Sobald crawl gerendertes HTML zurückgibt, ist der Parse-Schritt gewöhnliches BeautifulSoup, weil JavaScript bereits serverseitig ausgeführt wurde und die Daten-Knoten vorhanden sind. Verpacken Sie den Feldzugriff in einen kleinen Helfer, damit ein fehlendes Element den Lauf nicht abbricht.

python
import json
from crawlbase import CrawlingAPI
from bs4 import BeautifulSoup

api = CrawlingAPI({"token": "YOUR_CRAWLBASE_JS_TOKEN"})

def crawl(page_url):
    options = {"ajax_wait": "true", "page_wait": 5000}
    response = api.get(page_url, options)
    if response["status_code"] == 200:
        return response["body"].decode("utf-8")
    return None

def parse_products(html):
    soup = BeautifulSoup(html, "html.parser")
    items = []
    for card in soup.select("div.product-card"):
        title = card.select_one("[data-product-title]")
        price = card.select_one("span.price")
        items.append({
            "title": title.get_text(strip=True) if title else None,
            "price": price.get_text(strip=True) if price else None,
        })
    return items

def main():
    url = "https://example-shop.com/search?q=smartwatch"
    html = crawl(url)
    if not html:
        return
    products = parse_products(html)
    print(json.dumps(products, indent=2))

if __name__ == "__main__":
    main()

Führen Sie es mit python scraper.py aus und Sie erhalten eine saubere strukturierte Liste, bereit zum Schreiben in JSON, CSV oder eine Datenbank.

json
[
  {
    "title": "Aero Fit Smartwatch 2",
    "price": "$129.00"
  },
  {
    "title": "Pulse Sport Band Pro",
    "price": "$89.99"
  }
]
Selektoren driften

Klassennamen und Daten-Attribute ändern sich bei Redesigns von Seiten, sodass ein Selektor, der letzten Monat funktionierte, heute nichts mehr zurückgeben kann. Wenn ein Feld als None zurückkommt, inspizieren Sie erneut die Live-Seite in den Entwicklertools und aktualisieren Sie den Selektor. Regelmäßige Selektor-Wartung ist normal für jeden produktiven Scraper, kein Zeichen dafür, dass etwas kaputt ist.

Häufige Fallstricke beim Scrapen von JavaScript-Seiten

Einige Probleme sind für die meisten gescheiterten Läufe gegen clientseitig gerenderte Ziele verantwortlich. Sie im Voraus zu kennen spart viel Debugging.

  • Zu früh erfassen. Der häufigste Fehler ist das Parsen, bevor der Inhalt existiert. Bevorzugen Sie das Warten auf einen bestimmten Selektor oder, mit der Crawling API, setzen Sie auf ajax_wait und ein großzügiges page_wait statt auf eine blinde feste Verzögerung.
  • Inhalt hinter Interaktion. Manche Daten erscheinen erst nach einem Scroll, einem Tab-Klick oder einem "Mehr laden"-Druck. Ein direkter Abruf oder ein einzelnes Rendering löst das nicht aus. Hier verdient ein Browser, den Sie schrittweise steuern, oder das Rendering mit einer Scroll-Anweisung, seine Kosten.
  • Lazy-geladene und paginierte Listen. Infinite-Scroll-Seiten laden in Blöcken, während Sie scrollen. Steuern Sie entweder den Scroll in einem Browser oder, besser, finden Sie die paginierte API dahinter und fordern Sie jede Seite direkt an.
  • Blockiert werden trotz Rendering. Rendering ist keine Tarnung. Eine Rechenzentrum-IP oder ein offensichtlicher Automatisierungs-Fingerabdruck wird trotzdem herausgefordert. Residential-IP-Rotation ist das, was einen Lauf tatsächlich am Leben erhält.

Einen Ansatz wählen

Es gibt kein einziges richtiges Werkzeug, nur das richtige Werkzeug für die Aufgabe vor Ihnen.

Versuchen Sie zunächst einen direkten API-Aufruf. Wenn der Netzwerk-Tab einen offenen JSON-Endpunkt offenbart, ist das der sauberste und schnellste Weg, ohne jeden Rendering-Overhead. Prüfen Sie das immer, bevor Sie zu etwas Schwergewichtigerem greifen.

Nutzen Sie einen geskripteten Browser, wenn Sie Interaktion benötigen. Logins, mehrstufige Formulare, Klicks und scroll-ausgelöste Inhalte erfordern alle Selenium oder Playwright, wo Sie die Session schrittweise steuern. Akzeptieren Sie die RAM- und Einrichtungskosten als Preis dieser Kontrolle.

Nutzen Sie eine Rendering-API, wenn Sie fertiges HTML in großem Maßstab ohne Blockiert-Werden benötigen. Wenn die Aufgabe lautet "viele JavaScript-Seiten zuverlässig abrufen und parsen", entfernt die Crawling API die zwei schwierigsten Teile, Browser betreiben und IPs rotieren, und lässt Ihnen einen HTTP-Aufruf plus BeautifulSoup. Wenn Sie lieber Ihren eigenen Browser-Traffic durch einen rotierenden Pool leiten möchten, gibt Ihnen der Smart AI Proxy (auch AI Proxy genannt) Residential-Rotation als Drop-in-Proxy-Endpunkt. Für eine umfassendere Tour dieser Muster lesen Sie wie man JavaScript-Websites crawlt.

Zusammenfassung

Wichtigste Erkenntnisse

  • Einfache Abrufe sehen nur das Skelett. requests führt kein JavaScript aus, sodass clientseitig gerenderte Daten im heruntergeladenen HTML fehlen.
  • Drei echte Lösungen existieren. Einen echten Browser steuern, die zugrunde liegende JSON-API direkt aufrufen oder durch eine API rendern, die fertiges HTML zurückgibt.
  • Suchen Sie zuerst nach einer offenen API. Ein direkter JSON-Endpunkt ist der schnellste, sauberste Weg, wenn er erreichbar ist, ohne Rendering-Kosten.
  • Rendering ist keine Tarnung. Ein Headless-Browser auf einer Rechenzentrum-IP wird trotzdem blockiert; Residential-IP-Rotation ist das, was einen Lauf am Leben erhält.
  • Die Crawling API fasst beides zusammen. Ein JS-Token rendert die Seite hinter einer vertrauenswürdigen IP in einem Aufruf; ajax_wait und page_wait steuern, wie lange sie wartet, bevor BeautifulSoup das Ergebnis parst.

Häufig gestellte Fragen

Warum gibt requests keine Daten auf einer JavaScript-Seite zurück?

Weil requests nur das HTML herunterlädt, das der Server sendet, und JavaScript nie ausführt. Eine clientseitig gerenderte Seite sendet ein dünnes Skelett und erstellt dann ihren eigentlichen Inhalt im Browser, indem sie nach dem Laden eine API aufruft. Da dieser Schritt bei einem einfachen Abruf nie stattfindet, existieren die Daten-Knoten nicht, wenn BeautifulSoup die Antwort parst, sodass Ihre Selektoren nichts finden.

Brauche ich immer einen Headless-Browser, um JavaScript-Seiten mit Python zu scrapen?

Nein. Ein Headless-Browser ist eine Option, aber oft die schwergewichtigste. Bevor Sie Selenium oder Playwright starten, öffnen Sie den Netzwerk-Tab und suchen Sie nach dem JSON-Endpunkt, den die Seite aufruft. Wenn er erreichbar ist, ist das direkte Aufrufen mit requests schneller und sauberer. Greifen Sie zu einem Browser oder einer Rendering-API nur, wenn kein offener Endpunkt verfügbar ist oder die Daten Interaktion erfordern.

Was ist der Unterschied zwischen ajax_wait und page_wait?

ajax_wait weist die Crawling API an zu warten, bis die asynchronen (XHR/fetch) Anfragen der Seite abgeklungen sind, bevor das HTML erfasst wird, was die clientseitig gerenderten Daten füllt. page_wait fügt eine feste Verzögerung in Millisekunden nach dem Laden hinzu, was spät gerenderten Elementen zusätzliche Zeit gibt, um zu erscheinen. Verwenden Sie beides für clientseitig gerenderte Ziele und erhöhen Sie page_wait, wenn Felder leer zurückkommen.

Warum wird mein Headless-Browser trotzdem blockiert?

Weil Rendering und Tarnung separate Probleme sind. Das Ausführen eines echten Browsers löst das JavaScript-Ausführungsproblem, aber die Anfrage kommt immer noch von einer erkennbaren IP und einem Automatisierungs-Fingerabdruck. Anti-Bot-Systeme markieren Rechenzentrum-IPs und Standard-Headless-Signaturen unabhängig vom Rendering. Das Rotieren von Residential-IPs, das die Crawling API und Smart AI Proxy bereitstellen, adressiert die Blocking-Seite.

Kann ich BeautifulSoup mit der Crawling API verwenden?

Ja, und das ist der beabsichtigte Workflow. Die Crawling API gibt vollständig gerendertes HTML zurück, sodass Sie es mit BeautifulSoup genau wie jede statische Seite parsen. Der Unterschied ist, dass JavaScript bereits serverseitig ausgeführt wurde, sodass die Daten-Knoten, auf die Ihre Selektoren abzielen, im empfangenen HTML vorhanden sind.

Wie scrapt man JavaScript-Seiten, die beim Scrollen mehr Inhalt laden?

Infinite-Scroll-Seiten laden in Blöcken, während der Benutzer scrollt, sodass ein einzelner Abruf oder ein einzelnes Rendering nur den ersten Stapel erfasst. Sie haben zwei Optionen: den Scroll in Selenium oder Playwright scripen und auf jeden Stapel warten, oder die paginierte API, die der Scroll auslöst, im Netzwerk-Tab finden und jede Seite direkt anfordern. Der direkte API-Weg ist in der Regel schneller und zuverlässiger, wenn der Endpunkt erreichbar ist.

Jetzt loslegen

Crawlen Sie jede Website im großen Maßstab, ohne gegen die Infrastruktur zu kämpfen.

Crawlbase übernimmt Proxys, Fingerprints und CAPTCHAs, damit Ihr Team Datenpipelines ausliefert, statt Crawl-Infrastruktur zu pflegen. 1.000 Anfragen kostenlos, keine Karte erforderlich.

Self-Service · Kein Verkaufsgespräch erforderlich · Enterprise-Crawl-Volumen verfügbar