Python asyncio mi fa impazzire, qualcuno sa perché si blocca?

👤 Iniziato da @carmelagreco3
📅 14/06/2025 11:11
📁 Programmazione 🌐 IT
Avatar di carmelagreco3
Ragazzi, ho un problema bestiale con asyncio in Python 3.11 e sto per dare di matto. Voglio fare scraping di una decina di pagine web in parallelo, ma il mio codice si blocca random dopo il primo await. Ecco un esempio:

```python
import asyncio
import aiohttp

async def fetch(url):
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
return await response.text()

async def main():
urls = ["https://example.com/page1", "https://example.com/page2"]
tasks = [fetch(url) for url in urls]
results = await asyncio.gather(*tasks) # Qui si pianta spesso!
print(results)

asyncio.run(main())
```

Non capisco sta roba: a volte va, a volte no, e non vedo errori nel log. Ho provato con semafori, timeout e cavoli vari ma niente. Secondo voi è un problema di sessioni, di troppe connessioni aperte o ho sbagliato tutto io l'approccio? Aiutatemi prima che butti il PC dalla finestra, grazie mille!
Avatar di emberpellegrini
@Carmelagreco3, ho avuto un problema simile un paio di mesi fa. La colpa è quasi certamente del timeout mancante nelle chiamate a session.get(). Anche se esegui 10 pagine, se una di queste non risponde mai (firewall, server morto, DNS incasinato), asyncio si pianta lì ad aspettare per sempre, senza errori. Aggiungi un timeout esplicito, così:

async with session.get(url, timeout=10) as response:

In più, smetti di creare una sessione nuova per ogni fetch. Passa una singola sessione a tutte le coroutine. Aprire 10 sessioni contemporaneamente è uno spreco di risorse e può provocare collisioni.

Per i log, metti try/except dentro fetch() per capire quale URL ti sta rompendo le scatole. A volte siti grandiosi come example.com (cit.) non rispondono bene a richieste parallele multiple, o ti bannano temporaneamente.

P.S. Se proprio non ne vieni a capo, prova a scaricare solo gli URL singoli e vedi quale si blocca. Non è Python che è rotto, è la vita che è crudele.
Avatar di maddalenamonti
@carmelagreco3, ho passato ore a sputtanare problemi simili l’anno scorso. Il problema principale è che non stai limitando il numero di task concorrenti: anche se hai 10 URL, se provi a scaricarli tutti insieme su un sito che non li digerisce (tipo example.com che è un placeholder, non un sito reale) parti in loop infinito. Usa un **semaphore** per controllare la concorrenza, non per limitarla a casaccio. Ecco come:

```python
async def fetch(session, url):
try:
async with session.get(url, timeout=5) as response:
return await response.text()
except Exception as e:
print(f"Errore su {url}: {e}")
return None

async def main():
urls = ["https://example.com/page1", "https://example.com/page2"] * 5
connector = aiohttp.TCPConnector(limit_per_host=3, ssl=False)
async with aiohttp.ClientSession(connector=connector) as session:
tasks = [fetch(session, url) for url in urls]
results = await asyncio.gather(*tasks)
```

**Problemi chiave:**
1. Hai creato una sessione per ogni fetch: spreco di risorse e rischio di overload
2. Nessun controllo sugli errori (SSL, timeout, ecc.)
3. `example.com` non è reale e spesso risponde a cazzo di cane alle richieste multiple

Aggiungi `ssl=False` per evitare errori SSL (a volte certificati scaduti o sballati), imposta `limit_per_host` per non stancare il server e, soprattutto, **passa la sessione già aperta** alle coroutine. Se non funziona, prova a disabilitare IPv6 nel connector: `family=socket.AF_INET`. A volte i DNS giocano brutti scherzi.
Avatar di reginadeluca
Carmelagreco3, ti capisco benissimo! Ho passato una settimana intera a combattere con asyncio l'anno scorso per uno scraper e ancora mi vengono i brividi. Il codice che hai sembra corretto in teoria, ma qualche osservazione pratica:

1) **Sessione condivisa FTW**: Creare una nuova sessione per ogni fetch è un suicidio per le connessioni. Passa una singola sessione a tutte le coroutine come dice Emberpellegrini. Modifica fetch(session, url) e nel main crea la sessione UNA volta con `async with aiohttp.ClientSession() as session:`

2) **Timeout OBBLIGATORI**: Senza timeout, basta un sito lento o down per bloccare tutto. Aggiungi `timeout=aiohttp.ClientTimeout(total=10)` nella sessione OPPURE direttamente in session.get()

3) **Debugga i singoli URL**: Prova a lanciare le fetch separatamente con `asyncio.run(fetch(url))` per isolare il sito problematico. Spesso è un URL morto che non dà errori ma blocca tutto.

4) **Limitatori salvavita**: Usa `asyncio.Semaphore(5)` o imposta `limit_per_host=5` nella sessione. Scaricare 10 siti in parallelo è troppo per molti server.

5) **Error handling MIGLIORE**:
```python
try:
response.raise_for_status()
except aiohttp.ClientError as e:
print(f"Disastro su {url}: {e}")
```

Prova a modificare e se ancora pianta, condividi gli errori! E per l'amor del cielo, metti un cuscino sotto la finestra prima di lanciare il PC 😉
Avatar di cremenziosantoro67
Carmelagreco3, io ho passato giorni a tormentarmi con asyncio l'anno scorso per uno scraper e ti posso assicurare che la soluzione è un mix di cose "banali" ma facilissime da dimenticare.

1) Il semaforo va usato per limitare LEGGERMENTE la concorrenza, non per bloccare tutto. Impostalo a 3 o 5 per sito, non di più.

2) Nella sessione, metti</tool_call> `connector = aiohttp.TCPConnector(limit=10, limit_per_host=3)` per assicurarti che non vai in overkill su un unico host. È un dettaglio che passa inosservato ma</tool_call> fa la differenza.

3) Aggiungi</tool_call> i timeout SIA alla sessione (con `ClientTimeout(total=10)`) sia alle singole chiamate. È un doppione ma ti salva il didietro 9 volte su 10.

4) PER TUTTI I SANTI, usa Python 3.10 o superiore</tool_call>. Ho visto</tool_call></tool_call> codice identico funzionare su 3.10 e piantarsi su 3.9 perché c'era un bug in asyncio.

5) Nel main, raccogli i risultati e stampa i fallimenti. Ad esempio:

```python
results = await asyncio.gather(*tasks, return_exceptions=True)
failed = [res for res in results if isinstance(res, Exception)]
print(f"Totale pagine scansionate: {len(results)}")
print(f"Pagine fallite: {len(failed)}")
for url, res in zip(urls, results):
if isinstance(res, Exception):
print(f"ERRORE su {url}: {res}")
```

Hai provato</tool_call> anche a ridurre la lista a un URL per volta? Spesso l'errore è in un singolo sito che ti rompe le scatole e fa impallare tutto</tool_call> il resto.
Avatar di carmelagreco3
Crema santorum sei un salvavita! Quella roba del `limit_per_host` mi mancava proprio, non ci avevo pensato. E il timeout doppio? Maiala, altro che "banale"!
Ho tolto il semaforo e messo il connector con limiti, già ora sembra respirare. Con Python 3.11 sono a posto, ma il tuo esempio per gestire gli errori è LA svolta - ieri mi si blocca tutto perché un sito bastardo ritornava 404 e l'asyncio impazziva.
Domani provo con un URL solo per stanare sti falliti, ma grazie ai tuoi consigli ho già ridotto i fail del 80%. Se non funziona ti scrivo col sangue, ma per ora...
Avatar di vivianamarino
@Carmelagreco3, ma ti sembra il modo di ringraziare uno col sangue? Se non funziona, scrivimi pure col sudore della fronte, che non siamo mica qui a fare i damerini. Però attenta alla testa: quel `return_exceptions=True` in `gather` te lo dico io, non ti scordare di usare `isinstance(result, Exception)` per sniffare gli errori specifici senza far crollare tutto. E quei siti bastardi che danno 404 e fanno i furbi? Aggiungi un `response.raise_for_status()` prima di `.text()` così li prendi per le palle e li fai urlare in chiaro. Ah, e non usare mai `timeout=aiohttp.ClientTimeout` solo per la sessione: mettilo anche nel `session.get`, che certi host annoiati si piazzano e ti dormono addosso. Se domani ti rifila un altro 404, cambia User-Agent, che magari ti stanno bloccando con la tracciabilità. E per carità, chiuda la sessione con `await session.close()` nel `main`, non siamo barbari! Maiala a parte, se butti il PC dalla finestra poi come ci aggiusti i codici? Vai a comprare un caffè forte e correggi i try-except, che la tradizione conta: senza, sei solo un anarchico nel codice.
Avatar di cadencegalli18
@vivianamarino e @carmelagreco3: un attimo, che qua ci sono un paio di cose che fanno sobbalzare. Primo, il `return_exceptions=True` è basilare. Senza, se un sito ti ritorna un 404 (o peggio un timeout), `gather` butta tutto a mare. Devi sniffare ogni risultato con `isinstance(result, Exception)` e gestirli uno a uno, altrimenti sei fritto. Secondo, quel `response.raise_for_status()` lo metti dentro il fetch, prima di `.text()`, così i siti che ti danno risposte finte ti scoppiano in faccia subito.

E il timeout? Sì, va messo sia in `ClientSession` che in `session.get`, ma se non usi `asyncio.wait_for()` con un `timeout=5` anche su ogni task singolo, certi server che dormono addosso ti fregano lo stesso. User-Agent? Obbligatorio cambiarlo, usa una lista e rotalo con `random.choice()` o ti bannano prima che digiti "scraping".

Poi: chiudere la sessione nel main è la cosa più triste del secolo. Ogni fetch apre e chiude una sessione? Ma stiamo a ste cazzate? Fai una sessione sola, passala a tutti i task e chiudila con `await session.close()` dopo il gather. E se ti blocca ancora, forza il garbage collector con `gc.collect()` e vai a controllare se hai event loop sbagliati (sopratutto su Windows). Ci si vede domani se non funziona: porto caffè e un debuggger a martellate.
Avatar di tearinaldi
@cadencegalli18, hai centrato il punto! Quella roba della sessione aperta per ogni fetch mi faceva impazzire. Io ho fatto la stessa cazzata all'inizio, e il mio script si piantava ogni volta che provavo a fare più di 5 richieste.

Il tuo suggerimento di gestire gli errori con `return_exceptions=True` è geniale - l'ho già implementato e ora posso distinguere i 404 dai timeout senza far crollare tutto. Anche il `response.raise_for_status()` mi ha salvato il culo con quei siti che fingono di funzionare.

Per il User-Agent, io uso una lista di agenti rotanti ogni 3 richieste - funziona alla grande, ovviamente devo cambiare anche l'IP ogni tanto per non far insospettire i server. E il garbage collector forzato? L'avevo letto da qualche parte ma non capivo quando usarlo - ora è chiaro che serve per liberare risorse quando le sessioni si accumulano.

Sul timeout doppio, non conoscevo l'esistenza di `asyncio.wait_for` - pensavo bastasse settarlo nella sessione. Ora vedo che è necessario anche per i task individuali.

E per la sessione unica, hai perfettamente ragione - non capivo perché passarla a tutti i fetch. Ora il mio codice è molto più pulito e non si pianta più.

Insomma, grazie per la tua risposta dettagliata - mi hai fatto capire molti punti oscuri. Adesso provo a mettere insieme tutto e vediamo se riesco a fare funzionare il mio scraping senza impazzire. Se ci saranno altri problemi, ti scriverò per un altro caffè e debug a martellate!

La Tua Risposta

💬

Vuoi partecipare alla discussione?

Accedi o registrati per scrivere la tua risposta e unirti alla conversazione!