Die Bibliothek httpx ist zum Standard für asynchrone HTTP-Anfragen in Python geworden, hat jedoch beim Arbeiten mit Proxys einige Nuancen, die Parser in der Produktion brechen können. Lassen Sie uns untersuchen, wie man die asynchrone Proxy-Rotation einrichtet, HTTP/2 aktiviert und warum Ihr Skript alle 50-100 Anfragen abstürzt.
Warum httpx und nicht requests oder aiohttp
requests ist synchron und unterstützt HTTP/2 nicht von Haus aus. aiohttp ist asynchron, aber die Proxy-Konfiguration ist dort weniger benutzerfreundlich, und die Unterstützung für HTTP/2 fehlt völlig. httpx löst beide Fragen: ein synchroner und ein asynchroner Client mit einer einheitlichen API, native Unterstützung für HTTP/2 über die Erweiterung h2 und flexible Arbeit mit Proxys auf Transportebene.
Für Aufgaben wie das Parsen von Marktplätzen (Wildberries, Ozon, Avito), das Sammeln von Daten über API von sozialen Netzwerken oder das Überwachen von Preisen von Wettbewerbern ist Asynchronität entscheidend — sie bietet einen erheblichen Geschwindigkeitsvorteil im Vergleich zu sequenziellen Anfragen. Aber ohne eine durchdachte Proxy-Rotation und Verbindungsmanagement wird Asynchronität zur Quelle neuer Probleme: Erschöpfung des Verbindungs-Pools, hängende Koroutinen, Dateideskriptor-Lecks.
Ein weiterer wichtiger Punkt ist, dass HTTP/2 mehrere Anfragen in einer TCP-Verbindung multiplexieren kann, was die Last auf den Zielserver verringert und hilft, einige naive Anti-Bot-Systeme zu umgehen, die die Anzahl der TCP-Handshakes von einer IP-Adresse überwachen.
Grundlegende Proxy-Konfiguration in httpx
Seit Version httpx 0.26+ hat sich die Syntax zur Konfiguration von Proxys geändert — anstelle eines Wörterbuchs proxies wird der Parameter proxy verwendet. Dies ist eine häufige Quelle der Verwirrung, wenn Sie sich alte Tutorials ansehen.
import httpx
import asyncio
PROXY_URL = "http://user:[email protected]:8000"
async def fetch(url: str):
async with httpx.AsyncClient(proxy=PROXY_URL, timeout=15.0) as client:
response = await client.get(url)
return response.status_code, response.text[:200]
asyncio.run(fetch("https://httpbin.org/ip"))
Wenn Sie Proxys für HTTP- und HTTPS-Verkehr trennen oder verschiedene Proxys für verschiedene Domains konfigurieren müssen, verwenden Sie httpx.Client(mounts=...) — die Transportmittel werden nach dem URL-Muster montiert:
mounts = {
"http://": httpx.AsyncHTTPTransport(proxy="http://user:[email protected]:8000"),
"https://": httpx.AsyncHTTPTransport(proxy="http://user:[email protected]:8000"),
}
async with httpx.AsyncClient(mounts=mounts) as client:
response = await client.get("https://example.com")
Für die Authentifizierung mit Benutzername und Passwort funktioniert das Format http://user:pass@host:port stabil für HTTP-Proxys. Für SOCKS5 müssen Sie zusätzlich httpx[socks] installieren, andernfalls erhalten Sie einen ImportError zur Laufzeit und nicht beim Schreiben des Codes — dies ist eine Falle, in die viele tappen.
Asynchrone Proxy-Rotation: funktionierender Code
Der Hauptfehler bei der Proxy-Rotation besteht darin, für jede Anfrage einen neuen AsyncClient zu erstellen. Dies tötet die Leistung, da der Client einen Verbindungs-Pool hält, und seine Neuschaffung macht alle Vorteile von Keep-Alive-Verbindungen zunichte. Der richtige Ansatz besteht darin, im Voraus einen Pool von Clients zu erstellen, einen pro Proxy, und die Aufgaben zwischen ihnen zu verteilen.
import httpx
import asyncio
import itertools
import random
PROXIES = [
"http://user:[email protected]:8000",
"http://user:[email protected]:8000",
"http://user:[email protected]:8000",
]
class ProxyPool:
def __init__(self, proxies: list[str]):
self.clients = [
httpx.AsyncClient(proxy=p, timeout=httpx.Timeout(15.0, connect=5.0))
for p in proxies
]
self._cycle = itertools.cycle(self.clients)
def get_client(self) -> httpx.AsyncClient:
return next(self._cycle) # round-robin
# für zufällige Auswahl: random.choice(self.clients)
async def close_all(self):
for client in self.clients:
await client.aclose()
async def fetch_with_retry(pool: ProxyPool, url: str, retries: int = 3):
for attempt in range(retries):
client = pool.get_client()
try:
response = await client.get(url)
if response.status_code == 200:
return response.text
except (httpx.ConnectTimeout, httpx.ProxyError, httpx.ReadTimeout):
await asyncio.sleep(0.5 * (attempt + 1))
continue
return None
async def main():
pool = ProxyPool(PROXIES)
urls = [f"https://example.com/page/{i}" for i in range(100)]
sem = asyncio.Semaphore(20) # begrenzen der Parallelität
async def bound_fetch(url):
async with sem:
return await fetch_with_retry(pool, url)
results = await asyncio.gather(*(bound_fetch(u) for u in urls))
await pool.close_all()
return results
asyncio.run(main())
Dieses Muster löst drei Probleme gleichzeitig: Es wiederverwendet TCP-Verbindungen über Keep-Alive, verteilt die Last auf Proxys im Round-Robin-Verfahren und begrenzt die Parallelität über ein Semaphore, um den Zielserver nicht zu überlasten und keine Rate-Limit-Sperren zu provozieren.
Für Produktions-Parser, die mit Wildberries, Ozon oder Instagram API arbeiten, ist es wichtig, dass der Proxy-Pool groß genug und geografisch vielfältig ist — andernfalls rettet die Rotation innerhalb eines Subnetzes nicht vor einer Sperrung. Hier sind residential Proxys gut geeignet, da sie IPs echter Benutzer haben und seltener von Anti-Bot-Systemen gefiltert werden, die ASN und den Ruf des Bereichs analysieren.
HTTP/2 in httpx: warum und wie aktivieren
Standardmäßig verwendet httpx HTTP/1.1. Um HTTP/2 zu aktivieren, müssen Sie die Erweiterung installieren und das Flag explizit übergeben:
# pip install httpx[http2]
async with httpx.AsyncClient(http2=True, proxy=PROXY_URL) as client:
response = await client.get("https://example.com")
print(response.http_version) # erwarten "HTTP/2"
Ein wichtiger Hinweis: Nicht alle Proxys unterstützen das Tunneln von HTTP/2 korrekt. Einige günstige Rechenzentrums-Proxys funktionieren nur als HTTP/1.1 Proxys, selbst wenn der Zielserver HTTP/2 unterstützt — in diesem Fall wird httpx stillschweigend auf HTTP/1.1 über ein CONNECT-Tunnel zurückfallen, und Sie werden dies nicht immer ohne eine explizite Überprüfung von response.http_version bemerken.
HTTP/2 bietet einen echten Vorteil in drei Szenarien: wenn Sie viele Anfragen an eine Domain stellen (Multiplexing in einer Verbindung verringert die Latenz), wenn es wichtig ist, die Sichtbarkeit der Anfragen für Anti-Bot-Schutz zu verringern (weniger TCP-Handshakes von einer IP sieht natürlicher aus) und wenn die Zielseite selbst HTTP/2 verlangt und Fehler oder gekürzte Inhalte an Clients auf HTTP/1.1 zurückgibt — dies kommt manchmal bei Seiten auf Cloudflare vor.
Für Aufgaben, bei denen Geschwindigkeit und Stabilität von HTTP/2-Verbindungen über Proxys wichtig sind, sind dedizierte Rechenzentrums-Proxys besser geeignet — sie sind schneller als residential Proxys und unterstützen in der Regel korrekt das Tunneln moderner Protokolle.
5 typische Fehler mit Proxys in httpx
1. Erstellen des Clients für jede Anfrage
Code wie async with httpx.AsyncClient(...) as client: await client.get(url) innerhalb einer Schleife ist ein Antipattern. Jedes Erstellen eines Clients bedeutet einen neuen TCP-Connect, einen neuen TLS-Handschlag und vollständige Ignorierung des Connection Poolings. Bei 1000 Anfragen kann der Geschwindigkeitsunterschied zwischen dem richtigen und dem falschen Ansatz 5-10 Mal betragen.
2. Fehlende Timeouts oder zu große Standard-Timeouts
Standardmäßig setzt httpx ein Timeout von 5 Sekunden für alles gleichzeitig — connect, read, write, pool. Bei der Arbeit mit instabilen Proxys ist dies für connect zu wenig, aber für read zu viel, wenn der Proxy einfach "hängt". Teilen Sie die Timeouts explizit auf:
timeout = httpx.Timeout(
connect=5.0,
read=10.0,
write=10.0,
pool=5.0
)
client = httpx.AsyncClient(proxy=PROXY_URL, timeout=timeout)
3. Ignorieren der Limits des Verbindungs-Pools
Bei einer großen Anzahl paralleler Aufgaben (gather für 500+ Koroutinen ohne Semaphore) beginnt httpx, PoolTimeout auszulösen, da das Limit der Verbindungen im Pool (standardmäßig 100) erschöpft ist. Stellen Sie die Limits explizit über httpx.Limits(max_connections=200, max_keepalive_connections=50) ein und verwenden Sie immer ein Semaphore zur Kontrolle der Parallelität.
4. Deaktivierung von verify ohne Verständnis der Konsequenzen
Viele kopieren verify=False aus Stack Overflow, um SSL-Fehler beim Arbeiten über Proxys mit MITM-Zertifikaten zu vermeiden. Dies öffnet die Tür für "Man-in-the-Middle"-Angriffe und kann zu Datenlecks über kompromittierte Proxys führen. Wenn der Proxy ein spezielles Zertifikat benötigt — fügen Sie es explizit über verify="/path/to/cert.pem" hinzu, anstatt die Überprüfung vollständig zu deaktivieren.
5. Nicht geschlossene Clients und Dateideskriptor-Lecks
Wenn Sie einen Pool von Clients manuell erstellen (wie im obigen Beispiel), vergessen Sie nicht, await client.aclose() für jeden von ihnen am Ende aufzurufen. Lang laufende Skripte ohne explizite Schließung von Verbindungen stoßen auf das Limit der offenen Dateideskriptoren des Betriebssystems (normalerweise 1024 unter Linux standardmäßig), und der Parser stürzt mit einem mysteriösen Fehler Too many open files nach ein paar Stunden Laufzeit ab.
Welchen Proxy-Typ für das Parsen wählen
Die Wahl des Proxys hat direkten Einfluss darauf, wie viele Fehler aus der obigen Liste Sie überhaupt sehen werden — instabile Proxys verursachen Timeouts und Verbindungsabbrüche, selbst wenn der Code perfekt geschrieben ist.
| Proxy-Typ | Geschwindigkeit | Risiko einer Sperrung | Wann verwenden |
|---|---|---|---|
| Rechenzentrum | Hoch | Hoch auf geschützten Seiten | Open API, schwacher Anti-Bot-Schutz |
| Residential | Mittel | Niedrig | Wildberries, Ozon, soziale Netzwerke |
| Mobile | Mittel-niedrig | Minimal | Instagram, TikTok API, strenger Schutz |
Für asynchrones Parsen mit hoher Geschwindigkeit bei Anfragen an APIs mit moderatem Schutz bleiben Rechenzentrums-Proxys eine wirtschaftlich vorteilhafte Option — das Wichtigste ist, die Last gleichmäßig über den Pool zu verteilen, wie im obigen Code gezeigt.
Checkliste vor der Produktion
- httpx-Clients werden einmal erstellt und wiederverwendet, nicht für jede Anfrage
- Timeouts sind auf connect/read/write/pool aufgeteilt, nicht das Standard-Timeout verwendet
- Parallelität wird über
asyncio.Semaphorebegrenzt - Limits des Verbindungs-Pools sind an die tatsächliche Last angepasst
- Es gibt eine Retry-Logik mit der Behandlung von ProxyError, ConnectTimeout, ReadTimeout
- Alle Clients werden explizit über
aclose()beim Beenden geschlossen - verify=False wird nicht ohne zwingende Notwendigkeit verwendet
- HTTP/2 ist bewusst aktiviert und über response.http_version überprüft
Fazit
httpx bietet leistungsstarke Werkzeuge für die asynchrone Arbeit mit Proxys und HTTP/2, erfordert jedoch eine sorgfältige Konfiguration des Client-Pools, der Timeouts und der Verbindungs-Limits. Die meisten Abstürze von Parsern in der Produktion sind nicht auf die Bibliothek zurückzuführen, sondern auf architektonische Fehler: das Neuschaffen von Clients, fehlende Begrenzung der Parallelität und falsche Behandlung von Proxy-Ausnahmen.
Wenn Sie einen Parser für Marktplätze oder API von sozialen Netzwerken erstellen, die verdächtige Aktivitäten aktiv überwachen, ist die Stabilität der Proxy-Infrastruktur ebenso wichtig wie der Code. Für solche Aufgaben sollten Sie sich die residential Proxys ansehen — sie reduzieren die Anzahl der ProxyError- und ConnectTimeout-Fehler aufgrund der höheren Reputation der IP-Adressen im Vergleich zu günstigen Rechenzentrums-Pools.