La bibliothèque httpx est devenue la norme pour les requêtes HTTP asynchrones en Python, mais lorsqu'il s'agit de travailler avec des proxies, elle présente des nuances qui peuvent casser les parseurs en production. Voyons comment configurer la rotation asynchrone des proxies, activer HTTP/2 et pourquoi votre script échoue tous les 50-100 requêtes.
Pourquoi httpx, et pas requests ou aiohttp
requests est synchrone et ne supporte pas HTTP/2 par défaut. aiohttp est asynchrone, mais la configuration des proxies y est moins conviviale, et le support de HTTP/2 est totalement absent. httpx répond à ces deux questions : un client synchrone et asynchrone avec une API unique, un support natif de HTTP/2 via l'extension h2, et une gestion flexible des proxies au niveau du transport.
Pour les tâches de parsing sur les marketplaces (Wildberries, Ozon, Avito), la collecte de données via les API des réseaux sociaux ou la surveillance des prix des concurrents, l'asynchronisme est crucial — il offre un gain de vitesse exponentiel par rapport aux requêtes séquentielles. Mais sans une rotation adéquate des proxies et une gestion des connexions, l'asynchronisme devient une source de nouveaux problèmes : épuisement du pool de connexions, coroutines bloquées, fuites de descripteurs de fichiers.
Un autre point important — HTTP/2 peut multiplexe plusieurs requêtes dans une seule connexion TCP, ce qui réduit la charge sur le serveur cible et aide à contourner certains systèmes anti-bot naïfs qui surveillent le nombre de handshakes TCP depuis une seule IP.
Configuration de base des proxies dans httpx
Depuis la version httpx 0.26+, la syntaxe de configuration des proxies a changé — au lieu d'un dictionnaire proxies, on utilise le paramètre proxy. C'est une source fréquente de confusion si vous consultez de vieux tutoriels.
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"))
Si vous devez séparer les proxies pour le trafic HTTP et HTTPS ou configurer différents proxies pour différents domaines, utilisez httpx.Client(mounts=...) — les transports sont montés selon le modèle d'URL :
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")
Pour l'authentification par nom d'utilisateur et mot de passe, le format http://user:pass@host:port fonctionne de manière fiable pour les proxies HTTP. Pour SOCKS5, vous devrez installer httpx[socks], sinon vous obtiendrez une ImportError à l'exécution, et non lors de l'écriture du code — c'est un piège dans lequel beaucoup tombent.
Rotation asynchrone des proxies : code fonctionnel
La principale erreur dans la rotation des proxies est de créer un nouveau AsyncClient pour chaque requête. Cela tue la performance, car le client maintient un pool de connexions, et sa recréation annule tous les avantages des connexions keep-alive. L'approche correcte consiste à créer un pool de clients à l'avance, un par proxy, et à répartir les tâches entre eux.
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
# pour un choix aléatoire : 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) # limiter le parallélisme
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())
Ce modèle résout trois problèmes à la fois : il réutilise les connexions TCP via keep-alive, répartit la charge entre les proxies de manière round-robin et limite le parallélisme via un sémaphore, afin de ne pas surcharger le serveur cible et de ne pas provoquer de bannissement en raison de la limite de taux.
Pour les parseurs en production qui travaillent avec Wildberries, Ozon ou l'API Instagram, il est important que le pool de proxies soit suffisamment grand et diversifié géographiquement — sinon, la rotation au sein d'un même sous-réseau ne sauvera pas de la blocage. Les proxies résidentiels sont bien adaptés ici, car ils possèdent des IP de véritables utilisateurs et sont moins souvent filtrés par les systèmes anti-bot qui analysent l'ASN et la réputation de la plage d'adresses.
HTTP/2 dans httpx : pourquoi et comment l'activer
Par défaut, httpx utilise HTTP/1.1. Pour activer HTTP/2, vous devez installer l'extension et transmettre explicitement le drapeau :
# 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) # nous attendons "HTTP/2"
Un point important : tous les proxies ne supportent pas correctement le tunneling HTTP/2. Certains proxies de centres de données bon marché fonctionnent uniquement comme des proxies HTTP/1.1 même si le serveur final supporte HTTP/2 — dans ce cas, httpx reviendra silencieusement à HTTP/1.1 via un tunnel CONNECT, et vous ne le remarquerez pas toujours sans vérification explicite de response.http_version.
HTTP/2 offre un réel avantage dans trois scénarios : lorsque vous effectuez de nombreuses requêtes vers un même domaine (le multiplexage dans une seule connexion réduit les latences), lorsque réduire la visibilité des requêtes pour la protection anti-bot est important (moins de handshakes TCP depuis une seule IP semble plus naturel), et lorsque le site cible exige lui-même HTTP/2 et renvoie des erreurs ou un contenu limité aux clients sur HTTP/1.1 — cela se rencontre parfois sur des sites utilisant Cloudflare.
Pour les tâches où la vitesse et la stabilité des connexions HTTP/2 via des proxies sont importantes, les proxies de centres de données dédiés sont mieux adaptés — ils sont plus rapides que les proxies résidentiels et prennent généralement en charge correctement le tunneling des protocoles modernes.
5 erreurs typiques avec les proxies dans httpx
1. Recréer le client pour chaque requête
Un code de type async with httpx.AsyncClient(...) as client: await client.get(url) à l'intérieur d'une boucle est un anti-modèle. Chaque création de client entraîne une nouvelle connexion TCP, un nouveau handshake TLS, et une ignorance totale du pooling de connexions. Pour 1000 requêtes, la différence de vitesse entre l'approche correcte et incorrecte peut atteindre 5 à 10 fois.
2. Absence de délais d'attente ou délais d'attente par défaut trop longs
Par défaut, httpx fixe un délai d'attente de 5 secondes pour tout — connexion, lecture, écriture, pool. Lorsqu'on travaille avec des proxies instables, cela est insuffisant pour la connexion, mais trop long pour la lecture, si le proxy est simplement "bloqué". Séparez explicitement les délais d'attente :
timeout = httpx.Timeout(
connect=5.0,
read=10.0,
write=10.0,
pool=5.0
)
client = httpx.AsyncClient(proxy=PROXY_URL, timeout=timeout)
3. Ignorer les limites du pool de connexions
Lorsqu'il y a un grand nombre de tâches parallèles (gather sur 500+ coroutines sans sémaphore), httpx commence à lancer PoolTimeout, car la limite de connexions dans le pool (par défaut 100) est atteinte. Configurez les limites explicitement via httpx.Limits(max_connections=200, max_keepalive_connections=50) et utilisez toujours un sémaphore pour contrôler le parallélisme.
4. Désactiver la vérification sans comprendre les conséquences
Beaucoup copient verify=False depuis Stack Overflow pour se débarrasser des erreurs SSL lors de l'utilisation de proxies avec des certificats MITM. Cela ouvre la porte aux attaques de type "homme du milieu" et peut entraîner des fuites de données via un proxy compromis. Si le proxy nécessite un certificat spécial, ajoutez-le explicitement via verify="/path/to/cert.pem", au lieu de désactiver la vérification complètement.
5. Clients non fermés et fuites de descripteurs de fichiers
Si vous créez un pool de clients manuellement (comme dans l'exemple ci-dessus), n'oubliez pas d'appeler await client.aclose() pour chacun d'eux à la fin. Les scripts de longue durée sans fermeture explicite des connexions atteignent la limite des descripteurs de fichiers ouverts du système d'exploitation (généralement 1024 par défaut sur Linux), et le parseur échoue avec une erreur mystérieuse Too many open files après quelques heures de fonctionnement.
Quel type de proxy choisir pour le parsing
Le choix du proxy influence directement le nombre d'erreurs de la liste ci-dessus que vous rencontrerez — des proxies instables provoquent des délais d'attente et des ruptures de connexion, même si le code est écrit de manière parfaite.
| Type de proxy | Vitesse | Risque de blocage | Quand utiliser |
|---|---|---|---|
| Centre de données | Élevée | Élevé sur les sites sécurisés | API ouvertes, protection anti-bot faible |
| Résidentiels | Moyenne | Faible | Wildberries, Ozon, réseaux sociaux |
| Mobiles | Moyenne-faible | Minime | Instagram, TikTok API, protection stricte |
Pour le parsing asynchrone avec une vitesse élevée des requêtes vers l'API avec une protection modérée, les proxies de centres de données restent une option économiquement avantageuse — l'essentiel est de répartir uniformément la charge sur le pool, comme montré dans le code ci-dessus.
Checklist avant la mise en production
- Les clients httpx sont créés une fois et réutilisés, et non pour chaque requête
- Les délais d'attente sont séparés en connect/read/write/pool, et non le défaut
- Le parallélisme est limité via
asyncio.Semaphore - Les limites du pool de connexions sont configurées selon la charge réelle
- Il existe une logique de réessai avec gestion des ProxyError, ConnectTimeout, ReadTimeout
- Tous les clients sont explicitement fermés via
aclose()à la fin - verify=False n'est pas utilisé sans nécessité absolue
- HTTP/2 est activé de manière consciente et vérifié via response.http_version
Conclusion
httpx offre un puissant outil pour le travail asynchrone avec des proxies et HTTP/2, mais nécessite un réglage minutieux du pool de clients, des délais d'attente et des limites de connexions. La plupart des pannes des parseurs en production sont liées non pas à la bibliothèque, mais à des erreurs architecturales : recréation des clients, absence de limitation du parallélisme et mauvaise gestion des exceptions de proxy.
Si vous construisez un parseur pour des marketplaces ou des API de réseaux sociaux qui surveillent activement les activités suspectes, la stabilité de l'infrastructure proxy est tout aussi importante que le code. Pour de telles tâches, il vaut la peine de considérer les proxies résidentiels — elles réduisent le nombre d'erreurs ProxyError et ConnectTimeout en raison de la réputation plus élevée des adresses IP par rapport aux pools de centres de données bon marché.