La libreria httpx è diventata lo standard per le richieste HTTP asincrone in Python, ma quando si lavora con i proxy ci sono delle sfumature che rompono i parser in produzione. Analizziamo come configurare la rotazione asincrona dei proxy, abilitare HTTP/2 e perché il vostro script si interrompe ogni 50-100 richieste.
Perché httpx e non requests o aiohttp
requests è sincrono e non supporta HTTP/2 di default. aiohttp è asincrono, ma la configurazione dei proxy è meno amichevole e il supporto per HTTP/2 è completamente assente. httpx risolve entrambi i problemi: un client sincrono e asincrono con un'unica API, supporto nativo per HTTP/2 tramite l'estensione h2, e un lavoro flessibile con i proxy a livello di trasporto.
Per le attività di parsing di marketplace (Wildberries, Ozon, Avito), raccolta dati da API di social media o monitoraggio dei prezzi dei concorrenti, l'asincronicità è critica: offre un aumento esponenziale della velocità rispetto alle richieste sequenziali. Ma senza una corretta rotazione dei proxy e gestione delle connessioni, l'asincronicità diventa una fonte di nuovi problemi: esaurimento del pool di connessioni, coroutine bloccate, perdite di descrittori di file.
Un secondo punto importante è che HTTP/2 può multiplexare più richieste in una singola connessione TCP, riducendo il carico sul server di destinazione e aiutando a bypassare alcuni sistemi anti-bot ingenui che monitorano il numero di handshake TCP da un singolo IP.
Configurazione di base dei proxy in httpx
A partire dalla versione httpx 0.26+, la sintassi per la configurazione dei proxy è cambiata: invece di un dizionario proxies, si utilizza il parametro proxy. Questo è una fonte comune di confusione se si guardano vecchi tutorial.
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"))
Se è necessario separare i proxy per il traffico HTTP e HTTPS o configurare proxy diversi per domini diversi, utilizzare httpx.Client(mounts=...) — i trasporti vengono montati in base al modello 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")
Per l'autenticazione tramite nome utente e password, il formato http://user:pass@host:port funziona in modo affidabile per i proxy HTTP. Per SOCKS5 sarà necessario installare anche httpx[socks], altrimenti si riceverà un ImportError a runtime, e non durante la scrittura del codice — questa è una trappola in cui cadono in molti.
Rotazione asincrona dei proxy: codice funzionante
L'errore principale nella rotazione dei proxy è creare un nuovo AsyncClient per ogni richiesta. Questo uccide le prestazioni, perché il client mantiene un pool di connessioni, e la sua ricreazione annulla tutti i vantaggi delle connessioni keep-alive. L'approccio corretto è creare un pool di client in anticipo, uno per proxy, e distribuire i compiti tra di essi.
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
# per una scelta casuale: 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) # limitiamo il parallelismo
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())
Questo pattern risolve tre problemi contemporaneamente: riutilizza le connessioni TCP tramite keep-alive, distribuisce il carico tra i proxy in modo round-robin e limita il parallelismo tramite un semaforo, per non sovraccaricare il server di destinazione e non provocare un ban per rate-limit.
Per i parser in produzione che lavorano con Wildberries, Ozon o Instagram API, è importante che il pool di proxy sia sufficientemente grande e variegato geograficamente — altrimenti la rotazione all'interno di una sola subnet non salverà dalla blocco. Qui sono adatti i proxy residenziali, poiché hanno IP di utenti reali e rientrano meno nei filtri dei sistemi anti-bot che analizzano ASN e la reputazione dell'intervallo.
HTTP/2 in httpx: perché e come abilitare
Di default httpx utilizza HTTP/1.1. Per abilitare HTTP/2, è necessario installare l'estensione e passare esplicitamente il flag:
# 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) # ci aspettiamo "HTTP/2"
Un punto importante: non tutti i proxy supportano correttamente il tunneling HTTP/2. Alcuni proxy economici dei data center funzionano solo come proxy HTTP/1.1 anche se il server finale supporta HTTP/2 — in tal caso httpx passerà silenziosamente a HTTP/1.1 tramite un tunnel CONNECT, e non sempre lo noterete senza un controllo esplicito response.http_version.
HTTP/2 offre un reale vantaggio in tre scenari: quando si effettuano molte richieste a un singolo dominio (il multiplexing in una sola connessione riduce le latenze), quando è importante ridurre la visibilità delle richieste per la protezione anti-bot (meno handshake TCP da un singolo IP appare più naturale), e quando il sito di destinazione richiede HTTP/2 e restituisce errori o contenuti ridotti ai client su HTTP/1.1 — questo si verifica a volte su siti su Cloudflare.
Per le attività in cui la velocità e la stabilità delle connessioni HTTP/2 tramite proxy sono importanti, i proxy dei data center dedicati sono più adatti — sono più veloci dei residenziali e di solito supportano correttamente il tunneling dei protocolli moderni.
5 errori tipici con i proxy in httpx
1. Ricreare il client per ogni richiesta
Codice del tipo async with httpx.AsyncClient(...) as client: await client.get(url) all'interno di un ciclo — questo è un antipattern. Ogni creazione di un client implica una nuova connessione TCP, un nuovo handshake TLS, ignorando completamente il connection pooling. Su 1000 richieste, la differenza di velocità tra l'approccio corretto e quello errato può raggiungere 5-10 volte.
2. Mancanza di timeout o timeout predefiniti troppo lunghi
Di default httpx imposta un timeout di 5 secondi per tutto — connect, read, write, pool. Quando si lavora tramite proxy instabili, questo è insufficiente per il connect, ma troppo per il read, se il proxy è semplicemente "bloccato". Suddividete i timeout esplicitamente:
timeout = httpx.Timeout(
connect=5.0,
read=10.0,
write=10.0,
pool=5.0
)
client = httpx.AsyncClient(proxy=PROXY_URL, timeout=timeout)
3. Ignorare i limiti del pool di connessioni
Con un numero elevato di attività parallele (gather su 500+ coroutine senza semaforo) httpx inizia a lanciare PoolTimeout, perché il limite delle connessioni nel pool (di default 100) è esaurito. Impostate i limiti esplicitamente tramite httpx.Limits(max_connections=200, max_keepalive_connections=50) e utilizzate sempre un semaforo per controllare il parallelismo.
4. Disabilitare la verifica senza comprendere le conseguenze
Molti copiano verify=False da Stack Overflow per eliminare gli errori SSL quando si lavora tramite proxy con certificati MITM. Questo apre la strada ad attacchi "man-in-the-middle" e può portare a perdite di dati tramite proxy compromessi. Se il proxy richiede un certificato speciale, aggiungetelo esplicitamente tramite verify="/path/to/cert.pem", invece di disabilitare completamente la verifica.
5. Client non chiusi e perdite di descrittori di file
Se create un pool di client manualmente (come nell'esempio sopra), non dimenticate di chiamare await client.aclose() per ciascuno di essi al termine del lavoro. Script a lungo termine senza una chiusura esplicita delle connessioni raggiungono il limite dei descrittori di file aperti del sistema operativo (di solito 1024 su Linux di default), e il parser si interrompe con un errore misterioso Too many open files dopo alcune ore di lavoro.
Quale tipo di proxy scegliere per il parsing
La scelta del proxy influisce direttamente su quante delle errori elencati sopra vedrete — proxy instabili provocano timeout e interruzioni delle connessioni, anche se il codice è scritto perfettamente.
| Tipo di proxy | Velocità | Rischio di blocco | Quando utilizzare |
|---|---|---|---|
| Data center | Alta | Alto sui siti protetti | API aperte, protezione anti-bot debole |
| Residenziali | Media | Basso | Wildberries, Ozon, social media |
| Mobili | Media-bassa | Minimo | Instagram, TikTok API, protezione severa |
Per il parsing asincrono con alta velocità di richieste a API con protezione moderata, i proxy dei data center rimangono un'opzione economicamente vantaggiosa — l'importante è distribuire uniformemente il carico sul pool, come mostrato nel codice sopra.
Checklist prima della produzione
- I client httpx vengono creati una sola volta e riutilizzati, non per ogni richiesta
- I timeout sono separati in connect/read/write/pool, non si utilizza il predefinito
- Il parallelismo è limitato tramite
asyncio.Semaphore - I limiti del pool di connessioni sono impostati in base al carico reale
- Esiste una logica di retry con gestione di ProxyError, ConnectTimeout, ReadTimeout
- Tutti i client vengono chiusi esplicitamente tramite
aclose()al termine - verify=False non viene utilizzato senza estrema necessità
- HTTP/2 è abilitato consapevolmente e verificato tramite response.http_version
Conclusione
httpx offre un potente strumento per il lavoro asincrono con proxy e HTTP/2, ma richiede una configurazione attenta del pool di client, dei timeout e dei limiti delle connessioni. La maggior parte dei crash dei parser in produzione è legata non alla libreria, ma a errori architetturali: ricreazione dei client, assenza di limitazione del parallelismo e gestione errata delle eccezioni dei proxy.
Se state costruendo un parser per marketplace o API di social media che monitorano attivamente l'attività sospetta, la stabilità dell'infrastruttura proxy è importante quanto il codice. Per tali compiti, è consigliabile considerare i proxy residenziali — riducono la percentuale di errori ProxyError e ConnectTimeout grazie a una reputazione degli indirizzi IP più alta rispetto ai pool di data center economici.