Wer einen Scraper beschleunigen will, denkt meist zuerst an „mehr Kerne": die Arbeit auf Prozesse verteilen, einen größeren Server nehmen. Bei einer typischen Scraping-Aufgabe ist der Prozessor aber die meiste Zeit untätig. Das Programm wartet sekundenlang auf die Antwort des Zielservers, den TLS-Handshake und den Aufbau des Proxy-Tunnels. Die Nutzung dieser Wartezeit heißt Concurrency (Nebenläufigkeit), die Nutzung der Prozessorleistung Parallelism (Parallelität), und beide lösen unterschiedliche Probleme.
In diesem Artikel erklären wir den Unterschied zwischen beiden Konzepten, warum Scraping überwiegend eine Ein-/Ausgabe-lastige (I/O) Aufgabe ist und wann asyncio, Threads und Prozesse in Python helfen. Außerdem betrachten wir die Event Loop von Node.js, was die Geschwindigkeit wirklich begrenzt und wie Sie den Wert für Nebenläufigkeit festlegen. Die Beispiele haben wir gegen einen lokalen Server mit künstlicher Latenz ausgeführt, und wir stellen ein Messgerüst bereit, mit dem Sie Ihre eigene Aufgabe messen können.
Was ist Concurrency (Nebenläufigkeit)?
Nebenläufigkeit ist die Fähigkeit mehrerer Aufgaben, im selben Zeitraum voranzukommen. Die Aufgaben müssen nicht im selben Augenblick laufen; während eine wartet, läuft eine andere. Ein Koch, der Salat schneidet, während das Nudelwasser zum Kochen kommt, arbeitet nebenläufig: Eine Person erledigt zwei Aufgaben in derselben Zeit.
In der Programmierung gelingt das, indem eine Aufgabe die Kontrolle an eine andere abgibt, sobald sie auf eine Antwort wartet. asyncio in Python und die Event Loop in Node.js arbeiten so. Ein einzelner Prozessorkern und ein einzelner Thread können Hunderte Netzwerkanfragen gleichzeitig „offen" halten, weil keine dieser Anfragen beim Warten den Prozessor nutzt.
Was ist Parallelism (Parallelität)?
Parallelität bedeutet, mehrere Aufgaben physisch gleichzeitig auf verschiedenen Prozessorkernen auszuführen. Im Küchenbeispiel bereiten zwei Köche gleichzeitig zwei verschiedene Gerichte zu. Die Geschwindigkeit wächst direkt mit der Zahl der Arbeitskräfte, aber jede braucht eine eigene Arbeitsfläche und eigene Zutaten.
In der Programmierung wird Parallelität meist mit mehreren Prozessen oder mit Threads erreicht, die wirklich gleichzeitig laufen können. Ihr Nutzen zeigt sich, wenn die Arbeit den Prozessor stark beansprucht: ein großes HTML-Dokument parsen, Bilder verarbeiten, Daten komprimieren.
| Kriterium | Concurrency (Nebenläufigkeit) | Parallelism (Parallelität) |
|---|---|---|
| Grundidee | Wartezeiten überlappen | Arbeit auf Kerne verteilen |
| Kernbedarf | Ein Kern genügt | Mehrere Kerne |
| Geeignete Arbeit | I/O-lastig: Netzwerk, Festplatte, Datenbank | CPU-lastig: Parsen, Berechnung |
| Python-Werkzeuge | asyncio, Threads | multiprocessing, ProcessPoolExecutor |
| Node.js-Werkzeuge | Event Loop, Promise | worker_threads, mehrere Prozesse |
| Speicherkosten | Gering, klein pro Aufgabe | Hoch, eigener Speicher pro Prozess |
| Rolle beim Scraping | Anfragen senden und Antworten empfangen | Seiten parsen, Headless-Browser betreiben |
| Typischer Fehler | Das Ziel mit unbegrenzter Nebenläufigkeit belasten | I/O-Arbeit auf Prozesse verteilen und Ressourcen verschwenden |
Die beiden Konzepte sind keine Alternativen. Ein großes Scraping-System nutzt meist beide: Jeder Prozess verwaltet intern Hunderte Anfragen nebenläufig, und die Prozesse laufen parallel auf verschiedenen Kernen.
Warum ist Scraping I/O-lastig?
Ein Blick auf die Stationen einer einzelnen Seitenanfrage zeigt, wie wenig der Prozessor arbeitet:
- DNS-Auflösung. Den Domainnamen in eine IP-Adresse umwandeln. Eine Abfrage und eine Antwort über das Netz.
- Verbindung zum Proxy und Tunnel. Eine TCP-Verbindung zum Proxy, eine
CONNECT-Anfrage, dann Warten, bis der Proxy das Ziel erreicht. - TLS-Handshake. Eine verschlüsselte Verbindung zum Zielserver. Braucht mehrere Hin- und Rückwege.
- Anfrage senden und auf Antwort warten. Der Zielserver bereitet die Seite vor; bei Seiten mit Datenbankabfragen dauert das länger.
- Antwort herunterladen. Hängt von Seitengröße und Bandbreite ab.
- Parsen. Daten aus dem HTML extrahieren. Nur dieser Schritt nutzt den Prozessor.
In den ersten fünf Schritten wartet das Programm auf Bytes aus dem Netz. Bei einem Skript, das Seiten nacheinander herunterlädt, ist das Parsen ein kleiner Teil der Gesamtzeit; das Skript verbringt die meiste Zeit mit Warten, während der Prozessor untätig ist.
Dieses Verhältnis ist aber nicht fest. Auf einem lokalen Server, der jede Antwort um 300 Millisekunden verzögert, haben wir 40 Seiten mit je 400 Produktkarten heruntergeladen und geparst. Die Nebenläufigkeit von 1 auf 5 und 20 zu erhöhen, verkürzte die Downloadzeit um mehr als das Zehnfache. Bei einer Nebenläufigkeit von 20 dauerte das Parsen derselben Seiten auf einem Kern länger als das Herunterladen; ein Prozesspool halbierte diesen Schritt ungefähr. Mit steigender Nebenläufigkeit kann sich der Engpass also vom Netz zum Prozessor verschieben. Für Ihre Ziele werden die Zahlen anders ausfallen; wir empfehlen, Ihre eigene Aufgabe mit dem Messgerüst unten zu messen.
Was ist der Unterschied zwischen asyncio, Threads und Prozessen in Python?
Python hat drei Werkzeuge, und der Schlüssel zur Wahl ist der GIL (Global Interpreter Lock). Im Standard-Interpreter CPython erlaubt der GIL, dass jeweils nur ein Thread Python-Code ausführt. Ein Thread gibt den GIL aber frei, während er auf Daten aus dem Netz wartet. Daraus folgt:
asyncio: eine Event Loop in einem einzigen Thread. Aufgaben geben anawait-Stellen die Kontrolle aneinander ab. Für Hunderte oder Tausende nebenläufige Netzwerkanfragen ist es die leichteste Option. Die asyncio-Dokumentation beschreibt die Bausteine dieses Modells. Auch die Bibliothek muss asynchron sein: derAsyncClientvon HTTPX oder AIOHTTP.- Threads (
ThreadPoolExecutor): Weil der GIL beim Warten auf das Netz freigegeben wird, bringen Threads bei I/O-Arbeit echte Beschleunigung. Sie sind der einfachste Weg zu Nebenläufigkeit mit synchronen Bibliotheken wie Requests. Ein Thread kostet mehr Speicher als eineasyncio-Aufgabe, daher sinkt die Effizienz nach einigen Hundert gleichzeitigen Anfragen. - Prozesse (
ProcessPoolExecutor,multiprocessing): Jeder Prozess läuft mit eigenem Interpreter und eigenem GIL und bietet deshalb bei CPU-lastiger Arbeit echte Parallelität. Prozesse zu starten und Daten zwischen ihnen zu übertragen, ist teuer. Die concurrent.futures-Dokumentation definiert die gemeinsame Schnittstelle von Thread- und Prozesspools.
Mit Python 3.13 kam außerdem ein experimenteller Build, der ohne GIL laufen kann; weil die Bibliothekskompatibilität noch begrenzt ist, bleiben der Standard-Build und die obigen drei Werkzeuge in produktiven Scraping-Aufgaben verbreitet.
| Werkzeug | Bietet es Parallelität? | Geeignete Arbeit | Einsatz beim Scraping |
|---|---|---|---|
asyncio + HTTPX/AIOHTTP | Nein, ein Thread | Viele Netzwerkanfragen | Seiten herunterladen |
ThreadPoolExecutor + Requests | Während I/O faktisch ja | Mittlere Zahl an Netzwerkanfragen, synchroner Code | Bestehenden Requests-Code beschleunigen |
ProcessPoolExecutor | Ja | CPU-lastige Arbeit | Große Seiten parsen |
Die Unterstützung für Nebenläufigkeit in Python-Bibliotheken vergleichen wir in HTTPX, Requests und AIOHTTP im Vergleich.
Beispiel 1: begrenzte Nebenläufigkeit mit asyncio.Semaphore
Der folgende Code lädt Seiten nebenläufig herunter, begrenzt aber die Zahl gleichzeitig offener Anfragen mit dem Wert limit. Ohne Semaphore startet asyncio.gather alle Anfragen auf einmal, was Ihren eigenen Verbindungspool ebenso belastet wie die Zielseite.
import asyncio
import httpx
async def fetch(client, sem, url):
async with sem:
response = await client.get(url)
response.raise_for_status()
return response.text
async def fetch_all(urls, limit=10, proxy=None):
sem = asyncio.Semaphore(limit)
async with httpx.AsyncClient(proxy=proxy, timeout=30) as client:
return await asyncio.gather(*(fetch(client, sem, u) for u in urls), return_exceptions=True)
urls = [f"https://example.com/product/{i}" for i in range(100)]
pages = asyncio.run(fetch_all(urls, limit=10, proxy="http://user:pass@pr.proxynet.io:8000"))Ein einziger AsyncClient wird für alle Anfragen geteilt. So werden Verbindungen wiederverwendet und nicht bei jeder Anfrage ein neuer TLS-Handshake durchgeführt.
Beispiel 2: nebenläufig herunterladen, parallel parsen
Sind Seiten groß und dauert das Parsen spürbar, lohnt es sich, beide Modelle zu kombinieren: Herunterladen mit asyncio, Parsen in einem Prozesspool.
import asyncio
from concurrent.futures import ProcessPoolExecutor
from bs4 import BeautifulSoup
def parse(html):
soup = BeautifulSoup(html, "html.parser")
return [(p.h2.get_text(strip=True), p.select_one(".price").get_text(strip=True)) for p in soup.select(".product")]
async def scrape(urls, limit=10, proxy=None):
pages = await fetch_all(urls, limit=limit, proxy=proxy)
html_pages = [p for p in pages if isinstance(p, str)]
loop = asyncio.get_running_loop()
with ProcessPoolExecutor() as pool:
return await asyncio.gather(*(loop.run_in_executor(pool, parse, html) for html in html_pages))
if __name__ == "__main__":
results = asyncio.run(scrape(urls, limit=10))Die Zeile if __name__ == "__main__": ist unter Windows und macOS Pflicht: Der Prozesspool startet neue Prozesse, indem er das Modul von vorne lädt, und ohne diesen Schutz führt sich der Code immer wieder selbst aus.
Wichtig ist, dass dieser Aufbau nicht immer schneller ist. Prozesse zu starten und HTML zwischen ihnen zu übertragen, kostet Zeit; sind Seiten klein und wenige, können diese Kosten das Parsen selbst übersteigen. Im Test oben verkürzte der Prozesspool das Parsen bei Seiten mit 400 Karten deutlich, doch zur Gesamtzeit aus Herunterladen und Parsen kamen die Startkosten der Prozesse hinzu. Messen Sie vor der Entscheidung mit Ihren eigenen Seiten.
Wie funktioniert die Event Loop von Node.js?
Node.js führt JavaScript-Code in einem einzigen Thread aus und verwaltet Netzwerkoperationen über eine Event Loop. Rufen Sie fetch auf, übergibt Node.js die Anfrage an das Betriebssystem, und der JavaScript-Code läuft weiter; kommt die Antwort, wird der zugehörige Callback eingereiht. Der Event-Loop-Leitfaden von Node.js erklärt die Phasen dieser Schleife ausführlich.
Für Scraping hat das drei Folgen:
- Netzwerkanfragen sind von Natur aus nebenläufig. Jede
Promise-basierte Anfrage blockiert beim Warten die anderen nicht. Das ähnelt demasyncio-Modell von Python. - CPU-lastiger Code stoppt die ganze Schleife. Während eine synchrone Funktion ein großes HTML-Dokument parst, kann keine andere Antwort verarbeitet werden. Für solche Arbeit nutzen Sie separate Threads mit dem Modul
worker_threadsoder separate Prozesse. - Die Namensauflösung kann ein versteckter Engpass sein. Die Standardfunktion
dns.lookupvon Node.js führt den Resolver des Betriebssystems im kleinen Thread-Pool von libuv aus. Verbindet sich ein Skript gleichzeitig mit vielen verschiedenen Domains, kann dieser Pool voll laufen. Mit einem Proxy werden Zieldomains auf Proxy-Seite aufgelöst, was den Effekt mindert.
Um die Nebenläufigkeit zu begrenzen, brauchen Sie keine zusätzliche Bibliothek; ein einfacher Begrenzer genügt:
function limiter(max) {
let active = 0;
const queue = [];
const next = () => {
if (active >= max || queue.length === 0) return;
active++;
const { task, resolve, reject } = queue.shift();
task().then(resolve, reject).finally(() => {
active--;
next();
});
};
return (task) => new Promise((resolve, reject) => {
queue.push({ task, resolve, reject });
next();
});
}
const limit = limiter(10);
const results = await Promise.allSettled(
urls.map((url) => limit(() => fetch(url).then((r) => r.text()))),
);Wie Sie in Node.js einen Proxy übergeben, samt Beispiel mit Rotation und Wiederholungen, erklären wir in Proxys in Node.js verwenden.
Was begrenzt die Geschwindigkeit wirklich?
Mehr Nebenläufigkeit steigert die Geschwindigkeit bis zu einem Punkt; danach wirkt sie nicht mehr oder verlangsamt die Aufgabe. Beim Scraping setzen meist nicht der Prozessor, sondern diese Faktoren die Obergrenze:
- Das Rate-Limit der Zielseite. Erlaubt die Website pro Minute eine bestimmte Zahl von Anfragen aus derselben Quelle, erzeugt mehr Nebenläufigkeit nur mehr
429-Antworten. Den Umgang mit diesen Codes erklären wir in HTTP-Statuscodes beim Web Scraping. - Die Kapazität des Zielservers. Wird der Server einer kleinen Website langsamer, verlängert Ihre Nebenläufigkeit deren Antwortzeiten; Ihre Aufgabe und die echten Besucher der Website werden beide langsamer.
- Proxy-Kapazität. Das Limit gleichzeitiger Verbindungen des Tarifs, die Zahl der IPs im Pool und die Antwortzeiten der Ausgangspunkte. Mit einem Rotierender Proxy, der über eine Adresse viele Ausgangs-IPs bietet, verteilen sich Anfragen auf verschiedene Adressen, doch das Verbindungslimit des Tarifs gilt weiterhin.
- Bandbreite. Beim Herunterladen großer Seiten und bildreicher Antworten greift das Limit Ihrer Verbindung oder Ihres Proxy-Traffics.
- Kosten des Verbindungsaufbaus. Wird bei jeder Anfrage eine neue Verbindung mit neuem TLS-Handshake aufgebaut, steigt die Zeit. Den Client zu teilen und Verbindungen wiederzuverwenden, senkt diese Kosten.
- Entfernung. Die Latenz zwischen Ausgangspunkt des Proxys und Zielserver kommt zu jedem Hin- und Rückweg hinzu. Bei Residential-Proxy-Adressen, die über echte Privatanschlüsse laufen, schwankt diese Zeit je nach Standort und Leitung.
- Der Prozessor. Er wird erst entscheidend, wenn Headless-Browser laufen, sehr große Dokumente geparst werden oder auf derselben Maschine aufwendige Datenverarbeitung stattfindet.
Weitere Wege, die Geschwindigkeit einzustellen, ohne gesperrt zu werden oder der Website zu schaden, behandeln wir in Web Scraping ohne Sperren.
Wie hoch sollte die Nebenläufigkeit sein?
Eine Zahl für jede Aufgabe gibt es nicht; der richtige Wert entsteht durch Messen. Die Methode, die wir empfehlen:
- Beginnen Sie mit einem niedrigen Wert. Einige gleichzeitige Anfragen für eine einzelne Zielseite.
- Messen Sie drei Dinge. Abgeschlossene Seiten pro Minute, durchschnittliche Antwortzeit und Fehlerrate (
429,503, Timeouts). - Erhöhen Sie den Wert schrittweise. Wiederholen Sie bei jedem Schritt dieselbe Messung.
- Finden Sie den Wendepunkt. Steigen die abgeschlossenen Seiten nicht mehr, während Antwortzeit oder Fehlerrate steigen, gehen Sie zum vorherigen Wert zurück.
- Setzen Sie ein Limit pro Website. Crawlen Sie mehrere Websites gleichzeitig, darf die Gesamt-Nebenläufigkeit hoch sein, doch der Anteil pro Website sollte klein bleiben.
- Messen Sie regelmäßig neu. Infrastruktur und Rate-Limits von Zielseiten ändern sich mit der Zeit.
Ein einfaches Messgerüst:
import asyncio
import time
def measure(label, fn):
start = time.perf_counter()
result = fn()
elapsed = time.perf_counter() - start
print(f"{label}: {elapsed:.2f} s")
return result
for limit in (1, 5, 10, 20):
pages = measure(f"limit={limit}", lambda: asyncio.run(fetch_all(urls, limit=limit)))
errors = sum(isinstance(p, Exception) for p in pages)
print(f" Fehler: {errors} / {len(pages)}")time.perf_counter() ist ein hochauflösender Zähler, der von Änderungen der Systemuhr nicht beeinflusst wird, und eignet sich für Zeitmessungen besser als time.time(). Messen Sie mit einer kleinen URL-Liste, die die Zielseite nicht belastet, und nach Möglichkeit zu Zeiten, in denen die Website wenig besucht ist.
Anwendungsfälle
- Wenige Seiten von einer Website: synchrones Requests mit Pausen zwischen Anfragen. Keine Nebenläufigkeit nötig.
- Preis- und Katalogerfassung von vielen Websites: Nebenläufigkeit mit kleinem Limit pro Website über
asynciound ein rotierender Proxy zur Verteilung. Den allgemeinen Aufbau zeigt unsere Seite zur Datenerfassung. - Großes Crawling: mehrere Prozesse mit je
asyncio; eine Warteschlange pro Domain. Die Skalierung beschreibt unsere Seite zur Web-Crawler-Lösung. - Ein bestehendes Requests-Skript beschleunigen: mit
ThreadPoolExecutor, ohne den Code neu zu schreiben. Einen rotierenden Requests-Aufbau zeigt Proxys in Python rotieren. - Mit JavaScript geladene Seiten: Weil ein Headless-Browser viel CPU und Speicher braucht, begrenzen Kerne und Speicher die Zahl gleichzeitiger Browser.
Häufige Fehler
- I/O-lastige Arbeit auf Prozesse verteilen. Sie zahlen Startkosten und Speicher, doch die Geschwindigkeit steigt nicht, weil der Engpass das Netz ist.
- Eine synchrone Bibliothek in
asyncionutzen. Einrequests.get()-Aufruf in einerasync defstoppt die Event Loop, und alle Aufgaben laufen nacheinander. - Unbegrenztes
gatheroderPromise.all. Tausende Anfragen auf einmal zu starten, führt zu Verbindungsfehlern und Rate-Limits bei der Zielseite. - Für jede Anfrage einen neuen Client erstellen. Verbindungen werden nicht wiederverwendet, und jede Anfrage braucht einen neuen TLS-Handshake.
- Nur auf die Gesamtzeit schauen, nicht auf die Fehlerrate. Eine Aufgabe, die schneller wird, während ihre
429-Rate steigt, erfasst weniger Daten. - In Node.js im Hauptthread parsen. Bei großen Dokumenten stoppt die Event Loop, und wartende Antworten laufen womöglich in ein Timeout.
Entscheidungshilfe
| Ihre Situation | Empfehlung |
|---|---|
| Viele Seiten, kleines HTML | asyncio + HTTPX/AIOHTTP, mit Semaphore begrenzt |
| Bestehender synchroner Requests-Code | ThreadPoolExecutor |
| Große Seiten, aufwendiges Parsen | Herunterladen mit asyncio, Parsen mit ProcessPoolExecutor |
| Seiten mit Node.js herunterladen | Promise + Begrenzer für Nebenläufigkeit |
| Aufwendiges Parsen in Node.js | worker_threads |
| Headless-Browser | Wenige gleichzeitige Browser je nach Kernen und Speicher |
Die 429-Rate steigt | Nebenläufigkeit senken, Limit pro Website setzen |
| Keine Beschleunigung und keine Fehler | Engpass messen: Proxy, Bandbreite, Zielserver |
Häufig gestellte Fragen
Sind Concurrency und Parallelism dasselbe?
Nein. Nebenläufigkeit bedeutet, dass Aufgaben im selben Zeitraum vorankommen, und ist auf einem Kern möglich. Parallelität bedeutet, dass Aufgaben wirklich gleichzeitig auf mehreren Kernen laufen. Jedes parallele System ist nebenläufig, aber nicht jedes nebenläufige System parallel.
asyncio oder Threads für Scraping?
Schreiben Sie neu und senden viele Anfragen, verwaltet asyncio mit weniger Ressourcen mehr gleichzeitige Anfragen. Ist Ihr bestehender Code mit Requests geschrieben und gehen die gleichzeitigen Anfragen nicht über einige Hundert hinaus, ist ThreadPoolExecutor der einfachste Weg zur Beschleunigung ohne Neuschreiben.
Verlangsamt der GIL das Scraping?
Da der GIL während Netzwerkanfragen freigegeben wird, verlangsamt er das Herunterladen in der Praxis nicht. Bei CPU-lastigen Schritten wie dem Parsen können Threads nicht gleichzeitig Python-Code ausführen; für diese Schritte wird ein Prozesspool genutzt.
Erhöhen mehr Proxy-IPs die Geschwindigkeit?
Zählt die Zielseite ihr Rate-Limit pro IP, kann die Lastverteilung Ihnen erlauben, in derselben Zeit mehr Seiten zu erreichen. Wendet die Website das Limit aber nach anderen Kriterien an oder ist der Engpass Bandbreite oder Serverkapazität, ändert die IP-Zahl nichts. Der Website nicht zu schaden, ist eine Verantwortung unabhängig davon, wie viele IPs Sie haben.
Was passiert, wenn ich die Nebenläufigkeit zu hoch setze?
Auf Ihrer Seite stoßen Sie an Grenzen von Verbindungspool und Dateihandles; auf der Zielseite entstehen 429-Antworten, Timeouts und eine langsamere Website. Ab einem Punkt steigen die abgeschlossenen Seiten nicht mehr, wohl aber die Fehler.
Wie wird die Nebenläufigkeit bei Headless-Browsern eingestellt?
Jede Browserinstanz braucht deutlich Speicher und CPU, daher begrenzen die Maschinenressourcen die Zahl gleichzeitiger Seiten, nicht das Netz. Beginnen Sie mit wenigen Browserinstanzen und erhöhen Sie unter Beobachtung von Speicher- und CPU-Auslastung. Wann ein Headless-Browser wirklich nötig ist, erklären wir in Statische und dynamische Seiten.
Fazit
Nebenläufigkeit nutzt Wartezeit, Parallelität nutzt Prozessorkerne. Weil beim Scraping die meiste Zeit mit dem Warten auf Netzwerkantworten vergeht, steigert meist begrenzte Nebenläufigkeit mit asyncio, Threads oder der Event Loop von Node.js die Geschwindigkeit; ein Prozesspool ist nur für CPU-lastige Schritte wie aufwendiges Parsen und Headless-Browser nötig. Die Obergrenze setzen meist Rate-Limit der Zielseite, Proxy-Kapazität und Bandbreite. Legen Sie die Nebenläufigkeit durch Messen fest, nicht durch Schätzen, und setzen Sie ein Limit pro Website. Für Datenerfassung im großen Maßstab sehen Sie sich unsere Proxy-Dienste an.




