Scraping Asincrono in Python con httpx, aiohttp e Proxy

L'asincronia trasforma uno scraper lento in uno veloce — e uno veloce in uno bloccato, a meno che non si impostino correttamente proxy, semafori e timeout. Ecco il modello completo per httpx e aiohttp, con codice.

Se il tuo scraper trascorre la maggior parte del tempo di esecuzione in attesa della rete, l'asincronia è il più grande guadagno di velocità disponibile — e i proxy sono ciò che impedisce che tale velocità ti faccia bannare. Questa guida copre lo scraping asincrono in Python con httpx e aiohttp più proxy dall'inizio alla fine: come ogni libreria imposta un proxy, come limitare la concorrenza con un semaforo, come impostare timeout che effettivamente scattano, e come ruotare gli IP senza distruggere le sessioni. Tutto con codice eseguibile e credenziali segnaposto da sostituire con le tue.

Perché asincrono, e dove si inseriscono i proxy

Il requests standard è bloccante: ogni chiamata attende la risposta prima che inizi la successiva. Recupera 500 pagine e paghi 500 viaggi di andata e ritorno consecutivi. L'asincronia le recupera contemporaneamente su un unico ciclo di eventi, quindi il tempo totale si riduce verso la richiesta singola più lenta invece della somma. L'inghippo: un'esplosione di richieste simultanee da un unico IP è esattamente la firma che i sistemi anti-bot cercano. La soluzione non è rallentare fino a fermarsi — è distribuire il traffico attraverso un pool di proxy rotanti e limitare deliberatamente con un semaforo.

Imposta un proxy in httpx (asincrono)

httpx è la scelta pragmatica di default perché un modello di client fa sia sync che async, e supporta HTTP/2. Nota l'API moderna: è un argomento singolare proxy= sul client, non il vecchio dizionario proxies= — una fonte comune di confusione "perché il mio proxy viene ignorato" dopo un aggiornamento. Le credenziali vanno direttamente nell'URL del proxy.

import asyncio, httpx

PROXY = "http://USER:PASS@gate.quantumproxies.io:8000"

async def fetch(client, url):
    r = await client.get(url, timeout=httpx.Timeout(20.0))
    return url, r.status_code, r.text

async def main(urls):
    async with httpx.AsyncClient(proxy=PROXY, http2=True) as client:
        tasks = [fetch(client, u) for u in urls]
        return await asyncio.gather(*tasks)

urls = ["https://httpbin.org/ip"] * 5
print(asyncio.run(main(urls)))

Un solo client, riutilizzato per ogni richiesta, è il punto — mantiene caldo il pool di connessioni così salti ripetuti handshake TLS attraverso il proxy. Creare un nuovo client per ogni richiesta è l'errore di performance asincrono più comune: butta via il riutilizzo delle connessioni e lo stato dei cookie ad ogni chiamata.

Imposta un proxy in aiohttp (per richiesta)

aiohttp è nativo di asyncio e ti offre il controllo più fine sulla concorrenza, ma la sua convenzione proxy è diversa: il proxy viene passato per richiesta su session.get(), non sulla sessione. Questo è in realtà conveniente per la rotazione. Due cose mordono le persone qui — il User-Agent di default è letteralmente Python/3.x aiohttp/3.x, un segnale evidente che devi sovrascrivere, e dovresti dimensionare esplicitamente il pool di connessioni.

import aiohttp, asyncio

PROXY = "http://USER:PASS@gate.quantumproxies.io:8000"
HEADERS = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
           "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36"}

async def fetch(session, url):
    timeout = aiohttp.ClientTimeout(total=30, connect=10, sock_read=20)
    async with session.get(url, proxy=PROXY, timeout=timeout) as resp:
        return url, resp.status, await resp.text()

async def main(urls):
    conn = aiohttp.TCPConnector(limit=20, limit_per_host=8)
    async with aiohttp.ClientSession(connector=conn, headers=HEADERS) as session:
        return await asyncio.gather(*(fetch(session, u) for u in urls))

print(asyncio.run(main(["https://httpbin.org/ip"] * 5)))

TCPConnector(limit=20, limit_per_host=8) limita il totale delle connessioni aperte e le connessioni a qualsiasi singolo host — una prima linea di cortesia che impedisce a un target di assorbire l'intero pool.

Diagramma di un ciclo di scraping asincrono: asyncio.gather alimenta un semaforo, poi un gateway proxy rotante, poi recuperi target paralleli
Il semaforo limita; il gateway rotante maschera. Hai bisogno di entrambi — velocità asincrona senza uno di essi è un'onda di blocco.

Limita la concorrenza con un semaforo

Lanciare richieste simultanee illimitate è il modo più veloce per bruciare un pool di proxy e superare i limiti di velocità. Un asyncio.Semaphore è il limitatore: limita quante richieste sono in volo contemporaneamente, indipendentemente da quante attività metti in coda. Imposta un limite globale, e per lavori aggressivi anche un limite per dominio.

sem = asyncio.Semaphore(15)  # never more than 15 requests in flight

async def guarded_fetch(client, url):
    async with sem:
        return await fetch(client, url)

async def main(urls):
    async with httpx.AsyncClient(proxy=PROXY) as client:
        return await asyncio.gather(*(guarded_fetch(client, u) for u in urls))

Il numero giusto dipende dalla tolleranza del target e dalla dimensione del tuo pool, non da quanto velocemente la tua macchina può andare. Inizia conservativo (10–20), osserva il tuo tasso di blocco, e aumentalo solo mentre il successo rimane alto. Su un singolo IP fisso, mantienilo in cifre singole.

Timeout: modella ogni fase

Una richiesta proxy fallisce in più punti rispetto a una diretta — risoluzione DNS, connessione al proxy, impostazione del tunnel, connessione al target, attesa per gli header, e lettura del corpo sono tutti stalli distinti. Un singolo timeout generale nasconde quale fase si è bloccata. aiohttp's ClientTimeout(total=, connect=, sock_read=) e httpx's Timeout() ti permettono di limitarli separatamente. L'unica regola senza eccezioni: mai emettere una richiesta senza un timeout, o un'uscita morta bloccherà una coroutine per sempre e affamerà silenziosamente il tuo ciclo di eventi.

Ruota i proxy senza interrompere le sessioni

Ci sono due strategie di rotazione e scegliere quella sbagliata corrompe i tuoi dati. La rotazione casuale per richiesta è perfetta per recuperi di pagine senza stato. Ma distrugge qualsiasi flusso che dipende da cookie, login o localizzazione, perché la seconda richiesta arriva su un IP diverso dalla prima. La divisione pulita: ruota al confine dell'unità logica — un IP per segmento di crawl o per account — e usa un gateway rotante che ti consegna automaticamente un'uscita fresca così il tuo codice non gestisce mai una lista.

# rotating gateway: one endpoint, new exit IP per request
ROT = "http://USER:PASS@rotating.quantumproxies.io:8000"

# sticky session: same IP for a multi-step flow, tag the session id
STICKY = "http://USER-session-a1b2:PASS@gate.quantumproxies.io:8000"

async def crawl_segment(urls):
    async with httpx.AsyncClient(proxy=ROT) as client:  # rotates per call
        return await asyncio.gather(*(fetch(client, u) for u in urls))

Un gateway residenziale rotante è la scelta pragmatica di default per lo scraping ad alta concorrenza: rotazione per richiesta su oltre 90M di IP in oltre 200 paesi, con sessioni fisse quando un carrello o un login necessita della stessa uscita per alcuni minuti. Se stai valutando residenziale contro ISP o datacenter per il lavoro, la nostra guida su quale tipo di proxy usare delinea i compromessi.

Ottieni un gateway residenziale rotante

Retry, backoff ed jitter

Uscite morte e blocchi transitori sono normali su larga scala, non eccezionali. Ma i retry ingenui peggiorano le cose: quando 50 attività asincrone falliscono nello stesso istante e tutte riprovano immediatamente, lanci un'esplosione sincronizzata che colpisce il target più duramente della corsa originale. Aggiungi backoff esponenziale più jitter randomizzato così i retry si distribuiscono, limita i tentativi, e ruota l'IP in caso di fallimento piuttosto che riutilizzare quello bruciato.

import random
from httpx import HTTPError

async def robust_fetch(client, url, tries=3):
    for attempt in range(tries):
        try:
            r = await client.get(url, timeout=httpx.Timeout(20.0))
            if r.status_code < 400 and looks_real(r.text):
                return r
        except HTTPError:
            pass
        # exponential backoff + jitter before the next attempt
        await asyncio.sleep((2 ** attempt) + random.uniform(0, 1))
    return None
Confronto affiancato delle convenzioni proxy, timeout e controlli di concorrenza di httpx e aiohttp per lo scraping asincrono
Due librerie, due convenzioni proxy: httpx imposta il proxy sul client, aiohttp per richiesta.

Un 200 non è un successo

Il bug di scraping asincrono più sottile è trattare HTTP 200 come fatto. I sistemi anti-bot restituiscono 200 con una pagina CAPTCHA, un avviso di accesso negato, un set di risultati vuoto o una sfida JS — quindi un proxy che segna "successo" solo sul codice di stato ti sta alimentando silenziosamente pagine bloccate. Valida il contenuto: controlla un elemento noto, una lunghezza minima, o l'assenza di marcatori di sfida prima di fidarti di una risposta. Questo è il gate looks_real() nel ciclo di retry sopra.

Quando smettere di costruire manualmente lo stack

Il modello sopra — client asincrono, semaforo, timeout, rotazione, validazione del contenuto — gestisce la maggior parte dei target in modo pulito. Ma una volta che un sito aggiunge Cloudflare, fingerprinting TLS o rendering pesante lato client, l'HTTP asincrono grezzo inizia a perdere indipendentemente dal tuo proxy, perché un handshake TLS Python non assomiglia a quello di Chrome. A quel punto un Scraper API che porta un vero fingerprint del browser, ruota gli IP e rende JavaScript su richiesta è meno codice e un tasso di successo più alto rispetto a mantenerlo tutto a mano. Il nostro post su headless vs HTTP cost copre dove quella escalation paga.

Domande frequenti

Come uso un proxy con aiohttp?

Passa l'URL del proxy per richiesta: session.get(url, proxy="http://user:pass@host:port"). A differenza di requests, aiohttp non accetta un dizionario proxies sulla sessione. Sovrascrivi sempre il User-Agent di default (Python/3.x aiohttp/3.x è un segnale evidente di bot) e imposta un ClientTimeout così un'uscita morta non può bloccare la coroutine.

È meglio httpx o aiohttp per lo scraping asincrono?

Scegli httpx come default: un client funziona in modo sincrono e asincrono, HTTP/2 è integrato, e il proxy è un singolo argomento. Scegli aiohttp quando vuoi il massimo controllo sulla concorrenza — limiti espliciti del pool di connessioni e proxy per richiesta si adattano a crawl grandi e veloci. Entrambi vanno bene; la strategia proxy conta più della libreria.

Quante richieste simultanee dovrei eseguire?

Non quante la tua macchina permette — quante il target e il tuo pool tollerano. Inizia con un semaforo di 10–20 in volo, osserva il tasso di blocco ed errore, e aumentalo solo mentre il successo rimane alto. Su un singolo IP fisso, rimani in cifre singole. Una rotazione IP più ampia ti consente di eseguire una concorrenza totale più alta in sicurezza.

Perché il mio scraper asincrono viene bloccato quando quello sincrono no?

Perché la concorrenza concentra il segnale: molte richieste simultanee da un solo IP è un classico schema di bot. Distribuisci il carico attraverso un pool rotante, limita con un semaforo, aggiungi backoff con jitter sui retry, e valida il contenuto della risposta — un 200 può ancora essere una pagina di sfida. La velocità senza rotazione è ciò che ti ha bloccato.

Questo è il modello asincrono completo: scegli un client, imposta il proxy nel modo giusto per esso, limita la concorrenza con un semaforo, limita ogni fase di timeout, ruota al confine logico, e non fidarti mai di un semplice 200. Imposta correttamente il livello proxy e la maggior parte della lista di blocco scompare prima che tu la colpisca.

Prova il QuantumProxies Scraper API