Node.js Scraping mit Cheerio und Proxys: Der vollständige Guide

Baue eine produktionsreife Scraping-Pipeline mit axios, Cheerio und rotierenden Residential-Proxys — von der ersten Zeile Code bis zum 10.000-URL-Crawl mit Circuit Breaking.

Node.js Scraping mit Cheerio und Proxys: Der vollständige Guide

Wenn du mit Node.js im großen Maßstab scrapst, kennst du das Problem: Nach ein paar hundert Requests drosselt die Zielseite deinen Traffic oder blockiert deine IP komplett. Die Lösung? Eine axios proxy rotation kombiniert mit Cheerio für leichtgewichtiges, serverseitiges HTML-Parsing. In diesem Guide zeige ich dir, wie du eine produktionsreife Node.js Scraping-Pipeline aufbaust — von der ersten Zeile Code bis zum Concurrent-Crawl über 10.000 URLs mit rotierenden Residential-Proxys.

Warum Cheerio + axios für Node.js Scraping?

Cheerio ist im Grunde jQuery für den Server — es parst HTML-Strings, lässt dich mit vertrauten CSS-Selektoren navigieren und extrahiert Daten, und das alles ohne Browser-Overhead. Kombiniert mit axios als HTTP-Client bekommst du eine Pipeline, die Tausende Seiten pro Stunde verarbeiten kann.

Die Vorteile gegenüber Puppeteer oder Playwright sind offensichtlich:

  • Geschwindigkeit: 10–50× schneller, weil kein Browser-Prozess gestartet werden muss.
  • Ressourcen: Bis zu 90 % weniger RAM — entscheidend, wenn du hunderte parallele Requests fährst.
  • Einfachheit: Weniger Moving Parts, leichter zu debuggen, simpler zu deployen.
  • Skalierbarkeit: Perfekt für Container-Umgebungen und serverless Functions.

Aber Cheerio funktioniert nur zuverlässig bei Seiten, die den Inhalt bereits im initialen HTML ausliefern (SSR oder statische Seiten). Bei clientseitig gerenderten SPAs brauchst du einen Headless Browser — dazu später mehr.

Grundsetup — axios, Cheerio und der erste Request

Die Basis ist simpel: axios holt das HTML, Cheerio parst es. Keine Abhängigkeiten von Browser-Engines, keine komplexen Setup-Routinen.

const axios = require('axios');
const cheerio = require('cheerio');

async function scrapeProduct(url) {
  const { data: html } = await axios.get(url);
  const $ = cheerio.load(html);

  return {
    title: $('h1.product-title').text().trim(),
    price: parseFloat(
      $('span.price-current').text().replace(/[^\d.,]/g, '').replace(',', '.')
    ),
    availability: $('span.stock-status').text().trim(),
    image: $('img.product-image').attr('src'),
  };
}

// Erster Test
scrapeProduct('https://example-shop.de/product/123')
  .then(console.log)
  .catch(console.error);

Das war's — ein leichtgewichtiger Scraper in unter 20 Zeilen. Aber in der Praxis brauchst du Proxy-Unterstützung, Rate Limiting und Fehlerbehandlung. Los geht's.

Proxy-Konfiguration in axios

Für die Cheerio proxy Integration gibt es zwei Wege in axios: die native proxy-Konfiguration und den https-proxy-agent. Beide haben ihre Daseinsberechtigung.

Methode 1: Native proxy-Option

Die einfachste Methode — funktioniert für HTTP-Ziel-URLs out of the box:

const axios = require('axios');

const client = axios.create({
  proxy: {
    host: 'gate.proxyhat.com',
    port: 8080,
    auth: {
      username: 'user-country-DE',
      password: 'YOUR_PASSWORD',
    },
  },
});

const { data } = await client.get('https://example-shop.de/product/123');

Methode 2: https-proxy-agent für HTTPS-Ziele

Wenn du HTTPS-Seiten scrapst (die Mehrheit), braucht Node.js einen Tunnel. https-proxy-agent löst das sauber:

const axios = require('axios');
const { HttpsProxyAgent } = require('https-proxy-agent');

const agent = new HttpsProxyAgent('http://user-country-DE:YOUR_PASSWORD@gate.proxyhat.com:8080');

const client = axios.create({ httpsAgent: agent });

const { data } = await client.get('https://example-shop.de/product/123');

Diese Methode ist für Node.js scraping Cheerio proxies in der Praxis die robustere Wahl, weil sie HTTPS-Tunneling korrekt handhabt und sich nahtlos in axios-Interceptors integrieren lässt.

Cheerio vs. Headless Browser — wann was?

Nicht jede Seite lässt sich mit Cheerio scrapen. Die Entscheidung hängt davon ab, wie die Zielseite ihren Inhalt rendert:

Kriterium Cheerio + axios Puppeteer / Playwright
Render-Typ SSR, statisches HTML Client-side Rendering (SPA)
Geschwindigkeit ~50–200 ms/Request ~2–10 s/Request
RAM pro Instanz ~20–50 MB ~150–400 MB
Parallele Requests 100–500+ 5–20 (mit Browser-Pool)
JS-Ausführung Nein Ja
CAPTCHAs handhaben Umgehen per Proxy-Rotation Lösen per Integration
Setup-Komplexität Niedrig Hoch

Daumenregel: Wenn curl oder wget den Inhalt liefert, reicht Cheerio. Wenn der Quellcode nur ein leeres <div id="app"> zeigt, brauchst du Puppeteer.

Für die meisten E-Commerce-Seiten, Blogs, Foren und SERPs reicht Cheerio völlig aus — und das bei einem Bruchteil der Kosten.

Rotierende Proxy-Pools als axios-Interceptor

Jetzt wird's interessant. Anstatt jeden Request manuell mit einer Proxy-Konfiguration zu versehen, baust du die axios proxy rotation als wiederverwendbaren Interceptor. Das ist der framework-idiomatische Weg — Middleware, kein Hack.

Der Interceptor erzeugt für jeden Request eine neue Session-ID, wodurch ProxyHat eine frische Residential-IP zuweist:

const axios = require('axios');
const { HttpsProxyAgent } = require('https-proxy-agent');
const crypto = require('crypto');

// ProxyHat-Konfiguration
const PROXY_HOST = 'gate.proxyhat.com';
const PROXY_PORT = 8080;
const PROXY_USER = 'YOUR_USERNAME';
const PROXY_PASS = 'YOUR_PASSWORD';

function createRotatingProxyClient(country = 'DE', opts = {}) {
  const client = axios.create(opts);

  client.interceptors.request.use((config) => {
    // Eindeutige Session-ID erzwingt eine neue IP pro Request
    const sessionId = crypto.randomBytes(8).toString('hex');
    const username = `${PROXY_USER}-country-${country}-session-${sessionId}`;

    const agent = new HttpsProxyAgent(
      `http://${username}:${PROXY_PASS}@${PROXY_HOST}:${PROXY_PORT}`
    );

    config.httpsAgent = agent;
    config.httpAgent = agent;
    config.proxy = false; // https-proxy-agent übernimmt den Proxy
    return config;
  });

  return client;
}

// Verwendung: jeder Request bekommt automatisch eine neue IP
const client = createRotatingProxyClient('DE');
await client.get('https://example-shop.de/product/1');
await client.get('https://example-shop.de/product/2');

Dieser Interceptor-Ansatz hat mehrere Vorteile:

  • Trennung von Zuständigkeiten: Der Scraper-Code weiß nichts von Proxys — der Interceptor kümmert sich drum.
  • Einfacher Wechsel: Sticking Session statt Rotation? Nur die Session-ID ändern.
  • Testbarkeit: Den Interceptor entfernen, und alle Requests gehen direkt raus — perfekt fürs Local Dev.
  • Erweiterbarkeit: Retry-Logic, Circuit Breaking und Logging lassen sich als weitere Interceptors stapeln.

Sticky Sessions für Multi-Step-Flows

Manchmal brauchst du dieselbe IP über mehrere Requests hinweg — etwa bei Logins oder Paginierung innerhalb einer Session. Dafür nutzt du eine feste session-ID:

function createStickyProxyClient(country = 'DE', sessionId = null) {
  const sid = sessionId || crypto.randomBytes(8).toString('hex');
  const username = `${PROXY_USER}-country-${country}-session-${sid}`;

  const agent = new HttpsProxyAgent(
    `http://${username}:${PROXY_PASS}@${PROXY_HOST}:${PROXY_PORT}`
  );

  return axios.create({ httpsAgent: agent, httpAgent: agent, proxy: false });
}

// Gleiche IP für alle Requests in dieser Session (bis zu 30 Min)
const sticky = createStickyProxyClient('DE', 'my-order-flow-42');
await sticky.get('https://example-shop.de/login');
await sticky.get('https://example-shop.de/cart');

Concurrent Scraping mit p-limit

10.000 URLs sequenziell abzuarbeiten dauert ewig. p-limit ist ein leichtgewichtiger Concurrency-Limiter, der perfekt mit async/await funktioniert — kein komplexes Queue-System nötig.

const pLimit = require('p-limit');
const cheerio = require('cheerio');

// Maximal 50 parallele Requests — nicht mehr, um Proxy-Rate-Limits zu respektieren
const limit = pLimit(50);

async function scrapeWithConcurrency(urls) {
  const results = await Promise.all(
    urls.map((url) =>
      limit(async () => {
        try {
          const { data: html } = await client.get(url);
          const $ = cheerio.load(html);
          return {
            url,
            title: $('h1').text().trim(),
            price: $('span.price').text().trim(),
          };
        } catch (err) {
          return { url, error: err.message };
        }
      })
    )
  );

  const successful = results.filter((r) => !r.error);
  const failed = results.filter((r) => r.error);
  console.log(`✓ ${successful.length} erfolgreich, ✗ ${failed.length} fehlgeschlagen`);
  return { successful, failed };
}

Warum 50 parallel? Das hängt von deinen Proxy-Rate-Limits und der Ziel-Seite ab. Starte konservativ mit 20–50 und skaliere hoch, solange die Erfolgsrate über 95 % bleibt.

Praxisbeispiel — 10.000 URLs einer E-Commerce-Seite

Jetzt kombinieren wir alles: Interceptor-basierte Proxy-Rotation, Cheerio-Parsing, p-limit-Concurrency und robuste Fehlerbehandlung. Das ist die Pipeline, die du in Produktion fahren kannst.

const axios = require('axios');
const { HttpsProxyAgent } = require('https-proxy-agent');
const cheerio = require('cheerio');
const pLimit = require('p-limit');
const crypto = require('crypto');
const fs = require('fs/promises');

// ─── Konfiguration ───────────────────────────────────────
const PROXY_HOST = 'gate.proxyhat.com';
const PROXY_PORT = 8080;
const PROXY_USER = 'YOUR_USERNAME';
const PROXY_PASS = 'YOUR_PASSWORD';
const CONCURRENCY = 50;
const MAX_RETRIES = 3;
const RETRY_DELAYS = [5000, 15000, 45000]; // Exponentielles Backoff

// ─── Rotating Proxy Interceptor ──────────────────────────
function createScraperClient(country = 'DE') {
  const client = axios.create({
    timeout: 15000,
    headers: {
      'User-Agent':
        'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36',
      'Accept-Language': 'de-DE,de;q=0.9,en;q=0.5',
    },
  });

  client.interceptors.request.use((config) => {
    const sessionId = crypto.randomBytes(8).toString('hex');
    const username = `${PROXY_USER}-country-${country}-session-${sessionId}`;
    const agent = new HttpsProxyAgent(
      `http://${username}:${PROXY_PASS}@${PROXY_HOST}:${PROXY_PORT}`
    );
    config.httpsAgent = agent;
    config.httpAgent = agent;
    config.proxy = false;
    return config;
  });

  return client;
}

// ─── Retry-Logik ─────────────────────────────────────────
async function fetchWithRetry(client, url, retries = MAX_RETRIES) {
  for (let attempt = 0; attempt <= retries; attempt++) {
    try {
      const res = await client.get(url);
      return res.data;
    } catch (err) {
      const status = err.response?.status;
      if (status === 404) throw err; // 404 = Seite existiert nicht, kein Retry
      if (status === 403 || status === 429 || !status) {
        // Proxy-Rotation passiert automatisch durch den Interceptor
        const delay = RETRY_DELAYS[attempt] || 60000;
        console.warn(`⚠ ${status || 'network error'} für ${url} — Retry ${attempt + 1}/${retries} in ${delay}ms`);
        await new Promise((r) => setTimeout(r, delay));
        continue;
      }
      throw err;
    }
  }
  throw new Error(`Max retries überschritten für ${url}`);
}

// ─── Parsing ──────────────────────────────────────────────
function parseProduct(html, url) {
  const $ = cheerio.load(html);
  return {
    url,
    title: $('h1.product-title').text().trim(),
    price: $('span.price-current').text().trim(),
    availability: $('span.stock-status').text().trim(),
    breadcrumbs: $('nav.breadcrumb a')
      .map((_, el) => $(el).text().trim())
      .get()
      .join(' > '),
  };
}

// ─── Main Pipeline ───────────────────────────────────────
async function main() {
  const urls = (await fs.readFile('product_urls.txt', 'utf-8')).split('\n').filter(Boolean);
  console.log(`Starte Scraping von ${urls.length} URLs…`);

  const client = createScraperClient('DE');
  const limit = pLimit(CONCURRENCY);
  const results = [];
  const errors = [];

  await Promise.all(
    urls.map((url) =>
      limit(async () => {
        try {
          const html = await fetchWithRetry(client, url);
          const product = parseProduct(html, url);
          results.push(product);
        } catch (err) {
          errors.push({ url, error: err.message });
        }
      })
    )
  );

  await fs.writeFile('results.json', JSON.stringify(results, null, 2));
  await fs.writeFile('errors.json', JSON.stringify(errors, null, 2));
  console.log(`✓ ${results.length} Produkte gespeichert, ✗ ${errors.length} Fehler`);
}

main().catch(console.error);

Diese Pipeline verarbeitet 10.000 URLs mit automatischer IP-Rotation, exponentiellem Backoff bei 429/403-Fehlern und einem Concurrency-Limit, das sowohl deine Infrastruktur als auch die Proxy-Rate-Limits respektiert.

Fehlerbehandlung — 403, 429 und Circuit Breaking

Ein robuster Scraper muss mit Fehlerquellen umgehen können. Die häufigsten:

  • 403 Forbidden: IP geblockt oder CAPTCHA erforderlich. Lösung: Proxy-Rotation (passiert automatisch durch unseren Interceptor).
  • 429 Too Many Requests: Rate-Limit erreicht. Lösung: Exponentielles Backoff + Concurrency reduzieren.
  • Timeouts: Proxy langsam oder Ziel-Seite überlastet. Lösung: Retry mit neuer IP.
  • Verbindungsfehler: Proxy-Ausfall. Lösung: Circuit Breaker pausiert den Traffic, bevor Ressourcen verschwendet werden.

Circuit Breaker als axios-Interceptor

Ein Circuit Breaker verhindert, dass dein Scraper endlos in ein totes Ziel feuert. Nach einer definierten Anzahl aufeinanderfolgender Fehler öffnet sich der Circuit — alle weiteren Requests werden sofort abgelehnt, bis ein Timeout abgelaufen ist:

class CircuitBreaker {
  constructor(failureThreshold = 5, resetTimeout = 60000) {
    this.failures = 0;
    this.failureThreshold = failureThreshold;
    this.resetTimeout = resetTimeout;
    this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
    this.nextAttempt = Date.now();
  }

  recordFailure() {
    this.failures++;
    if (this.failures >= this.failureThreshold) {
      this.state = 'OPEN';
      this.nextAttempt = Date.now() + this.resetTimeout;
      console.error(`🔴 Circuit OPEN — pausiere Requests für ${this.resetTimeout / 1000}s`);
    }
  }

  recordSuccess() {
    this.failures = 0;
    this.state = 'CLOSED';
  }

  canExecute() {
    if (this.state === 'CLOSED') return true;
    if (this.state === 'OPEN' && Date.now() >= this.nextAttempt) {
      this.state = 'HALF_OPEN';
      return true; // Ein Probe-Request darf durch
    }
    return this.state !== 'OPEN';
  }
}

// Als Response-Interceptor integrieren
const breaker = new CircuitBreaker();

client.interceptors.response.use(
  (response) => { breaker.recordSuccess(); return response; },
  (error) => {
    breaker.recordFailure();
    if (!breaker.canExecute()) {
      return Promise.reject(new Error('Circuit is OPEN — Requests pausiert'));
    }
    return Promise.reject(error);
  }
);

Mit diesem Muster baust du ein Defense-in-Depth-System: Der Proxy-Interceptor rotiert IPs bei Fehlern, der Retry-Interceptor versucht es mit Backoff, und der Circuit Breaker stoppt den Traffic, wenn das Ziel grundsätzlich nicht erreichbar ist.

Skalierung — Container, Queue und Persistenz

Für noch größere Workloads (100k+ URLs) reicht ein einzelner Node.js-Prozess nicht mehr aus. Die Architektur-Erweiterung:

  1. Message Queue (Redis/BullMQ): URLs als Jobs eintragen, Worker konsumieren und verarbeiten. Automatische Retry-Logik und Dead-Letter-Queues für dauerhaft fehlgeschlagene URLs.
  2. Container-Fleet: Mehrere Worker-Container fahren dieselbe Queue. Jeder Container nutzt den Proxy-Interceptor — die IP-Rotation passiert pro Request, nicht pro Container.
  3. Ergebnis-Persistenz: Statt in JSON-Dateien schreibst du in eine Datenbank (PostgreSQL, MongoDB) oder einen Object Store (S3).
  4. Monitoring: Erfolgsrate, Latenz und Fehler-Typen in einer Metrik-Dashboard (Prometheus + Grafana) tracken.

Für die meisten Anwendungsfälle bis 50.000 URLs reicht aber die oben gezeigte p-limit-Pipeline völlig aus — und lässt sich mit ProxyHat's flexiblem Pricing kosteneffizient betreiben.

Key Takeaways

Cheerio + axios reicht für SSR-Seiten. Wenn der Quellcode den Inhalt enthält, brauchst du keinen Browser — spar dir den Overhead.

Proxy-Rotation als Interceptor, nicht als Hack. Der axios-Request-Interceptor kapselt die Proxy-Logik sauber — austauschbar, testbar, erweiterbar.

Jeder Request = neue IP. Mit ProxyHat's Session-basierter Username-Syntax rotierst du IPs ohne externe Proxy-Liste pflegen zu müssen.

Concurrency limitieren, Fehler isolieren. p-limit für Parallelität, Circuit Breaker für Überlast-Schutz, exponentielles Backoff für temporäre Fehler.

403/429 sind keine Endstation. Automatische Proxy-Rotation + Retry = die meisten Blockaden lösen sich von selbst.

Bereit loszulegen? Registriere dich auf dashboard.proxyhat.com, hol dir deine Credentials und setze den Interceptor ein — deine erste Scraping-Pipeline steht in unter 30 Minuten.

Weitere Guides, die dich interessieren könnten: Web Scraping Use Cases und SERP Tracking mit Proxys.

Bereit loszulegen?

Zugang zu über 50 Mio. Residential-IPs in über 148 Ländern mit KI-gesteuerter Filterung.

Preise ansehenResidential Proxies
← Zurück zum Blog