Eine einzelne Seite aus einer modernen Web-App zu scrapen ist eine Sache. Die gesamte Website zu crawlen ist eine andere, und sie ist schwieriger als die meisten Tutorials zugeben. Wenn man daran geht, JavaScript-Websites zu crawlen, die mit React, Vue, Angular oder einem anderen Framework gebaut sind, das die Seite im Browser befüllt, stößt man auf zwei Hindernisse, die sich gegenseitig verstärken. Jede Seite zeigt ihren eigentlichen Inhalt erst nach dem Ausführen von JavaScript, und die Navigation, der man normalerweise folgen würde, um weitere Seiten zu entdecken, wird selbst durch JavaScript gezeichnet, sodass ein einfacher HTTP-Fetch ein nahezu leeres Dokument ohne Links zum Folgen liefert.
Diese Anleitung zeigt, wie man einen funktionierenden Crawler erstellt, der eine JavaScript-gerenderte Website von Anfang bis Ende durchläuft. Man rendert jede Seite, damit ihre Links und Inhalte erscheinen, parsed diese Links mit BeautifulSoup, führt eine Frontier-Warteschlange und ein visited-Set, damit die Traversierung terminiert, und drosselt höflich, um willkommen zu bleiben. Der Abruf erfolgt über die Crawlbase Crawling API mit einem JavaScript-Token, das jede Seite hinter einer vertrauenswürdigen IP rendert und fertiges HTML zurückgibt. Für große Jobs behandeln wir auch den asynchronen Crawler, damit man nicht auf jedes Rendern blockiert.
Warum das Crawlen einer JS-Website zwei Probleme sind, nicht eines
Ein traditioneller Crawler ist eine enge Schleife: eine URL abrufen, die Anker extrahieren, die neuen in eine Warteschlange einreihen, wiederholen. Diese Schleife setzt voraus, dass das abgerufene HTML bereits sowohl den Inhalt als auch die Links enthält. Bei einer server-gerenderten Website tut es das. Bei einer client-gerenderten Website tut es das nicht.
Das erste Problem ist Rendering. Wenn man eine React- oder Vue-Route mit einem einfachen HTTP-Client anfordert, gibt der Server eine Hülle zurück: ein Root-<div>, ein Bündel Script-Tags und fast nichts anderes. Der Artikeltext, das Produktraster, die gewünschte Tabelle, all das wird erst injiziert, nachdem der Browser das JavaScript heruntergeladen und ausgeführt hat. Kein Browser, kein Inhalt.
Das zweite Problem ist Link-Entdeckung, und es ist das, das naive Crawler still bricht. Die Navigation, Paginierung und „Verwandte"-Links der Website sind oft auch client-seitig gerendert. Selbst wenn man nur die Links und nicht den Inhalt wollte, gibt ein einfacher Fetch immer noch nichts zum Folgen. Der Crawl stirbt auf der ersten Seite, weil die Frontier nie über sie hinauswächst. Um eine JavaScript-Website zu crawlen, muss man jede Seite rendern, nicht weil man immer den Body braucht, sondern weil Rendering das ist, was die Links überhaupt erst existieren lässt.
Die einzige Regel, die JS-Crawling zum Funktionieren bringt: Jede Seite rendern, bevor man nach Links sucht. Inhalt und Navigation erscheinen im selben Render-Durchlauf, sodass man nach fertigem HTML sowohl die gewünschten Daten als auch die als Nächstes zu folgenden URLs mit demselben Parser extrahieren kann.
Was Sie bauen werden
Ein Breadth-First-Crawler in Python, der von einer Seed-URL auf einer JavaScript-gerenderten Website startet und nach außen geht, innerhalb einer Domain bleibend. Konkret wird er:
- Jede Seite rendern über die Crawling API mit einem JS-Token, sodass Inhalt und Links beide vorhanden sind.
- Links extrahieren aus dem gerenderten HTML mit BeautifulSoup und sie zu absoluten, gleich-domain URLs normalisieren.
- Eine Frontier-Warteschlange zu besuchender URLs und ein visited-Set verwalten, sodass nichts zweimal abgerufen wird und die Traversierung terminiert.
- Höflich drosseln mit einer Verzögerung zwischen Anfragen und einer Obergrenze für besuchte Seiten.
Voraussetzungen
Ein paar Dinge müssen vorhanden sein, bevor man Code schreibt. Keines davon dauert lange.
Grundlegendes Python. Man sollte in der Lage sein, ein Skript auszuführen und Pakete mit pip zu installieren. Wenn Warteschlangen und Sets vertraut sind, ist man bereit.
Python 3.8 oder höher. Prüfen Sie mit python --version. Von python.org installieren, falls nicht vorhanden.
Ein Crawlbase-Konto und JS-Token. Melden Sie sich an, öffnen Sie Ihr Dashboard und kopieren Sie Ihr JavaScript (JS)-Token von der Kontodokumentationsseite. Das JS-Token ist dasjenige, das Seiten in einem echten Browser rendert; das normale Token ruft nur statisches HTML ab und würde dieselbe leere Hülle liefern, die ein einfacher Fetch zurückgibt. Das Token aus der Versionskontrolle heraushalten.
Das Projekt einrichten
Eine virtuelle Umgebung erstellen, damit Abhängigkeiten isoliert bleiben, dann die zwei Bibliotheken installieren, die der Crawler benötigt.
python --version python -m venv crawler_env source crawler_env/bin/activate pip install crawlbase beautifulsoup4
Unter Windows mit crawler_env\Scripts\activate statt der source-Zeile aktivieren. Das crawlbase-Paket ist der offizielle Client für die Crawling API, und beautifulsoup4 parsed das zurückgegebene HTML, sodass man sowohl Anker als auch Inhalte extrahieren kann.
Schritt 1: Eine einzelne Seite rendern und bestätigen, dass die Links erscheinen
Bevor man die Schleife baut, den schwierigen Teil beweisen, dass das Rendern einer client-seitigen Seite Links aufdeckt, die ein einfacher Fetch verpassen würde. Den Client mit dem JS-Token initialisieren und eine URL anfordern, die API auffordern, auf asynchronen Inhalt zu warten.
from crawlbase import CrawlingAPI api = CrawlingAPI({"token": "YOUR_CRAWLBASE_JS_TOKEN"}) def render(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__": html = render("https://example.com/") print(len(html) if html else "No HTML returned")
Die zwei Wait-Optionen sind wichtig für client-gerenderte Ziele. ajax_wait teilt der API mit, auf den Abschluss des asynchronen Inhalts zu warten, und page_wait hält für eine feste Anzahl von Millisekunden nach dem Laden an, damit spät-gerenderte Elemente vor der Erfassung erscheinen. Fünf Sekunden sind ein vernünftiger Ausgangspunkt; erhöhen, wenn die Links einer Seite leer zurückkommen. Die Länge dieses gerenderten Bodys mit einem einfachen requests.get auf derselben URL vergleichen und man sieht in der Regel, dass die gerenderte Version weit größer ist, weil Navigation und Inhalt jetzt vorhanden sind.
Schritt 2: Links extrahieren und normalisieren
Mit gerendem HTML die Anker herausziehen und sie in saubere, absolute URLs umwandeln, die man vergleichen und in die Warteschlange einreihen kann. Zwei Details halten den Crawl vernünftig: relative hrefs gegen die Seite auflösen, von der sie stammen, und URL-Fragmente entfernen, damit /page und /page#section nicht als zwei Seiten behandelt werden.
from urllib.parse import urljoin, urldefrag, urlparse from bs4 import BeautifulSoup def extract_links(html, base_url, domain): soup = BeautifulSoup(html, "html.parser") links = set() for a in soup.select("a[href]"): href = urljoin(base_url, a["href"]) href, _ = urldefrag(href) parsed = urlparse(href) if parsed.scheme in ("http", "https") and parsed.netloc == domain: links.add(href) return links
Die Gleich-Domain-Prüfung (parsed.netloc == domain) verhindert, dass der Crawler auf externe Websites wandert, was den Unterschied zwischen dem Crawlen einer Website und dem versehentlichen Versuch macht, das gesamte Web zu crawlen. Das Zurückgeben eines set dedupliziert Links, die mehrfach auf einer einzelnen Seite gefunden wurden. Da man diese aus gerendem HTML extrahiert hat, enthalten sie die Links, die JavaScript gezeichnet hat, was genau das ist, was ein Plain-Fetch-Crawler verpasst hätte.
Eine JS-Website zu crawlen bedeutet, jede Seite hinter einer vertrauenswürdigen IP zu rendern, immer wieder, ohne geblockt zu werden. Die Crawling API nimmt ein JS-Token, führt jede Seite in einem echten Browser aus, rotiert serverseitig Residential-IPs und gibt fertiges HTML zurück, sodass sowohl Inhalt als auch Links vorhanden sind. Man überspringt das Betreiben einer Headless-Browser-Flotte und eines Proxy-Pools selbst. Im kostenlosen Tarif starten und auf eine Seed-URL zeigen.
Schritt 3: Die Frontier und das visited-Set verwalten
Nun das Kernstück jedes Crawlers: eine Frontier mit zu besuchenden URLs und ein visited-Set mit bereits gesehenen URLs. Ohne das visited-Set würde eine echte Website voller gegenseitiger Links ewig schleifen; ohne eine Seitenobergrenze würde eine große Website laufen, bis man das Budget aufgebraucht hat. Beide Schutzvorrichtungen gehören in jeden Crawler.
import time from collections import deque def crawl_site(seed_url, max_pages=50, delay=2.0): domain = urlparse(seed_url).netloc frontier = deque([seed_url]) visited = set() pages = [] while frontier and len(visited) < max_pages: url = frontier.popleft() if url in visited: continue visited.add(url) html = render(url) if not html: continue pages.append({"url": url, "html": html}) print(f"[{len(visited)}] crawled {url}") for link in extract_links(html, url, domain): if link not in visited: frontier.append(link) time.sleep(delay) return pages
Ein deque mit popleft gibt Breadth-First-Traversierung, sodass der Crawler über die Website hinausgeht statt tief in einen Zweig einzutauchen. Eine URL als visited zu markieren, sobald man sie aus der Frontier entnimmt (nicht nach dem Abruf), bedeutet, dass eine Seite, die nicht rendert, trotzdem als gesehen gilt, sodass eine unzuverlässige URL die Schleife nicht gefangen halten kann. Die max_pages-Obergrenze und die delay-Verzögerung zwischen Anfragen sind die zwei Höflichkeitshebel; auf die Website und das eigene Budget abstimmen.
Vor dem Crawlen in großem Volumen die robots.txt des Ziels lesen und ihre Disallow-Regeln und Crawl-Delay einhalten. Pythons Standardbibliothek urllib.robotparser kann eine URL gegen die Regeln in wenigen Zeilen prüfen. Höfliches Pacing und das Fernbleiben von nicht erlaubten Pfaden ist das, was einen Crawler willkommen statt geblockt hält.
Schritt 4: Alles zusammensetzen
Den Renderer, den Link-Extraktor und die Frontier-Schleife in ein ausführbares Skript zusammenfügen. Diese Version zieht auch den Seitentitel von jeder gerenderten Seite, damit man echten Inhalt zurückkommen sieht, Beweis, dass Rendering seine Arbeit über die gesamte Traversierung tut.
import json import time from collections import deque from urllib.parse import urljoin, urldefrag, urlparse from crawlbase import CrawlingAPI from bs4 import BeautifulSoup api = CrawlingAPI({"token": "YOUR_CRAWLBASE_JS_TOKEN"}) def render(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 def extract_links(html, base_url, domain): soup = BeautifulSoup(html, "html.parser") links = set() for a in soup.select("a[href]"): href, _ = urldefrag(urljoin(base_url, a["href"])) parsed = urlparse(href) if parsed.scheme in ("http", "https") and parsed.netloc == domain: links.add(href) return links def title_of(html): soup = BeautifulSoup(html, "html.parser") return soup.title.get_text(strip=True) if soup.title else None def crawl_site(seed_url, max_pages=50, delay=2.0): domain = urlparse(seed_url).netloc frontier = deque([seed_url]) visited = set() results = [] while frontier and len(visited) < max_pages: url = frontier.popleft() if url in visited: continue visited.add(url) html = render(url) if not html: continue results.append({"url": url, "title": title_of(html)}) print(f"[{len(visited)}] {url}") for link in extract_links(html, url, domain): if link not in visited: frontier.append(link) time.sleep(delay) return results def main(): pages = crawl_site("https://example.com/", max_pages=25) with open("crawl.json", "w") as f: json.dump(pages, f, indent=2) print(f"Crawled {len(pages)} pages") if __name__ == "__main__": main()
Mit python crawler.py ausführen und man sieht die Frontier wachsen, da jede gerenderte Seite neue Links beisteuert, dann schrumpfen, sobald die Seitenobergrenze erreicht wird. Die Ausgabe ist eine JSON-Datei jeder URL, die der Crawler besucht hat, mit ihrem Titel. title_of durch eine echte Extraktionsfunktion ersetzen und man hat einen vollständigen Content-Crawler. Für eine tiefere Einführung in das Parsen des Bodys einer einzelnen gerenderten Seite, siehe JavaScript-Seiten mit Python scrapen.
Mit dem asynchronen Crawler skalieren
Die synchrone Schleife oben ist perfekt für Dutzende oder niedrige Hunderte von Seiten, hat aber eine strukturelle Decke: Sie blockiert bei jedem Rendern. Jede Seite wartet darauf, dass die API ein vollständiges Browser-Rendering abschließt, bevor die nächste Anfrage überhaupt beginnt, sodass ein Fünf-Sekunden-Rendering über tausend Seiten weit über eine Stunde Echtzeit bedeutet, die mit Warten verbracht wird, meiste davon untätig.
Für größere Jobs zum asynchronen Crawler wechseln. Statt eine Seite abzurufen und zu warten, schiebt man URLs in den Crawler, und Crawlbase rendert sie auf seiner eigenen Infrastruktur und liefert das fertige HTML an einen Webhook-Callback, den man kontrolliert. Der Code hört auf, eine Render-und-Warte-Schleife zu sein, und wird zu zwei entkoppelten Hälften: ein Einreicher, der URLs so schnell wie man sie entdeckt einspeist, und ein Empfänger, der gerenderte Seiten aufnimmt, Links extrahiert und die neuen zurückeinreicht. Man crawlt mit dem Durchsatz der Crawler-Flotte, nicht mit der Latenz eines einzelnen Renderings.
Die bereits geschriebene Crawling-Logik überträgt sich direkt. Die Frontier, das visited-Set, die Gleich-Domain-Prüfung und die Link-Extraktion sind identisch; nur der Transport ändert sich von einem blockierenden api.get-Aufruf zu einem Submit-und-Callback-Ablauf. Für ein vollständiges Muster, siehe Daten mit dem Crawlbase Crawler extrahieren. Wenn der Stack auf der JVM statt Python basiert, lässt sich dasselbe Frontier-und-Visited-Design sauber auf einen Web-Crawler in Java bauen übertragen.
Häufige Fallstricke beim Crawlen von JS-Websites
Einige Fehlermodi tauchen immer wieder auf. Sie im Voraus zu kennen spart viel Debugging.
-
Leere Link-Sets. Wenn
extract_linksauf einer Seite, von der man weiß, dass sie Navigation hat, nichts zurückgibt, hat die Seite wahrscheinlich nicht fertig gerendert.page_waiterhöhen undajax_waiteingeschaltet lassen, damit spät-injizierte Anker beim Parsen vorhanden sind. - Unendliche Frontiers. Kalender, Facettenfilter und Sitzungs-ID-Query-Strings erzeugen endlose eindeutige URLs. Tracking-Parameter normalisieren und URLs jenseits einer Tiefenbegrenzung überspringen, damit der Crawl tatsächlich endet.
-
Off-Site crawlen. Ohne die Gleich-Domain-Schutzvorrichtung verwandelt ein externer Link den Website-Crawl in einen außer Kontrolle geratenen Vorgang. Immer auf
netlocfiltern. -
Den Server überlasten. Keine Verzögerung bedeutet einen Anfrage-Burst, der wie ein Angriff aussieht und eine Sperrung verdient. Einen vernünftigen
delayeinhalten und jede Crawl-Verzögerung in robots.txt respektieren.
Wenn man lieber seinen eigenen Headless-Browser-Traffic durch einen rotierenden Residential-Pool leiten möchte statt die verwaltete API zu verwenden, gibt der Smart AI Proxy dieselbe IP-Rotation als Drop-in-Proxy-Endpunkt, und man übernimmt das Rendering selbst.
Wichtigste Erkenntnisse
- Eine JS-Website zu crawlen sind zwei Probleme. Jede Seite braucht Rendering um Inhalt anzuzeigen, und die gefolgten Links sind auch JS-gebaut, also muss man jede Seite rendern, um die nächsten zu entdecken.
-
Zuerst rendern, dann parsen. Die Crawling API mit einem JS-Token plus
ajax_waitundpage_waitgibt fertiges HTML zurück, sodass Inhalt und Links zusammen ankommen. - Eine Frontier und ein visited-Set sind Pflicht. Eine Breadth-First-Warteschlange, ein Gesehen-URL-Set, ein Gleich-Domain-Filter und eine Seitenobergrenze sind das, was die Traversierung terminieren lässt.
- Höflich sein. Verzögerung zwischen Anfragen, robots.txt einhalten und URLs normalisieren, damit der Crawler nicht auf Tracking-Parametern schleift.
- Mit dem async Crawler skalieren. Für große Jobs URLs einreichen und gerenderte Seiten per Callback empfangen, um mit Flotten-Durchsatz statt auf jedes Rendering blockierend zu crawlen.
Häufig gestellte Fragen
Warum stoppt ein einfacher Crawler nach der ersten Seite auf einer JavaScript-Website?
Weil die Navigations-Links client-seitig gerendert werden. Ein einfacher HTTP-Fetch gibt eine Hülle mit den Skripten zurück, aber keinem der Anker, die das Framework nach dem Ausführen zeichnet, sodass der Link-Extraktor nichts in die Warteschlange einzureihen findet und die Frontier nie wächst. Jede Seite zuerst zu rendern ist das, was diese Links existieren lässt, weshalb das Crawlen einer JS-Website Rendering erfordert, selbst wenn man nur URLs entdecken möchte.
Brauche ich das normale Token oder das JS-Token, um eine JavaScript-Website zu crawlen?
Das JS-Token. Das normale Token ruft statisches HTML ab, was bei einer client-gerenderten Website die leere Hülle ohne Inhalt und ohne gerenderte Links ist. Das JS-Token führt die Seite in einem echten Browser aus, bevor es das HTML zurückgibt, sodass sowohl die Daten als auch die Navigation für Parser und Frontier vorhanden sind.
Wie verhindere ich, dass der Crawler ewig schleift?
Ein visited-Set führen und es vor jedem Abruf prüfen, und eine URL als visited markieren, sobald man sie aus der Frontier entnimmt, nicht nachdem sie erfolgreich ist. Eine max_pages-Obergrenze und einen Gleich-Domain-Filter hinzufügen. Zusammen garantieren diese, dass die Traversierung terminiert, selbst auf einer Website, wo jede Seite auf jede andere verlinkt.
Wie unterscheidet sich Crawlen vom Scrapen einer einzelnen JS-Seite?
Das Scrapen einer einzelnen Seite ist ein Rendern plus ein Parsen für die gewünschten Felder. Crawlen ist dasselbe Rendern-und-Parsen wiederholt über viele Seiten, plus die zusätzliche Maschinerie des Entdeckens von Links, deren Einreihen in die Warteschlange, Deduplizieren und Pacing der Traversierung. Die Rendering-Technik ist geteilt; Crawlen fügt Frontier, visited-Set und Höflichkeitskontrollen obendrauf hinzu.
Wann sollte ich den asynchronen Crawler statt einer synchronen Schleife verwenden?
Zum async Crawler wechseln, wenn das Blockieren auf jedem Rendern zum Engpass wird, typischerweise sobald man mehr als ein paar Hundert Seiten crawlt. Statt auf jedes Rendern in Sequenz zu warten, URLs einreichen und fertige Seiten per Webhook-Callbacks empfangen, sodass man mit dem Durchsatz der Crawlbase-Flotte statt mit der Latenz eines einzelnen Renderings auf einmal crawlt.
Wie crawle ich höflich, ohne geblockt zu werden?
Eine Verzögerung zwischen Anfragen hinzufügen, wie viele Seiten man pro Durchlauf besucht begrenzen und die robots.txt der Website lesen, um ihre Disallow-Regeln und Crawl-Delay einzuhalten. Anfragen über rotierende Residential-IPs leiten, was die Crawling API für einen erledigt, sodass keine einzelne Adresse eine Ratenbegrenzung auslöst. Die Statuscodes beobachten und zurückfahren, wenn Herausforderungen auftauchen.
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.
