Async Python Scraping mit httpx, aiohttp und Proxies

Async verwandelt einen langsamen Scraper in einen schnellen – und einen schnellen Scraper in einen blockierten, es sei denn, Sie setzen Proxies, Semaphoren und Timeouts richtig ein. Hier ist das gesamte Muster für httpx und aiohttp, mit Code.

Wenn Ihr Scraper die meiste Zeit damit verbringt, auf das Netzwerk zu warten, ist async der größte Geschwindigkeitsvorteil – und Proxies sind das, was diese Geschwindigkeit davon abhält, Sie zu sperren. Dieser Leitfaden behandelt async Python Scraping mit httpx und aiohttp plus Proxies von Anfang bis Ende: wie jede Bibliothek einen Proxy setzt, wie man die Parallelität mit einem Semaphor begrenzt, wie man Timeouts setzt, die tatsächlich ausgelöst werden, und wie man IPs rotiert, ohne Ihre Sitzungen zu zerstören. Alles mit ausführbarem Code und Platzhalter-Anmeldedaten, die Sie durch Ihre eigenen ersetzen.

Warum async und wo Proxies passen

Standard requests ist blockierend: Jeder Aufruf wartet auf die Antwort, bevor der nächste startet. Holen Sie 500 Seiten ab und Sie zahlen 500 Rundreisen nacheinander. Async holt sie gleichzeitig in einer Ereignisschleife ab, sodass sich die Gesamtzeit dem langsamsten einzelnen Request annähert, anstatt der Summe. Der Haken: Ein Schwall gleichzeitiger Anfragen von einer IP ist genau das Signal, auf das Anti-Bot-Systeme achten. Die Lösung ist nicht, auf ein Kriechen zu verlangsamen – es ist, den Verkehr über einen rotierenden Proxy-Pool zu verteilen und absichtlich mit einem Semaphor zu drosseln.

Setzen Sie einen Proxy in httpx (async)

httpx ist der pragmatische Standard, da ein Client-Modell sowohl synchron als auch asynchron funktioniert und HTTP/2 spricht. Beachten Sie die moderne API: Es ist ein singuläres proxy=-Argument auf dem Client, nicht das alte proxies=-Dict – eine häufige Quelle der "Warum wird mein Proxy ignoriert"-Verwirrung nach einem Upgrade. Anmeldedaten gehen direkt in die Proxy-URL.

import asyncio, httpx

PROXY = "http://USER:PASS@gate.quantumproxies.io:8000"

async def fetch(client, url):
    r = await client.get(url, timeout=httpx.Timeout(20.0))
    return url, r.status_code, r.text

async def main(urls):
    async with httpx.AsyncClient(proxy=PROXY, http2=True) as client:
        tasks = [fetch(client, u) for u in urls]
        return await asyncio.gather(*tasks)

urls = ["https://httpbin.org/ip"] * 5
print(asyncio.run(main(urls)))

Ein Client, der bei jeder Anfrage wiederverwendet wird, ist der Punkt – er hält den Verbindungspool warm, sodass Sie wiederholte TLS-Handshakes über den Proxy überspringen. Einen neuen Client pro Anfrage zu erstellen, ist der häufigste async Performance-Fehler: Es verwirft die Wiederverwendung von Verbindungen und den Cookie-Zustand bei jedem Aufruf.

Setzen Sie einen Proxy in aiohttp (pro Anfrage)

aiohttp ist asyncio-nativ und bietet die feinste Kontrolle über die Parallelität, aber seine Proxy-Konvention unterscheidet sich: Der Proxy wird pro Anfrage auf session.get() übergeben, nicht auf der Sitzung. Das ist tatsächlich praktisch für die Rotation. Zwei Dinge beißen hier die Leute – der Standard-User-Agent ist buchstäblich Python/3.x aiohttp/3.x, ein eindeutiges Zeichen, das Sie überschreiben müssen, und Sie sollten die Größe des Verbindungspools explizit festlegen.

import aiohttp, asyncio

PROXY = "http://USER:PASS@gate.quantumproxies.io:8000"
HEADERS = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
           "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36"}

async def fetch(session, url):
    timeout = aiohttp.ClientTimeout(total=30, connect=10, sock_read=20)
    async with session.get(url, proxy=PROXY, timeout=timeout) as resp:
        return url, resp.status, await resp.text()

async def main(urls):
    conn = aiohttp.TCPConnector(limit=20, limit_per_host=8)
    async with aiohttp.ClientSession(connector=conn, headers=HEADERS) as session:
        return await asyncio.gather(*(fetch(session, u) for u in urls))

print(asyncio.run(main(["https://httpbin.org/ip"] * 5)))

TCPConnector(limit=20, limit_per_host=8) begrenzt die insgesamt offenen Verbindungen und die Verbindungen zu einem einzelnen Host – eine erste Linie der Höflichkeit, die verhindert, dass ein Ziel Ihren gesamten Pool absorbiert.

Diagramm einer async Scraping-Schleife: asyncio.gather füttert einen Semaphor, dann ein rotierendes Proxy-Gateway, dann parallele Zielabrufe
Der Semaphor drosselt; das rotierende Gateway verschleiert. Sie brauchen beides – async Geschwindigkeit ohne eines von beiden ist eine Blockwelle.

Begrenzen Sie die Parallelität mit einem Semaphor

Unbegrenzte gleichzeitige Anfragen abzufeuern, ist der schnellste Weg, einen Proxy-Pool zu verbrennen und Ratenlimits auszulösen. Ein asyncio.Semaphore ist die Drossel: Es begrenzt, wie viele Anfragen gleichzeitig in Bearbeitung sind, unabhängig davon, wie viele Aufgaben Sie in die Warteschlange stellen. Setzen Sie ein globales Limit und für aggressive Jobs auch ein pro-Domain-Limit.

sem = asyncio.Semaphore(15)  # never more than 15 requests in flight

async def guarded_fetch(client, url):
    async with sem:
        return await fetch(client, url)

async def main(urls):
    async with httpx.AsyncClient(proxy=PROXY) as client:
        return await asyncio.gather(*(guarded_fetch(client, u) for u in urls))

Die richtige Anzahl hängt von der Toleranz des Ziels und Ihrer Poolgröße ab, nicht davon, wie schnell Ihre Maschine gehen kann. Beginnen Sie konservativ (10–20), beobachten Sie Ihre Blockrate und erhöhen Sie sie nur, solange der Erfolg hoch bleibt. Bei einer einzelnen Sticky-IP halten Sie sie einstellig.

Timeouts: Modellieren Sie jede Phase

Eine proxied Anfrage schlägt an mehr Stellen fehl als eine direkte – DNS-Auflösung, Verbindung zum Proxy, Tunnelaufbau, Verbindung zum Ziel, Warten auf Header und Lesen des Körpers sind allesamt unterschiedliche Stalls. Ein einzelnes Pauschal-Timeout verbirgt, welche Phase gehangen hat. aiohttp's ClientTimeout(total=, connect=, sock_read=) und httpx's Timeout() ermöglichen es Ihnen, sie separat zu begrenzen. Die eine Regel ohne Ausnahmen: Niemals eine Anfrage ohne Timeout ausgeben, sonst hängt ein toter Ausgang eine Coroutine für immer und verhungert leise Ihre Ereignisschleife.

Rotieren Sie Proxies, ohne Sitzungen zu zerstören

Es gibt zwei Rotationsstrategien und die falsche Wahl korrumpiert Ihre Daten. Zufällige Rotation pro Anfrage ist perfekt für zustandslose Seitenabrufe. Aber es zerstört jeden Ablauf, der von Cookies, Login oder Lokalisierung abhängt, da die zweite Anfrage auf einer anderen IP als die erste landet. Die saubere Trennung: Rotieren Sie an der logischen Einheitengrenze – eine IP pro Crawl-Segment oder pro Konto – und verwenden Sie ein rotierendes Gateway, das Ihnen automatisch einen neuen Ausgang gibt, sodass Ihr Code niemals eine Liste verwaltet.

# rotating gateway: one endpoint, new exit IP per request
ROT = "http://USER:PASS@rotating.quantumproxies.io:8000"

# sticky session: same IP for a multi-step flow, tag the session id
STICKY = "http://USER-session-a1b2:PASS@gate.quantumproxies.io:8000"

async def crawl_segment(urls):
    async with httpx.AsyncClient(proxy=ROT) as client:  # rotates per call
        return await asyncio.gather(*(fetch(client, u) for u in urls))

Ein rotierendes Residential-Gateway ist der pragmatische Standard für High-Concurrency-Scraping: Rotation pro Anfrage über 90M+ IPs in 200+ Ländern, mit Sticky-Sessions, wenn ein Warenkorb oder Login denselben Ausgang für ein paar Minuten benötigt. Wenn Sie Residential gegen ISP oder Rechenzentrum für den Job abwägen, legt unser Leitfaden zu welchen Proxy-Typ Sie verwenden sollten die Kompromisse dar.

Holen Sie sich ein rotierendes Residential-Gateway

Wiederholungen, Backoff und Jitter

Tote Ausgänge und vorübergehende Blocks sind im großen Maßstab normal, nicht außergewöhnlich. Aber naive Wiederholungen verschlimmern die Situation: Wenn 50 async Aufgaben im selben Moment fehlschlagen und alle sofort wiederholen, feuern Sie eine synchronisierte Welle ab, die das Ziel härter trifft als der ursprüngliche Lauf. Fügen Sie exponentielles Backoff plus randomisierten Jitter hinzu, damit sich Wiederholungen verteilen, begrenzen Sie die Versuche und rotieren Sie die IP bei einem Fehler, anstatt die verbrannte erneut zu verwenden.

import random
from httpx import HTTPError

async def robust_fetch(client, url, tries=3):
    for attempt in range(tries):
        try:
            r = await client.get(url, timeout=httpx.Timeout(20.0))
            if r.status_code < 400 and looks_real(r.text):
                return r
        except HTTPError:
            pass
        # exponential backoff + jitter before the next attempt
        await asyncio.sleep((2 ** attempt) + random.uniform(0, 1))
    return None
Gegenüberstellung der httpx und aiohttp Proxy-Konventionen, Timeouts und Parallelitätskontrollen für async Scraping
Zwei Bibliotheken, zwei Proxy-Konventionen: httpx setzt den Proxy auf dem Client, aiohttp pro Anfrage.

Ein 200 ist kein Erfolg

Der subtilste async Scraping-Fehler ist, HTTP 200 als erledigt zu behandeln. Anti-Bot-Systeme geben 200 mit einer CAPTCHA-Seite, einem Zugangsverweigerungshinweis, einem leeren Ergebnissatz oder einer JS-Herausforderung zurück – also ein Proxy, der "Erfolg" nur anhand des Statuscodes bewertet, liefert Ihnen stillschweigend blockierte Seiten. Validieren Sie den Inhalt: Überprüfen Sie auf ein bekanntes Element, eine Mindestlänge oder das Fehlen von Herausforderungsmarkern, bevor Sie einer Antwort vertrauen. Das ist das looks_real() Tor in der oben beschriebenen Wiederholungsschleife.

Wann man aufhören sollte, den Stack selbst zu bauen

Das oben beschriebene Muster – async Client, Semaphor, Timeouts, Rotation, Inhaltsvalidierung – behandelt die meisten Ziele sauber. Aber sobald eine Seite Cloudflare, TLS-Fingerprinting oder schwere Client-seitige Renderingschichten hinzufügt, verliert rohes async HTTP unabhängig von Ihrem Proxy, weil ein Python TLS-Handshake nichts mit dem von Chrome zu tun hat. An dieser Linie ist ein Scraper API, das einen echten Browser-Fingerabdruck trägt, IPs rotiert und JavaScript bei Bedarf rendert, weniger Code und eine höhere Erfolgsquote als alles selbst zu pflegen. Unser Beitrag zu Headless vs HTTP Kosten behandelt, wo sich diese Eskalation auszahlt.

Häufig gestellte Fragen

Wie verwende ich einen Proxy mit aiohttp?

Übergeben Sie die Proxy-URL pro Anfrage: session.get(url, proxy="http://user:pass@host:port"). Im Gegensatz zu requests nimmt aiohttp kein Proxies-Dict auf der Sitzung. Überschreiben Sie immer den Standard-User-Agent (Python/3.x aiohttp/3.x ist ein offensichtliches Bot-Signal) und setzen Sie ein ClientTimeout, damit ein toter Ausgang die Coroutine nicht hängen lässt.

Ist httpx oder aiohttp besser für async Scraping?

Greifen Sie standardmäßig zu httpx: Ein Client funktioniert synchron und asynchron, HTTP/2 ist integriert, und der Proxy ist ein einzelnes Argument. Wählen Sie aiohttp, wenn Sie maximale Kontrolle über die Parallelität wünschen – explizite Verbindungspool-Begrenzungen und pro Anfrage Proxies eignen sich für große, schnelle Crawls. Beide sind in Ordnung; die Proxy-Strategie ist wichtiger als die Bibliothek.

Wie viele gleichzeitige Anfragen sollte ich ausführen?

Nicht so viele, wie Ihre Maschine zulässt – so viele, wie das Ziel und Ihr Pool tolerieren. Beginnen Sie mit einem Semaphor von 10–20 gleichzeitig, beobachten Sie die Block- und Fehlerrate und erhöhen Sie es nur, solange der Erfolg hoch bleibt. Bei einer Sticky-IP bleiben Sie einstellig. Eine breitere IP-Rotation ermöglicht es Ihnen, die Gesamtparallelität sicherer zu erhöhen.

Warum wird mein async Scraper blockiert, wenn der synchrone es nicht wurde?

Weil die Parallelität das Signal konzentriert: Viele gleichzeitige Anfragen von einer IP sind ein klassisches Bot-Muster. Verteilen Sie die Last über einen rotierenden Pool, drosseln Sie mit einem Semaphor, fügen Sie jittered Backoff bei Wiederholungen hinzu und validieren Sie den Antwortinhalt – ein 200 kann immer noch eine Herausforderungsseite sein. Geschwindigkeit ohne Rotation ist das, was Sie blockiert hat.

Das ist das vollständige async Muster: Wählen Sie einen Client, setzen Sie den Proxy richtig dafür, begrenzen Sie die Parallelität mit einem Semaphor, begrenzen Sie jede Timeout-Phase, rotieren Sie an der logischen Grenze und vertrauen Sie niemals einem bloßen 200. Richten Sie die Proxy-Schicht zuerst richtig ein und die meisten Blocklisten verschwinden, bevor Sie sie erreichen.

Probieren Sie die QuantumProxies Scraper API aus