Tony Wang9 Min. LesezeitWeb Scraping mit Node.js — fetch, Cheerio und Puppeteer
Scrape mit Node.js: natives fetch und Cheerio für statische Seiten, Puppeteer für JavaScript-Seiten — und wann eine Scraping API die bessere Wahl ist.
Wenn du den ganzen Tag JavaScript schreibst, musst du kein Python lernen, um das Web zu scrapen. Node.js bringt alles mit, was ein einfacher Scraper braucht — fetch ist seit Node 18 eingebaut, Cheerio bietet dir HTML-Parsing im jQuery-Stil über rohes Markup, und Puppeteer steuert ein echtes Chrome, wenn die Seite erst nach dem Ausführen von JavaScript existiert. Dieser Leitfaden baut mit jedem davon einen funktionierenden Scraper, in dieser Reihenfolge, auf einer einzigen konsistenten Übungsseite, und ist dann ehrlich darüber, wo selbst gebaute Node-Scraper in der Produktion an eine Grenze stoßen. Es ist das JavaScript-Gegenstück zu unserem Leitfaden zu Web Scraping mit Python — dieselbe Reise, in deiner Sprache.
Statische Seiten: natives fetch + Cheerio
Die meisten Scraping-Tutorials beginnen damit, einen HTTP-Client zu installieren. Lass das weg — seit Node 18 ist fetch global verfügbar. Die einzige Abhängigkeit für einen Scraper statischer Seiten ist Cheerio:
npm install cheerio
Cheerio 1.x ist ein duales CommonJS/ESM-Paket und will Node 18.17 oder neuer, was gut mit dem nativen fetch zusammenpasst. Wir scrapen books.toscrape.com, eine Sandbox-Buchhandlung, die genau für diese Art von Übung gebaut wurde: 1.000 Bücher, 50 Seiten, kein Login, keine Tricks.
Das Muster ändert sich nie: das HTML abrufen, in Cheerio laden, Elemente mit CSS-Selektoren auswählen und jedes in ein schlichtes Objekt umwandeln.
// scrape-books.mjs — Node 18+, run with: node scrape-books.mjs
import * as cheerio from "cheerio";
import { writeFile } from "node:fs/promises";
const BASE = "https://books.toscrape.com/";
const res = await fetch(BASE);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const html = await res.text();
const $ = cheerio.load(html);
const books = $("article.product_pod")
.map((_, el) => {
const $el = $(el);
const link = $el.find("h3 a");
return {
title: link.attr("title"),
price: $el.find(".price_color").text(),
inStock: $el.find(".availability").text().trim().startsWith("In stock"),
url: new URL(link.attr("href"), BASE).href,
};
})
.get();
await writeFile("books.json", JSON.stringify(books, null, 2));
console.log(`Saved ${books.length} books`);
Ein paar Dinge, die es sich zu beachten lohnt:
cheerio.load(html)gibt$zurück — eine Funktion, die du genau wie jQuery benutzt.$("article.product_pod")wählt jede Buchkarte aus;.find(),.text()und.attr()steigen in jede einzelne hinab..map(...).get()wandelt eine Cheerio-Auswahl in ein echtes JavaScript-Array um. Innerhalb des Callbacks istelein roher DOM-Knoten, also wickle ihn wieder in$()ein, bevor du ihn abfragst.new URL(href, BASE)löst relative Links auf — die Seite verlinkt aufcatalogue/some-book_1/index.html, und das macht daraus eine absolute URL, die du später tatsächlich abrufen kannst.- Prüfe
res.ok, bevor du parst. Eine 404-Seite oder eine Fehlerseite ist ebenfalls HTML; Cheerio parst sie bereitwillig und reicht dir ein leeres Array, was ein viel verwirrenderer Fehler ist als eine geworfene Exception.
Willst du CSV statt JSON? Für eine flache Struktur wie diese brauchst du keine Bibliothek:
const header = "title,price,inStock,url";
const rows = books.map((b) =>
[JSON.stringify(b.title), b.price, b.inStock, b.url].join(",")
);
await writeFile("books.csv", [header, ...rows].join("\n"));
Das JSON.stringify auf dem Titel ist ein günstiger Weg, ein Feld zu zitieren und zu escapen, das Kommas enthalten könnte. Für alles Verschachtelte oder Unordentliche greif zu einem richtigen CSV-Paket — aber greif nicht danach, bevor du es brauchst.
Eine Sache wird Cheerio nie tun: JavaScript ausführen. Es parst das Markup, das der Server gesendet hat, und das ist die ganze Geschichte. Das macht es schnell — kein Browser, kein Rendering — und es ist die richtige Standardwahl für jede Seite, deren Daten im »Seitenquelltext anzeigen« sichtbar sind. Wenn die Daten dort nicht stehen, spring vor zum Puppeteer-Abschnitt.
Pagination und Höflichkeit
Eine Seite ist eine Demo. Ein echter Scraper geht alle 50 Seiten durch, und wie er sie durchgeht, ist der Unterschied zwischen einem nützlichen Werkzeug und einer gesperrten IP. Drei Gewohnheiten zählen: identifiziere dich, drossle dein Tempo und scheitere laut.
// scrape-all.mjs
import * as cheerio from "cheerio";
import { writeFile } from "node:fs/promises";
const CATALOGUE = "https://books.toscrape.com/catalogue/";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const books = [];
for (let page = 1; ; page++) {
const res = await fetch(`${CATALOGUE}page-${page}.html`, {
headers: { "user-agent": "books-demo/1.0 (learning project)" },
});
if (res.status === 404) break; // ran past the last page — we're done
if (!res.ok) throw new Error(`HTTP ${res.status} on page ${page}`);
const $ = cheerio.load(await res.text());
$("article.product_pod").each((_, el) => {
books.push({
title: $(el).find("h3 a").attr("title"),
price: $(el).find(".price_color").text(),
});
});
console.log(`page ${page}: ${books.length} books so far`);
await sleep(1000); // one request per second is a polite default
}
await writeFile("books.json", JSON.stringify(books, null, 2));
Diese Schleife kodiert die drei Gewohnheiten:
- Ein aussagekräftiger
user-agent. Betreiber, die deinen Traffic in ihren Logs sehen, sollten erkennen können, was er ist. Ein leerer oder gefälschter Browser-UA ist das Erste, was nach Missbrauch aussieht. - Eine Verzögerung zwischen Anfragen. Der
sleep-Helfer ist vier Zeilen kürzer als jedes Paket, das dasselbe tut. Eine Anfrage pro Sekunde kostet dich auf dieser Seite unter einer Minute und hält dich weit von jeder Rate-Limiting-Schwelle entfernt. Nodes asynchrones Modell macht es verlockend, alle 50 Abrufe mitPromise.allabzufeuern — widerstehe dem. Nebenläufigkeit ist für deine eigenen Server, nicht für fremde. - Explizite Fehlerbehandlung. Ein 404 bedeutet hier »hinter der letzten Seite«, also beendet er die Schleife. Alles andere, das nicht OK ist, wirft mit der Seitennummer in der Meldung. Stille Fehler erzeugen Datensätze mit Löchern, die du Wochen später entdeckst.
JavaScript-gerenderte Seiten: Puppeteer
Früher oder später wirst du eine Seite abrufen, sie in Cheerio laden und nichts bekommen — die Selektoren, die in den DevTools klar existieren, geben ein leeres Array zurück. Das ist eine client-gerenderte Seite: der Server hat ein Gerüst gesendet und die Daten kamen später per JavaScript an. Cheerio führt nie JavaScript aus, also existieren die Daten aus seiner Sicht nicht.
Die Lösung ist ein Headless-Browser — ein echtes Chrome ohne Fenster, gesteuert aus deinem Skript. In Node ist Puppeteer die Standardwahl:
npm install puppeteer
// scrape-rendered.mjs
import puppeteer from "puppeteer";
const browser = await puppeteer.launch(); // headless by default
const page = await browser.newPage();
await page.goto("https://quotes.toscrape.com/js/", {
waitUntil: "networkidle2",
});
await page.waitForSelector(".quote"); // wait for the JS to render them
const quotes = await page.$$eval(".quote", (cards) =>
cards.map((card) => ({
text: card.querySelector(".text")?.textContent.trim(),
author: card.querySelector(".author")?.textContent.trim(),
}))
);
await browser.close();
console.log(quotes);
Das Ziel hier ist der JavaScript-gerenderte Zwilling der Schwesterseite unserer Buchhandlung — Zitate, die erst nach dem Client-seitigen Rendering existieren. Zwei Zeilen tragen die ganze Last: waitUntil: "networkidle2" hält goto an, bis das Netzwerk weitgehend ruhig geworden ist, und waitForSelector bestätigt, dass die konkreten Elemente, die dich interessieren, tatsächlich gerendert wurden, bevor du sie liest. page.$$eval führt dann deinen Callback innerhalb des Browsers aus, sodass du ganz gewöhnlichen querySelector-Code schreibst und schlichtes JSON zurückbekommst.
Die Kosten sind allerdings real. Ein Cheerio-Scrape ist ein paar Millisekunden Parsing; ein Puppeteer-Scrape startet ein ganzes Chrome, lädt jedes Asset und führt jedes Skript aus — rund zwei Größenordnungen langsamer und weit speicherhungriger. Setze es nur für Seiten ein, die es brauchen, und verwende weiterhin fetch + Cheerio für alles andere. Viele Produktions-Setups mischen die beiden: Puppeteer zum Rendern, dann cheerio.load(await page.content()) zum Parsen mit der angenehmeren API.
Die andere gängige Option ist Playwright, das eine erstklassige JavaScript-API, auto-wartende Locators und eine einheitliche Schnittstelle über Chromium, Firefox und WebKit hinweg bietet. Unser Leitfaden zum Scraping mit Playwright ist um Python herum geschrieben, aber die Konzepte — Wartestrategien, Locators versus Selektoren, wann ein Headless-Browser seine Kosten wert ist — übertragen sich fast Zeile für Zeile auf die JS-API. Speziell fürs Scraping sind Puppeteer und Playwright nah genug beieinander, dass beides eine gute Wahl ist; Playwrights Auto-Warten beseitigt eine Klasse von flackernden Selektor-Bugs, während Puppeteer leichter und Chrome-only ist.
Node vs Python fürs Web Scraping
Die ehrliche Antwort vorweg: beide sind exzellent, und der entscheidende Faktor ist fast nie die Sprache. So ordnen sich die Teile ein.
| Node.js | Python | |
|---|---|---|
| HTTP-Client | fetch eingebaut (Node 18+) | requests / httpx (installiert) |
| HTML-Parsing | Cheerio | BeautifulSoup, lxml, parsel |
| Headless-Browser | Puppeteer, Playwright | Playwright, Selenium |
| Vollständiges Crawl-Framework | Crawlee | Scrapy (ausgereifter) |
| Async-Modell | Async von Haus aus — Nebenläufigkeit ist natürlich | Sync von Haus aus; asyncio ist opt-in |
| Datenaufbereitung nach dem Scrape | Dünner (kein pandas-Äquivalent) | pandas, der ganze Data-Stack |
| Am besten, wenn | Deine App bereits JS/TS ist; Scraping JS-lastiger Seiten | Datenpipelines, Analyse, ML downstream |
Nodes echte Vorteile: fetch mit null Abhängigkeiten, eine asynchrone Laufzeit, in der höfliche Nebenläufigkeit (etwa drei Seiten parallel, jede getaktet) idiomatisch statt aufgesetzt ist, und die Tatsache, dass die Seiten, die du scrapest, selbst in JavaScript geschrieben sind — das Debuggen einer client-gerenderten App ist natürlicher, wenn du ihre Sprache sprichst. Pythons echte Vorteile: Scrapy hat in seiner Reife kein Node-Pendant, und in dem Moment, in dem dein Scrape eine Analyse speist, übernehmen pandas und der wissenschaftliche Stack.
Aber der Ausschlaggeber ist langweilig: nutze die Sprache, die dein Projekt bereits nutzt. Ein Scraper ist eine Komponente, keine Identität. Wenn dein Stack TypeScript ist, kauft dir das Schreiben des Scrapers in Python eine zweite Toolchain zum Pflegen und sonst nichts. Wenn du wirklich bei null anfängst, auf einem datenlastigen Projekt, macht der Python-Leitfaden den Fall für die andere Seite.
Die Produktionsgrenze — und die API-Brücke
Alles oben funktioniert makellos auf Übungsseiten, und das ist kein Zufall: die Sandbox-Seiten existieren, weil echte Ziele sich wehren. Richte denselben Code auf eine große E-Commerce- oder Reiseseite, und du triffst auf die Produktionsgrenze — Cloudflare, DataDome, PerimeterX und Konsorten prüfen TLS-Handshakes, Browser-Fingerprints, IP-Reputation und Verhalten, bevor deine Anfrage die Seite überhaupt erreicht. Natives fetch scheitert schon allein an der TLS-Prüfung; ein Vanilla-Puppeteer verrät Automatisierungsmarker, auf die Fingerprinting-Skripte direkt testen. Du kannst dagegen ankämpfen — Stealth-Plugins, rotierende Residential-Proxys, Header-Fälschung — und unser Leitfaden zum Scrapen von Seiten, die Bots blockieren behandelt dieses Wettrüsten ehrlich. Aber es ist ein Wettrüsten, und es wird zum Vollzeitjob.
Die Alternative ist, eine Web Scraping API für dich kämpfen zu lassen. Dein Code bleibt genau das, was du bereits kennst — ein einziger fetch-Aufruf — aber die Proxys, Fingerprints, das Rendering und die Retries passieren auf der anderen Seite des Endpoints, und du bekommst strukturiertes JSON statt zu parsendem HTML:
// Same fetch you've used all guide — the hard part moved behind the endpoint
const keyword = encodeURIComponent("mechanical keyboard");
const res = await fetch(
`https://api.crawlora.net/api/v1/amazon/search?k=${keyword}`,
{ headers: { "x-api-key": process.env.CRAWLORA_API_KEY } }
);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const { data } = await res.json();
for (const item of data) {
console.log(item.title, item.price);
}
Kein Cheerio, keine Selektoren, kein Parser, den man reparieren muss, wenn sich das Layout verschiebt — der Endpoint gibt normalisierte Felder für Amazon-Suchergebnisse zurück, und es gibt Äquivalente für andere Plattformen in den Docs. Die Abrechnung erfolgt pay-on-success: dir werden nur erfolgreiche 2xx-Antworten berechnet, sodass eine blockierte oder fehlgeschlagene Anfrage nichts kostet. Wo DIY und API jeweils Sinn ergeben, ist eine echte Frage mit einem echten Trade-off — Web Scraping vs API geht sie durch — aber die Kurzfassung lautet: DIY zum Lernen und für kooperative Seiten, eine API, wenn das Ziel sich wehrt oder wenn die Parser-Wartung anfängt, deine Sprints aufzufressen. Das Pricing skaliert mit der Nutzung, und der kostenlose Tarif umfasst 2.000 Credits/Monat, ohne Karte.
- Node 18+ installiert, sodass fetch global verfügbar ist
- Statische Seiten: fetch + cheerio.load, mit CSS-Selektoren auswählen, in Objekte umwandeln, JSON oder CSV schreiben
- Pagination: Schleife mit sleep-Helfer, aussagekräftigem user-agent und lauter Fehlerbehandlung
- robots.txt und Bedingungen geprüft, bevor eine echte Seite gescrapet wird
- JS-gerenderte Seiten: Puppeteer mit waitForSelector, oder Playwright für auto-wartende Locators
- Anti-Bot-Wände treffen: hör auf, gegen Fingerprints zu kämpfen, und wechsle zu einem Scraping-API-Endpoint
Überspring das Anti-Bot-Wettrüsten
Schreib weiter JavaScript — ruf dokumentierte Endpoints auf, die normalisiertes JSON zurückgeben, während Proxys, Rendering und Retries für dich erledigt werden. 2.000 Credits/Monat, ohne Karte.
Weiterführende Lektüre
- Web Scraping mit Python — dieselbe Reise in der anderen Sprache, mit BeautifulSoup und Scrapy.
- Web Scraping mit Playwright — tiefer bei Headless-Browsern, Wartestrategien und Locators.
- Seiten scrapen, die Bots blockieren — was Anti-Bot-Systeme tatsächlich prüfen und was es braucht, um durchzukommen.
- Web Scraping vs API — ein Rahmen für die Build-or-Buy-Entscheidung.
Häufig gestellte Fragen
Eignet sich Node.js für Web Scraping?
Ja. Node 18+ bringt fetch nativ mit, Cheerio erledigt das HTML-Parsing mit einer jQuery-artigen API, und Puppeteer oder Playwright steuern ein Headless-Chrome für JavaScript-gerenderte Seiten. Seine asynchrone Laufzeit macht getaktete Nebenläufigkeit zudem natürlich. Wenn dein Stack ohnehin JavaScript oder TypeScript ist, gibt es keinen Grund, zum Scrapen die Sprache zu wechseln.
Cheerio vs. Puppeteer — was sollte ich nutzen?
Nutze Cheerio immer dann, wenn die Daten im Seitenquelltext sichtbar sind: es parst HTML in Millisekunden ohne Browser. Nutze Puppeteer nur, wenn die Seite ihre Daten mit Client-seitigem JavaScript rendert, denn ein Chrome zu starten ist rund zwei Größenordnungen langsamer und deutlich schwerer. Viele Scraper mischen beides — Puppeteer rendert, dann parst Cheerio page.content().
Brauche ich axios oder eine andere HTTP-Bibliothek zum Scrapen in Node.js?
Nein. Seit Node 18 ist fetch global verfügbar, sodass ein Scraper statischer Seiten nur Cheerio als Abhängigkeit braucht. Eine HTTP-Bibliothek bietet Komfort für Interceptors oder fortgeschrittene Retry-Logik, aber zum Abrufen von HTML und Aufrufen von JSON-APIs deckt natives fetch das ab.
Wie scrape ich JavaScript-gerenderte Seiten in Node.js?
Nutze einen Headless-Browser. Mit Puppeteer: starte einen Browser, page.goto mit waitUntil networkidle2, waitForSelector für die Elemente, die du brauchst, dann page.$$eval, um sie in schlichte Objekte umzuwandeln. Playwright ist die Hauptalternative, mit auto-wartenden Locators und Cross-Browser-Unterstützung — seine JS-API spiegelt dieselben Konzepte wider.
Ist Node.js oder Python besser für Web Scraping?
Sie sind gleichermaßen fähig, also nutze die Sprache, die dein Projekt bereits spricht. Node punktet mit eingebautem fetch, natürlicher asynchroner Nebenläufigkeit und dem Debuggen JS-lastiger Seiten in ihrer eigenen Sprache; Python punktet mit der Reife von Scrapy und dem pandas-Data-Stack downstream. Die Sprache ist selten der entscheidende Faktor.
Warum wird mein Node.js-Scraper auf echten Websites blockiert?
Produktionsseiten sitzen hinter Anti-Bot-Systemen, die TLS-Handshakes, Browser-Fingerprints, IP-Reputation und Verhalten prüfen. Natives fetch scheitert schon allein an der TLS-Prüfung, und ein Vanilla-Puppeteer verrät Automatisierungsmarker. Du kannst mit Stealth-Plugins und rotierenden Proxys dagegen ankämpfen, oder diese Ziele über eine Scraping API leiten, die Proxys, Fingerprints und Retries übernimmt und strukturiertes JSON zurückgibt.
Wie bin ich beim Scrapen mit Node.js höflich?
Sende einen aussagekräftigen user-agent, damit Betreiber deinen Traffic identifizieren können, füge eine Verzögerung zwischen Anfragen ein (eine pro Sekunde ist eine sichere Standardwahl), lies und respektiere robots.txt und die Bedingungen der Seite, sammle nur öffentliche Daten und scheitere bei Fehlern laut, statt Retries zu hämmern.