Architektur für Web Scraping im großen Maßstab: Ein Praxisleitfaden

Ein paar hundert Seiten sind ein Skript. Millionen sind ein verteiltes System. Hier ist die Architektur, die Sie dorthin bringt — Warteschlangen, asynchrone Worker, Proxy-Tiering, Wiederholungsversuche, Deduplizierung und Datenqualitätsprüfungen, mit funktionierendem Code.

Das Scraping von ein paar hundert Seiten ist ein Skript. Das Scraping von Millionen ist ein verteiltes System. Sobald Ihre Zielanzahl von "läuft über Nacht auf meinem Laptop" zu "muss diese Woche fertig sein, ohne zu überhitzen" wechselt, hört der schwierige Teil auf, das Parsen zu sein, und wird alles darum herum — wie Sie Arbeit in die Warteschlange stellen, sie verteilen, Blockaden vermeiden, Fehler wiederholen und das Ergebnis speichern und überprüfen. Dies ist Web Scraping im großen Maßstab als Architektur, kein Schnipsel, mit funktionierendem Code in jeder Phase.

Skalierung ist Parallelität, nicht eine größere Schleife

Eine Zahl macht es konkret. Nehmen Sie eine Kategorie mit 20.000 Listenseiten, jeweils 20 Elemente — 400.000 Seiten zum Abrufen. Bei realistischen 2,5 Sekunden pro Seite dauert ein strikt sequentieller Durchlauf etwa 1.000.000 Sekunden, ungefähr 11,5 Tage Wartezeit auf Seitenladezeiten, bevor Sie ein einziges Feld parsen. Treiben Sie 200 Seiten parallel an und diese 11,5 Tage schrumpfen auf eine Stunde Echtzeit. Zeit ist die Einschränkung im großen Maßstab, und Parallelität ist, wie Sie sie zurückgewinnen. Alles andere in der Architektur existiert, um diese Parallelität überlebensfähig zu machen.

Statistikdiagramm zeigt 400.000 Seiten, die 11,5 Tage sequentiell versus etwa eine Stunde bei 200 parallel benötigen, mit 10.000 Fehlern pro Million bei 1 Prozent
Parallelität verwandelt einen 11,5-tägigen Lauf in eine Stunde — aber bei einer Million Anfragen ist selbst eine 1% Fehlerquote 10.000 tote Seiten.

Die Architektur auf einen Blick

Ein Scraper, der Millionen von Seiten übersteht, ist ein kleines verteiltes System mit einer Handvoll benannter Teile, die jeweils ein Problem lösen, das nur bei großem Volumen auftritt:

Zuerst die Warteschlange: Entkoppeln Sie die Entdeckung vom Abrufen

Die wichtigste strukturelle Entscheidung ist, eine Warteschlange zwischen "was zu scrapen ist" und "das Scrapen durchführen" zu setzen. Ein Producer zählt URLs auf; ein Pool von Workern leert sie. Keine Seite weiß, wie schnell die andere läuft, und Sie können Worker hinzufügen, ohne den Producer zu berühren. In Python ist dies Celery oder RQ über Redis; in Node, BullMQ; im größeren Maßstab, RabbitMQ oder Kafka. Das Muster in einer Datei:

import asyncio, aiohttp

CONCURRENCY = 50
queue = asyncio.Queue()

async def worker(session):
    while True:
        url = await queue.get()
        try:
            async with session.get(url, timeout=20) as resp:
                await handle(url, await resp.text(), resp.status)
        except Exception as err:
            await on_failure(url, err)
        finally:
            queue.task_done()

async def run(urls):
    for u in urls:
        queue.put_nowait(u)
    async with aiohttp.ClientSession() as session:
        tasks = [asyncio.create_task(worker(session)) for _ in range(CONCURRENCY)]
        await queue.join()
        for t in tasks:
            t.cancel()

Der Knopf, der zählt, ist CONCURRENCY. Zu niedrig verschwendet die Parallelität, die den Maßstab ermöglicht; zu hoch überfordert sowohl das Ziel als auch Ihren eigenen Ausgang. Sie finden den richtigen Wert, indem Sie die Fehlerraten beobachten — genau deshalb ist Überwachung ein erstklassiger Teil des Systems, kein nachträglicher Gedanke.

Flussdiagramm einer großen Scraping-Pipeline: Warteschlange, asynchrone Worker, Proxy-Schicht, Parsen und Qualitätsprüfungen, Speicherung
Acht benannte Teile. Die Proxy-, Anti-Bot- und Rendering-Schichten sind am schwersten gesund zu halten — und der natürliche Ort zum Kaufen.

Die Proxy-Schicht bricht zuerst

Bei geringem Volumen bemerken Sie kaum Anti-Bot-Abwehrmaßnahmen; im großen Maßstab brechen sie den Lauf zuerst. Senden Sie ein paar hunderttausend Anfragen von einer IP und Sie werden rate-limitiert, dann herausgefordert, dann blockiert. Die Lösung ist die Rotation über viele Adressen. Stufen Sie es: günstige Rechenzentrums-IPs für nachsichtige Ziele und APIs, rotierende Wohn-IPs für schwierige kommerzielle Ziele, die echten Nutzerverkehr erwarten. Aber Rotation allein reicht nicht aus — moderne Abwehrmaßnahmen lesen auch TLS-Fingerabdrücke und Header-Reihenfolge, sodass der Verkehr wie ein Browser aussehen muss, nicht nur von einer frischen IP kommen.

Wiederholungsversuche: Scheitern ist der Normalzustand

Bei einer Million Anfragen ist eine 1% vorübergehende Fehlerquote 10.000 fehlgeschlagene Seiten. Fehler sind bei diesem Volumen kein Randfall — sie sind Routine, und die Pipeline muss einen fehlgeschlagenen Abruf als normal behandeln, anstatt als fatal. Wiederholen Sie mit exponentiellem Backoff und einem Limit, dann verschieben Sie die URL in eine Dead-Letter-Warteschlange, anstatt den Lauf zu blockieren. Lesen Sie warum es fehlgeschlagen ist: ein Timeout oder 503 ist es wert, erneut versucht zu werden, ein harter 404 nicht.

import asyncio, random

async def fetch_with_retry(session, url, tries=4):
    for attempt in range(tries):
        try:
            async with session.get(url, timeout=20) as r:
                if r.status == 404:
                    return None                # don't retry a hard 404
                if r.status < 400:
                    return await r.text()
        except Exception:
            pass
        await asyncio.sleep(2 ** attempt + random.random())  # backoff + jitter
    await dead_letter(url)                     # give up after the cap
    return None

Deduplizierung: nicht dieselbe Seite zweimal crawlen

Entdeckung im großen Maßstab produziert ständig Duplikate — dasselbe Produkt erreichbar über drei Pfade, Tracking-Parameter, die eine Seite wie zehn aussehen lassen. Normalisieren Sie URLs, bevor sie in die Warteschlange gelangen, und halten Sie dann eine Gesehen-Menge (ein Redis-Set oder ein Bloom-Filter, sobald das Set Hunderte von Millionen erreicht):

from urllib.parse import urlsplit, urlunsplit, parse_qsl, urlencode

seen = set()

def normalize(url):
    s = urlsplit(url.lower())
    q = [(k, v) for k, v in parse_qsl(s.query) if not k.startswith("utm_")]
    return urlunsplit((s.scheme, s.netloc, s.path.rstrip("/"), urlencode(sorted(q)), ""))

def enqueue(url):
    key = normalize(url)
    if key not in seen:
        seen.add(key)
        queue.put_nowait(key)

Speicherung und Datenqualität

Zwei maßstabspezifische Gewohnheiten: in Chargen schreiben, damit die Speicherung nicht Ihr Engpass ist, und Rohdaten von geparsten Daten trennen, damit Sie ohne erneutes Crawlen neu parsen können, wenn sich Selektoren ändern. Fügen Sie dann die Hälfte der Überwachung hinzu, die die meisten Teams überspringen — Datenqualitätsprüfungen. Ein Lauf kann 100% HTTP-Erfolg melden und trotzdem Müll produzieren, wenn sich das Layout verschoben hat und Ihre Selektoren jetzt nichts mehr treffen. Bestätigen Sie, dass erforderliche Felder nicht leer sind und Werte sinnvoll sind:

def validate(row):
    assert row.get("title"), "empty title — selector may have drifted"
    price = row.get("price")
    assert isinstance(price, (int, float)) and 0 < price < 1_000_000, "bad price"
    return row

# fail loud on page 5,000, not silently after 5,000,000 empty rows

Proxies, Anti-Bot und Rendering an eine API auslagern

Rendern Sie nur, wenn Sie müssen

Ein Headless-Browser ist die teuerste Operation in der Pipeline — CPU, Speicher und Sekunden pro Seite, die alles bei einer Million Seiten dominieren. Viele Seiten liefern Daten immer noch im initialen HTML oder einem JSON-Endpunkt; ein einfacher Abruf plus ein Parser ist um Größenordnungen billiger. Versuchen Sie zuerst den günstigen Weg, bestätigen Sie, dass die Felder vorhanden sind, und eskalieren Sie das Rendering nur für die Seiten, die es benötigen. Unsere Aufschlüsselung der Renderkosten versus HTTP liefert reale Zahlen zu dieser Lücke.

Bauen vs. kaufen der schwierigen Schichten

Alles oben ist baubar, also ist die ehrliche Frage, welche Teile Ihre Ingenieurszeit verdienen. Das Datenmodell, die Parsing-Logik, die Qualitätsprüfungen und das Speicherschema sind spezifisch für Ihr Projekt — nur Sie können sie gut bauen. Der Proxy-Pool, die Anti-Bot-Behandlung, die Headless-Render-Flotte und die Wiederholungs- und Lieferwarteschlange sind generische Infrastruktur, die teuer zu bauen ist und mühsam gesund zu halten ist, wenn sich Ziele entwickeln. Das ist die Grenze, auf der eine verwaltete Scraper API sitzt: Mieten Sie die Teile, die für alle gleich sind. Unsere Bauen-gegen-Kaufen-Aufschlüsselung arbeitet die Wartungssteuer im Detail heraus, und Scraper als Produktionssoftware ausführen behandelt die Beobachtbarkeit des Ganzen.

Häufig gestellte Fragen

Wie scrapen Sie Millionen von Seiten?

Mit Parallelität, nicht mit einer größeren Schleife. Setzen Sie eine Warteschlange zwischen URL-Entdeckung und Abrufen, leeren Sie sie mit einem Pool von asynchronen oder verteilten Workern, rotieren Sie IPs, um Blockaden zu vermeiden, wiederholen Sie vorübergehende Fehler mit Backoff, deduplizieren Sie URLs und schreiben Sie Chargen in die Speicherung. Ein sequentieller Millionenseitenlauf dauert Tage; derselbe Job über einen parallelen Worker-Pool ist in Stunden fertig.

Was ist die beste Architektur für Web Scraping im großen Maßstab?

Eine Warteschlangen- und Worker-Pipeline: ein Producer zählt URLs in eine Warteschlange (Redis, RabbitMQ oder Kafka), Worker rufen gleichzeitig über eine rotierende Proxy-Schicht ab, rendern nur die Seiten, die JavaScript benötigen, wiederholen Fehler in eine Dead-Letter-Warteschlange, deduplizieren mit einer Gesehen-Menge und speichern Roh- und geparste Daten getrennt. Verpacken Sie es in Überwachung mit Datenqualitätsprüfungen, damit Abweichungen frühzeitig sichtbar werden.

Wie viele Anfragen können Sie parallel ausführen?

Es hängt vom Ziel und Ihrem Ausgang ab, nicht von einer festen Zahl. Scraping ist IO-gebunden, sodass eine bescheidene Box viele Hunderte von laufenden Anfragen halten kann. Beginnen Sie mit einer Parallelität von etwa 50, beobachten Sie die Fehlerrate und erhöhen Sie sie, bis die Fehler steigen — das ist Ihre Obergrenze. Über die Grenzen einer Maschine hinaus fügen Sie verteilte Worker hinzu, anstatt einen einzelnen Knoten stärker zu belasten.

Wie gehen Sie mit Fehlern im großen Maßstab um?

Gehen Sie von Fehlern aus — bei einer Million Anfragen ist selbst eine 1% Fehlerquote 10.000 tote Seiten. Wiederholen Sie vorübergehende Fehler (Timeouts, 503s) mit exponentiellem Backoff und Jitter, begrenzen Sie die Versuche und verschieben Sie dauerhafte Fehler in eine Dead-Letter-Warteschlange, anstatt den Lauf zu blockieren. Wiederholen Sie keine harten 404s. Die zufällige Sammlungsauswahl verteilt auch Fehler, sodass Sie nicht bei jedem Lauf auf denselben Seiten scheitern.

Skalierung besteht größtenteils aus den Teilen, die nicht Spaß machen zu bauen: rotierende IPs, Anti-Bot, Headless-Rendering, Warteschlangen und Wiederholungen. Besitzen Sie die Teile, die spezifisch für Ihre Daten sind — das Modell, die Parser, die Qualitätsprüfungen — und mieten Sie die generische Infrastruktur, die für alle der gleiche Aufwand ist. Richten Sie zuerst die Warteschlange und die Parallelität richtig ein; alles andere dient dazu, diese Parallelität beim Kontakt mit einer Million echter Seiten zu überstehen.

Betreiben Sie die Abrufschicht mit rotierenden Wohn-IPs