Rotation de proxies asynchrone en Python : httpx et aiohttp (Guide 2026)

Publié le 12 juin 2026 · ≈10 min de lecture

Le scraping asynchrone est l'endroit où les configurations de proxy s'effondrent silencieusement. Avec requests, vous faites tourner une IP par appel et vous passez à la suite ; avec asyncio, vous avez soudain 200 requêtes en vol, ayant toutes besoin de leur propre exit, de leur propre affinité de session et de leur propre réessai quand une cible renvoie un 403. Faites-le mal et soit vous martelez une IP jusqu'au bannissement, soit vous détruisez chaque session de connexion en faisant tourner l'IP en plein milieu du flux.

Ce guide montre les patterns qui tiennent vraiment la route en concurrence : rotation par requête dans httpx et aiohttp, un pool de workers borné, des sessions sticky pour les flux avec état et des réessais conscients des blocages — avec du code fonctionnel que vous pouvez coller.

Pourquoi l'async change le problème du proxy

Un scraper synchrone touche un proxy à la fois, donc « faire tourner à chaque requête » est trivialement correct. L'async brise trois hypothèses à la fois :

httpx : rotation de proxy par requête

httpx.AsyncClient lie un proxy par client, donc pour faire tourner vous choisissez le proxy par requête et routez à travers un petit pool de clients indexés par exit :

import asyncio, itertools, httpx

PROXIES = [
    "socks5h://USERNAME:[email protected]:913",
    "socks5h://USERNAME:[email protected]:913",
    "socks5h://USERNAME:[email protected]:913",
]
pool = itertools.cycle(PROXIES)

# One reusable client per exit (connection pooling stays intact)
clients = {p: httpx.AsyncClient(proxy=p, timeout=30) for p in PROXIES}

async def fetch(url):
    proxy = next(pool)
    r = await clients[proxy].get(url)
    return r.status_code, r.text

async def main(urls):
    results = await asyncio.gather(*(fetch(u) for u in urls))
    for client in clients.values():
        await client.aclose()
    return results

Réutiliser un client par exit maintient en vie HTTP/2 et le pooling de connexions au lieu de payer un nouveau handshake à chaque requête. Notez que httpx>=0.28 a renommé proxies= en proxy= sur le client.

aiohttp : rotation via la requête

aiohttp est plus simple ici — une seule ClientSession prend un argument proxy= par requête, donc vous faites tourner en ligne :

import asyncio, itertools, aiohttp

pool = itertools.cycle(PROXIES)  # http:// or socks5h:// (needs aiohttp-socks for SOCKS)

async def fetch(session, url):
    proxy = next(pool)
    async with session.get(url, proxy=proxy, timeout=aiohttp.ClientTimeout(total=30)) as r:
        return r.status, await r.text()

async def main(urls):
    async with aiohttp.ClientSession() as session:
        return await asyncio.gather(*(fetch(session, u) for u in urls))

aiohttp parle nativement les proxies HTTP ; pour socks5h:// ajoutez aiohttp-socks et passez un ProxyConnector.

Plafonnez la concurrence avec un sémaphore

Un gather sans limites est le moyen le plus rapide de faire marquer toutes les IP de votre pool d'un coup. Bornez-le pour que chaque exit porte un nombre de requêtes parallèles plausible pour un humain :

sem = asyncio.Semaphore(10)   # at most 10 in flight

async def fetch_capped(session, url):
    async with sem:
        return await fetch(session, url)

Règle empirique : maintenez la concurrence à hauteur ou en dessous du nombre d'exits distincts dont vous disposez, pour ne pas empiler de nombreuses requêtes parallèles sur une seule IP résidentielle.

Sessions sticky pour les flux avec état

La rotation sert à entrer ; les sessions sticky servent à rester. Une connexion, un panier, tout ce qui est lié aux cookies doit tenir un exit pendant toute la séquence. Épinglez le proxy par tâche au lieu de par requête :

async def run_account(account, proxy):
    # Same exit IP for every step of this account's flow
    async with httpx.AsyncClient(proxy=proxy, timeout=30) as client:
        await client.post("https://target.site/login", data=account.creds)
        await client.get("https://target.site/dashboard")
        await client.post("https://target.site/cart", json=account.order)

async def main(accounts):
    # One sticky exit per account, accounts run concurrently
    await asyncio.gather(*(run_account(a, p)
                           for a, p in zip(accounts, itertools.cycle(PROXIES))))

Utilisez un fournisseur qui prend en charge les sessions résidentielles sticky pour que le même exit soit tenu pendant toute la durée de vie de la tâche, au lieu d'être silencieusement remplacé sous vos pieds.

Réessais conscients des blocages

En concurrence, vous ne pouvez pas faire confiance à un 200 — les systèmes antibot renvoient un 200 avec un corps de défi. Détectez les blocages et réessayez la seule tâche échouée sur un exit neuf :

BLOCK_MARKERS = ("just a moment", "/cdn-cgi/challenge-platform",
                 "datadome", "px-captcha", "access denied")

def looks_blocked(status, text):
    return status in (403, 429, 503) or any(m in text[:4000].lower() for m in BLOCK_MARKERS)

async def fetch_retry(url, attempts=4):
    for _ in range(attempts):
        proxy = next(pool)
        async with httpx.AsyncClient(proxy=proxy, timeout=30) as c:
            r = await c.get(url)
            if not looks_blocked(r.status_code, r.text):
                return r
        await asyncio.sleep(0.5)
    raise RuntimeError(f"blocked after {attempts} exits: {url}")

L'empreinte doit encore correspondre

Un hic que l'async ne résout pas : httpx et aiohttp reposent tous deux sur la pile TLS de Python, donc ils envoient un JA3 qu'aucun vrai navigateur ne produit. Un pool de rotation parfait sur une IP résidentielle propre est tout de même identifié comme automatisation lors du handshake. Associez la rotation asynchrone à l'usurpation de TLS — voir Contourner le fingerprinting TLS avec curl_cffi, et vérifiez le JA3 + ASN de votre exit sur www.jibaoproxy.com/tools/fingerprint.html.

Liste de contrôle

Pour tous les produits IP · un immense pool de nœuds disponibles à tout moment

Inscrivez-vous maintenant et obtenez jusqu'à 100 % de remboursement sur votre recharge

Les nouveaux utilisateurs reçoivent 500 Mo à l'inscription, plus un bonus sur la première recharge. Offre à durée limitée.