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.
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.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 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.
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.
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.
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}")
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.
Les nouveaux utilisateurs reçoivent 500 Mo à l'inscription, plus un bonus sur la première recharge. Offre à durée limitée.