O scraping assíncrono é onde as configurações de proxy silenciosamente desmoronam. Com requests você rotaciona um IP por chamada e segue em frente; com asyncio você de repente tem 200 requisições em voo, todas precisando do seu próprio exit, da sua própria afinidade de sessão e da sua própria retentativa quando um alvo lança um 403. Erre nisso e você ou martela um único IP até o banimento ou destrói toda sessão de login ao rotacionar no meio do fluxo.
Este guia mostra os padrões que realmente se sustentam sob concorrência: rotação por requisição em httpx e aiohttp, um pool de workers acotado, sessões sticky para fluxos com estado e retentativas conscientes de bloqueio — com código funcional que você pode colar.
Um scraper síncrono toca em um proxy por vez, então "rotacionar a cada requisição" é trivialmente correto. O assíncrono quebra três premissas de uma vez:
O httpx.AsyncClient vincula um proxy por cliente, então, para rotacionar, você escolhe o proxy por requisição e roteia através de um pequeno pool de clientes indexado por 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
Reutilizar um cliente por exit mantém o HTTP/2 e o pooling de conexão vivos em vez de pagar um novo handshake a cada requisição. Note que httpx>=0.28 renomeou proxies= para proxy= no cliente.
O aiohttp é mais simples aqui — uma única ClientSession aceita um argumento proxy= por requisição, então você rotaciona inline:
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))
O aiohttp fala proxies HTTP nativamente; para socks5h:// adicione o aiohttp-socks e passe um ProxyConnector.
Um gather sem limites é a forma mais rápida de ter todos os IPs do seu pool sinalizados de uma vez. Limite-o para que cada exit carregue um número de requisições paralelas plausível para um humano:
sem = asyncio.Semaphore(10) # at most 10 in flight
async def fetch_capped(session, url):
async with sem:
return await fetch(session, url)
Regra prática: mantenha a concorrência em ou abaixo do número de exits distintos que você tem, para não estar empilhando muitas requisições paralelas em um único IP residencial.
A rotação serve para entrar; as sessões sticky servem para permanecer dentro. Um login, um carrinho, qualquer coisa vinculada a cookies deve manter um exit para toda a sequência. Fixe o proxy por tarefa em vez de por requisição:
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))))
Use um provedor que suporte sessões residenciais sticky, para que o mesmo exit seja mantido durante toda a vida da tarefa, e não silenciosamente rotacionado por baixo dos panos.
Sob concorrência, você não pode confiar em um 200 — sistemas anti-bot retornam um 200 com um corpo de desafio. Detecte bloqueios e tente novamente a única tarefa que falhou em um exit novo:
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}")
Um detalhe que o assíncrono não resolve: httpx e aiohttp ambos assentam sobre a stack TLS do Python, então enviam um JA3 que nenhum navegador real produz. Um pool de rotação perfeito em um IP residencial limpo ainda é identificado como automação pela fingerprint no handshake. Combine a rotação assíncrona com a impersonação de TLS — veja Como Burlar o TLS Fingerprinting com curl_cffi, e verifique o JA3 + ASN do seu exit em www.jibaoproxy.com/tools/fingerprint.html.
Novos usuários recebem 500MB ao se cadastrar, mais um bônus na primeira recarga. Oferta por tempo limitado.