O aiohttp é o cliente a escolher quando um scraper necessita de centenas de pedidos em curso e se pretende ter controlo total sobre o ciclo de eventos. Encaminhá-lo através de um proxy requer apenas alguns argumentos-chave, mas os valores predefinidos relativos à autenticação, limites de ligação e tempos de espera são diferentes dos do requests e do httpx, e é aí que a maioria das configurações de proxy do aiohttp falha. No final deste guia, terá um ClientSession que se autentica num gateway residencial rotativo, executa pedidos paralelos limitados, atinge o tempo limite de forma previsível e reporta erros de proxy por tipo.
Os exemplos destinam-se ao aiohttp 3.14 no Python 3.11 ou mais recente. Nos casos em que o comportamento mudou numa versão recente, a versão é indicada.
Configurar um proxy aiohttp na sessão
A palavra-chave proxy é aceite tanto por pedido como ao nível da sessão. A configuração «por pedido» corresponde à API original e continua a funcionar; a configuração ao nível da sessão é a que se deve utilizar para um scraper, pois elimina a possibilidade de uma única chamada escapar ao proxy.
import asyncio
import aiohttp
PROXY = "http://USERNAME:PASSWORD@premium-residential.geonode.com:9000"
async def main() -> None:
async with aiohttp.ClientSession(proxy=PROXY) as session:
async with session.get("https://api.ipify.org?format=json") as resp:
print(resp.status, await resp.json())
asyncio.run(main())
A porta 9000 é o gateway rotativo HTTP(S) do Geonode, pelo que cada nova ligação é estabelecida a partir de um endereço IP residencial diferente. Uma chamada individual ainda pode substituir o valor predefinido com o seu próprio argumento proxy=, o que é útil quando uma solicitação precisa de ser direta, por exemplo, para um ponto final de métricas interno.
Formato por pedido
async with aiohttp.ClientSession() as session:
async with session.get("https://api.ipify.org?format=json", proxy=PROXY) as resp:
print(await resp.json())
Utilize isto quando o proxy varia de pedido para pedido, como quando um gestor de pool atribui uma sessão fixa diferente por tarefa.
Autenticação: credenciais de URL ou proxy_headers
Existem duas formas suportadas de enviar credenciais de proxy e uma forma obsoleta que ainda poderá encontrar em tutoriais mais antigos.
| Método | Exemplo | Estado |
|---|
| Credenciais na URL do proxy | proxy="http://user:pass@host:9000" | Suportado |
proxy_headers com encode_basic_auth | proxy_headers={"Proxy-Authorization": aiohttp.encode_basic_auth("user", "pass")} | Suportado, recomendado |
proxy_auth=aiohttp.BasicAuth("user", "pass") | | Obsoleta desde a versão 3.14, removida na versão 4.0 |
A forma de URL é a mais curta. A forma de cabeçalho mantém o segredo fora da URL, o que é importante se as URLs do proxy acabarem por aparecer em registos ou mensagens de exceção. aiohttp.encode_basic_auth devolve a cadeia de caracteres Basic ... já pronta:
import asyncio
import os
import aiohttp
PROXY = "http://premium-residential.geonode.com:9000"
async def main() -> None:
proxy_headers = {
"Proxy-Authorization": aiohttp.encode_basic_auth(
os.environ["GEONODE_USER"], os.environ["GEONODE_PASS"]
)
}
async with aiohttp.ClientSession(proxy=PROXY, proxy_headers=proxy_headers) as session:
async with session.get("https://api.ipify.org?format=json") as resp:
print(await resp.json())
asyncio.run(main())
Se a sua base de código ainda utiliza proxy_auth=aiohttp.BasicAuth(...), esta funciona na versão 3.14 com um DeprecationWarning. Faça a migração agora: o parâmetro e a classe serão removidos no aiohttp 4.0, e a substituição consiste numa alteração de uma única linha.
Pedidos paralelos com asyncio.gather e um Semaphore
A razão para utilizar o aiohttp é a concorrência, e a razão pela qual a concorrência causa falhas nos proxies é que, por predefinição, não há nada que a limite. Um asyncio.gather com mais de mil corrotinas tentará abrir mil ligações; o conector limita esse número a 100, e o restante fica em fila no aiohttp, o que é melhor do que nada, mas não lhe dá qualquer controlo sobre o ritmo. Um Semaphore torna o limite explícito e permite ajustá-lo independentemente do conector.
import asyncio
import aiohttp
PROXY = "http://USERNAME:PASSWORD@premium-residential.geonode.com:9000"
CONCURRENCY = 20
async def fetch(session: aiohttp.ClientSession, sem: asyncio.Semaphore, url: str) -> tuple[str, int]:
async with sem:
async with session.get(url) as resp:
await resp.read()
return url, resp.status
async def main() -> None:
urls = [f"https://httpbin.org/get?i={i}" for i in range(100)]
sem = asyncio.Semaphore(CONCURRENCY)
connector = aiohttp.TCPConnector(limit=CONCURRENCY, ttl_dns_cache=300)
timeout = aiohttp.ClientTimeout(total=30, connect=10, sock_read=20)
async with aiohttp.ClientSession(
proxy=PROXY, connector=connector, timeout=timeout
) as session:
results = await asyncio.gather(
*(fetch(session, sem, u) for u in urls), return_exceptions=True
)
ok = sum(1 for r in results if not isinstance(r, Exception))
print(f"{ok}/{len(urls)} succeeded")
asyncio.run(main())
O return_exceptions=True evita que um pedido falhado cancele o lote. Faça corresponder o semáforo ao limite do conector, para que nenhum dos lados coloque silenciosamente na fila tarefas que o outro não consiga processar.
TCPConnector: limit e limit_per_host
O conector é responsável pelo conjunto de sockets. Dois parâmetros são importantes para o tráfego do proxy aiohttp.
limit é o número total de ligações simultâneas, com o valor predefinido de 100. O limit_per_host limita as ligações a um triplo (host, port, is_ssl); o valor predefinido é 0, o que significa ilimitado. Através de um proxy, todos os pedidos para todos os destinos partilham o anfitrião e a porta do proxy, pelo que o limite por anfitrião se aplica ao gateway e não aos sites de destino. Para uma carga de trabalho exclusivamente de proxy, defina limit para a simultaneidade pretendida e deixe limit_per_host em 0; defina limit_per_host apenas quando a mesma sessão também comunicar diretamente com as origens e pretender protegê-las.
connector = aiohttp.TCPConnector(limit=30, limit_per_host=10, ttl_dns_cache=300)
O ttl_dns_cache=300 mantém a resposta DNS do gateway durante cinco minutos, em vez dos dez segundos predefinidos, o que elimina uma ida e volta do resolvedor na maioria das novas ligações. A sessão assume a posse do conector e encerra-o ao sair; defina o connector_owner=False apenas se pretender deliberadamente partilhar um conector entre sessões.
ClientTimeout em vez do valor predefinido de 5 minutos
O tempo limite total predefinido do aiohttp é de 300 segundos. Esse valor é generoso para pedidos diretos, mas demasiado longo para o tráfego via proxy, onde uma saída residencial bloqueada deve falhar rapidamente para que se possa tentar novamente com outro IP.
timeout = aiohttp.ClientTimeout(total=30, connect=10, sock_read=20)
O total limita toda a solicitação, incluindo redirecionamentos e o download do corpo da resposta. O connect limita a aquisição de uma ligação a partir do conjunto, além do handshake TCP e do proxy. O sock_read limita o intervalo entre bytes consecutivos, o que deteta alvos que aceitam a ligação e, em seguida, enviam dados aos poucos. Passe o objeto para a sessão como valor predefinido ou para uma solicitação única para o substituir. A expiração gera o asyncio.TimeoutError, discutido abaixo.
trust_env e proxies de ambiente
Ao contrário do requests, o aiohttp ignora o HTTP_PROXY, o HTTPS_PROXY e o NO_PROXY, a menos que se opte por ativá-los com o trust_env=True. Quando ativado, utiliza o urllib.request.getproxies(), respeita o no_proxy e também lê as credenciais de proxy do ~/.netrc, caso estejam presentes.
async with aiohttp.ClientSession(trust_env=True) as session:
async with session.get("https://api.ipify.org?format=json") as resp:
print(await resp.json())
Para um scraper que tenha de utilizar sempre um gateway específico, desative o trust_env e defina explicitamente o proxy=. Dessa forma, um proxy corporativo num executor CI não pode desviar o encaminhamento, e o no_proxy não pode isentar discretamente um alvo.
SOCKS5 com aiohttp-socks
O aiohttp não tem suporte integrado para SOCKS, pelo que um proxy aiohttp através do SOCKS5 necessita de um conector de terceiros. O pacote aiohttp-socks fornece um ProxyConnector que substitui o TCPConnector e gere o handshake SOCKS. Instale-o com o pip install aiohttp-socks. A porta rotativa do SOCKS5 do Geonode é 11000:
import asyncio
import aiohttp
from aiohttp_socks import ProxyConnector
SOCKS_URL = "socks5://USERNAME:PASSWORD@premium-residential.geonode.com:11000"
async def main() -> None:
connector = ProxyConnector.from_url(SOCKS_URL)
async with aiohttp.ClientSession(connector=connector) as session:
async with session.get("https://api.ipify.org?format=json") as resp:
print(await resp.json())
asyncio.run(main())
Não combine o ProxyConnector com o proxy= na sessão; o conector já encaminha tudo. O ProxyConnector aceita as mesmas palavras-chave limit e limit_per_host que o TCPConnector.
A limitação do TLS-in-TLS
O aiohttp comunica com proxies HTTP e estabelece túneis para os destinos https:// através deles utilizando o comando CONNECT. O que não faz de forma fiável é ligar-se ao próprio proxy através de TLS, ou seja, a um URL de proxy do tipo https://, porque o TLS-in-TLS depende do suporte ao transporte asyncio, que tem variado ao longo das versões do Python. A documentação é explícita ao indicar que isto não deve ser dado como certo. Na prática, isto não tem importância para o Geonode: o gateway é acedido através de HTTP simples nas portas 9000/10000 e o seu tráfego https:// continua a ser encriptado de ponta a ponta dentro do túnel CONNECT; por isso, mantenha o esquema de URL do proxy como http://.
Sessões fixas para fluxos com várias etapas
A porta rotativa atribui um novo endereço IP por ligação. Para fluxos de início de sessão ou listagens paginadas, é necessário manter o mesmo endereço IP durante alguns minutos, o que o Geonode disponibiliza na porta fixa HTTP(S) 10000 através do nome de utilizador:
import secrets
import aiohttp
def sticky_proxy(country: str = "us", lifetime: int = 600) -> str:
session_id = secrets.token_hex(4) # 8 alphanumeric characters
user = f"USERNAME-country-{country}-session-{session_id}-lifetime-{lifetime}"
return f"http://{user}:PASSWORD@premium-residential.geonode.com:10000"
session = aiohttp.ClientSession(proxy=sticky_proxy())
O tempo de vida varia entre 180 e 86 400 segundos. A segmentação por estado e cidade não pode ser combinada, e o asn requer a indicação de um país. A geração de um novo ID de sessão permite a rotação sob demanda.
Diagnóstico: o que significa cada exceção
| Exceção | Significado | Solução típica |
|---|
aiohttp.ClientProxyConnectionError | Não foi possível abrir uma ligação TCP ao anfitrião proxy | Verifique o nome do anfitrião, a porta e a firewall; aumente o tempo de espera da connect |
aiohttp.ClientHttpProxyError com .status == 407 | O proxy rejeitou as credenciais durante o CONNECT | Corrija o nome de utilizador ou a palavra-passe; verifique a sintaxe da opção do nome de utilizador |
aiohttp.ClientHttpProxyError com 502/503 | O gateway não conseguiu estabelecer o túnel | Repita a tentativa com um novo ID de sessão; confirme se a porta corresponde ao protocolo |
asyncio.TimeoutError | Um campo do ClientTimeout expirou | Tente novamente com um IP novo; ajuste o sock_read |
aiohttp.ClientConnectorError | Falha genérica de ligação (DNS, recusada) | Verifique o .os_error |
O ClientProxyConnectionError é uma subclasse do ClientConnectorError e o ClientHttpProxyError é uma subclasse do ClientResponseError; por isso, detete primeiro os casos específicos.
import asyncio
import aiohttp
PROXY = "http://USERNAME:PASSWORD@premium-residential.geonode.com:9000"
async def fetch_ip(session: aiohttp.ClientSession) -> str | None:
try:
async with session.get("https://api.ipify.org?format=json") as resp:
resp.raise_for_status()
return (await resp.json())["ip"]
except aiohttp.ClientHttpProxyError as e:
if e.status == 407:
print("proxy auth failed: check credentials")
else:
print("proxy returned", e.status)
except aiohttp.ClientProxyConnectionError as e:
print("cannot reach proxy:", e)
except asyncio.TimeoutError:
print("timed out through proxy")
return None
async def main() -> None:
timeout = aiohttp.ClientTimeout(total=30, connect=10, sock_read=20)
async with aiohttp.ClientSession(proxy=PROXY, timeout=timeout) as session:
print(await fetch_ip(session))
await asyncio.sleep(0.25) # let SSL transports close before the loop stops
asyncio.run(main())
Note-se que, para um alvo http:// simples, o código 407 do proxy surge como uma resposta normal e não como uma exceção; por isso, verifique também o resp.status nesse caso.
Encerramento suave
Quando uma sessão é encerrada, o asyncio pode ainda estar a desativar os transportes SSL subjacentes. Se o ciclo de eventos parar primeiro, obtém-se o código de erro ResourceWarning: unclosed transport. A correção documentada consiste numa breve pausa após o bloco async with da sessão: await asyncio.sleep(0) para sessões HTTP simples e await asyncio.sleep(0.25) quando a sessão efetuou pedidos HTTPS, o que, através de um túnel CONNECT, acontece quase sempre. O exemplo acima inclui essa correção.
Erros comuns
Criar uma sessão por pedido
O ClientSession contém o conjunto de ligações e o conjunto de cookies. Abrir uma sessão por pedido anula a funcionalidade «keep-alive» para o gateway, multiplica os handshakes do proxy e aciona o aviso de encerramento em cada chamada. Crie uma sessão por trabalhador e passe-a de um para o outro.
Esquecer-se de que o proxy é o único «host»
Com o limit_per_host definido para um valor baixo, cada pedido encaminhado pelo proxy conta para a única entrada (host, port) do gateway; assim, um limite de 5 por anfitrião restringe todo o seu scraper a 5 ligações. Defina o limit em vez disso, ou deixe o limit_per_host em 0.
Confiar no tempo de espera predefinido
Cinco minutos é tempo suficiente para que uma saída residencial bloqueada ocupe um slot de semáforo durante todo o intervalo. Passe sempre um ClientTimeout com valores para connect e sock_read.
Continuar a passar o proxy_auth
Atualmente, funciona com um aviso, mas deixa de funcionar na versão 4.0. Substitua-o pelo proxy_headers={"Proxy-Authorization": aiohttp.encode_basic_auth(user, pw)} ou coloque as credenciais no URL do proxy.
Próximo passo
Com a sessão acima, já autenticou, vinculou e tornou observável o tráfego do proxy aiohttp; o que resta é o conjunto de proxies por onde este passa. Os proxies residenciais Geonode servem os endpoints aqui utilizados: 2,5 milhões de IPs online, 30 a 50 milhões de IPs únicos em rotação mensal em mais de 195 países, com segmentação por país, estado, cidade e ASN através do nome de utilizador e sessões persistentes até 24 horas via HTTP, HTTPS e SOCKS5. Os preços começam em 0,27 $/GB em volume, o plano básico é de 10 GB por 7,92 $/mês e um período de teste de 3 dias custa 5 $, sem contrato. Os casos de utilização de web scraping e análise de preços mostram como as equipas combinam esta configuração do conector com lógicas de repetição de tentativas e agendamento.
Perguntas frequentes
Como configuro um proxy aiohttp para toda uma sessão?
Passe proxy= ao construtor de ClientSession: aiohttp.ClientSession(proxy="http://user:pass@host:port"). Todos os pedidos dessa sessão utilizam-no, a menos que a chamada substitua proxy= por outro valor. O proxy ao nível da sessão está disponível desde o aiohttp 3.11; em versões mais antigas, deve passar proxy= em cada chamada a get() ou post().
Por que razão o proxy_auth está obsoleto no aiohttp e o que o substitui?
Desde o aiohttp 3.14 que o parâmetro proxy_auth e a classe aiohttp.BasicAuth estão obsoletos e com remoção prevista para a versão 4.0. O substituto é um cabeçalho simples: proxy_headers={"Proxy-Authorization": aiohttp.encode_basic_auth("user", "pass")}. Este realiza a mesma codificação Base64 sem uma classe auxiliar e funciona tanto por pedido como ao nível da sessão.
O aiohttp suporta proxies HTTPS ou SOCKS5?
O aiohttp comunica nativamente com proxies HTTP e utiliza CONNECT para encaminhar destinos https:// através deles. A ligação ao próprio proxy através de TLS (TLS-in-TLS) depende do suporte a asyncio na sua compilação do Python e não deve ser considerada fiável. O SOCKS4 e o SOCKS5 requerem o pacote de terceiros aiohttp-socks e o seu ProxyConnector.
Quantas solicitações simultâneas posso executar através de um proxy aiohttp?
O TCPConnector permite 100 ligações simultâneas por predefinição, sem limite por anfitrião. Através de um proxy, todas as solicitações partilham o mesmo anfitrião e porta; por isso, defina explicitamente o limit, adicione o limit_per_host se também efetuar chamadas diretas e envolva as corrotinas de solicitação num asyncio.Semaphore para que as tarefas em fila não abram todas as ligações de rede ao mesmo tempo.
Por que razão a minha solicitação aiohttp através de um proxy fica bloqueada em vez de falhar?
O ClientSession tem um tempo limite total de 5 minutos por predefinição, o que parece um bloqueio quando uma saída residencial fica paralisada. Passe o aiohttp.ClientTimeout(total=30, connect=10, sock_read=20) para a sessão ou para a solicitação, para que os bloqueios sejam rapidamente identificados como asyncio.TimeoutError; em seguida, tente novamente com um ID de sessão novo, em vez de esperar.
Quais são os limites práticos do encaminhamento de scrapers através de proxies residenciais?
Um proxy apenas altera o IP de origem. Não concede permissão para ignorar o ficheiro robots.txt, os termos do site ou os limites de taxa, nem torna a recolha de dados pessoais legal. Mantenha a simultaneidade dentro de limites razoáveis, reduza a frequência em caso de erros 429 e lembre-se de que as sessões «sticky» do Geonode duram, no máximo, 24 horas antes de o IP ser libertado.