La sintassi async/await di Python è un meccanismo che consente di scrivere facilmente operazioni asincrone, svolgendo un ruolo particolarmente importante nelle applicazioni che gestiscono compiti legati a I/O o numerose richieste. In questo articolo, esploreremo i concetti di base di questa sintassi, nonché le modalità pratiche di utilizzo e alcuni esempi di applicazione. Impariamo le basi della programmazione asincrona e approfondiamo la comprensione tramite esempi di codice concreti.
Concetti di base della sintassi async/await
La sintassi async/await di Python è costituita da parole chiave che permettono di implementare facilmente la programmazione asincrona. Usando queste parole chiave, è possibile gestire in modo efficiente operazioni che richiedono tempo (come le operazioni di I/O), migliorando la reattività del programma.
Cos’è la programmazione asincrona
La programmazione asincrona è una tecnica che permette di eseguire altre operazioni mentre un compito è in attesa di completamento. Mentre nella programmazione sincrona ogni compito viene eseguito in sequenza, nella programmazione asincrona sembra che più compiti vengano eseguiti “simultaneamente”.
Ruolo di async e await
- async: Viene utilizzato per definire una funzione asincrona. Questa funzione è chiamata coroutine e può invocare altre operazioni asincrone utilizzando
await
. - await: Viene utilizzato per aspettare il risultato di un’operazione asincrona. Durante l’attesa con
await
, altre operazioni possono essere eseguite, migliorando l’efficienza complessiva del programma.
Esempio di utilizzo di base
Ecco un semplice esempio di utilizzo di async/await:
import asyncio
async def say_hello():
print("Hello")
await asyncio.sleep(1) # Aspetta per 1 secondo
print("World")
# Esegui la funzione asincrona
asyncio.run(say_hello())
Questo codice stampa “Hello”, poi aspetta per 1 secondo e infine stampa “World”. Anche mentre il programma è in attesa con await
, altre operazioni asincrone possono essere eseguite.
Caratteristiche delle coroutines
- Le funzioni definite con
async
non possono essere eseguite direttamente; devono essere eseguite tramiteawait
oasyncio.run()
. - Per utilizzare in modo efficiente la programmazione asincrona, è necessario combinare correttamente le coroutines e i task (che verranno trattati nel prossimo paragrafo).
Panoramica e ruolo della libreria asyncio
La libreria asyncio
di Python, parte della libreria standard, offre un insieme di strumenti per gestire in modo efficiente la programmazione asincrona. Con asyncio
, è possibile implementare facilmente operazioni di I/O e la gestione parallela di più task.
Ruolo di asyncio
- Gestione del ciclo degli eventi: Gestisce la pianificazione e l’esecuzione dei task in modo centralizzato.
- Gestione di coroutines e task: Registra le operazioni asincrone come task e le esegue in modo efficiente.
- Supporto per le operazioni asincrone di I/O: Esegue operazioni di I/O come letture da file o comunicazioni di rete in modo asincrono.
Cos’è un ciclo degli eventi?
Il ciclo degli eventi è il motore che gestisce l’esecuzione dei task asincroni. In asyncio
, questo ciclo gestisce le coroutines e pianifica le operazioni in modo efficiente.
import asyncio
async def example_task():
print("Task started")
await asyncio.sleep(1)
print("Task finished")
async def main():
# Esegui il task nel ciclo degli eventi
await example_task()
# Avvia il ciclo degli eventi ed esegui main()
asyncio.run(main())
Funzioni e classi principali di asyncio
asyncio.run()
: Avvia il ciclo degli eventi ed esegue una funzione asincrona.asyncio.create_task()
: Registra una coroutine come task nel ciclo degli eventi.asyncio.sleep()
: Attende in modo asincrono per un periodo di tempo specificato.asyncio.gather()
: Esegue più task contemporaneamente e raccoglie i risultati.asyncio.Queue
: Una coda per scambiare dati tra task asincroni in modo efficiente.
Esempio di applicazione semplice
Ecco un esempio di esecuzione parallela di più task:
async def task1():
print("Task 1 started")
await asyncio.sleep(2)
print("Task 1 finished")
async def task2():
print("Task 2 started")
await asyncio.sleep(1)
print("Task 2 finished")
async def main():
# Esecuzione parallela
await asyncio.gather(task1(), task2())
asyncio.run(main())
In questo programma, il task1 e il task2 vengono eseguiti contemporaneamente, con task2 che termina prima di task1.
Vantaggi di asyncio
- Gestione efficiente di molti task.
- Aumento delle prestazioni nei task legati a I/O.
- Pianificazione flessibile tramite il ciclo degli eventi.
Comprendere asyncio ti permetterà di sfruttare appieno il potenziale della programmazione asincrona.
Differenze e utilizzi di coroutine e task
In Python, le coroutines e i task sono concetti fondamentali per la programmazione asincrona. Comprendere le loro caratteristiche e ruoli ti permetterà di utilizzare entrambi in modo efficiente per gestire la programmazione asincrona.
Cos’è una coroutine?
Una coroutine è una funzione speciale definita come funzione asincrona tramite async def
, che può eseguire altre operazioni asincrone usando await
. Le coroutines possono fermarsi a metà esecuzione e essere riprese da un altro punto.
Esempio: Definizione e utilizzo di una coroutine
import asyncio
async def my_coroutine():
print("Start coroutine")
await asyncio.sleep(1)
print("End coroutine")
# Esecuzione della coroutine
asyncio.run(my_coroutine())
Cos’è un task?
Un task è una coroutine che è stata “incapsulata” per essere eseguita nel ciclo degli eventi. Viene creata utilizzando asyncio.create_task()
e viene registrata nel ciclo degli eventi per l’esecuzione parallela.
Esempio di creazione ed esecuzione di un task
import asyncio
async def my_coroutine(number):
print(f"Coroutine {number} started")
await asyncio.sleep(1)
print(f"Coroutine {number} finished")
async def main():
# Crea e esegui più task contemporaneamente
task1 = asyncio.create_task(my_coroutine(1))
task2 = asyncio.create_task(my_coroutine(2))
# Attendi che entrambi i task finiscano
await task1
await task2
asyncio.run(main())
In questo esempio, il task1 e il task2 iniziano contemporaneamente, e ciascuno esegue il proprio compito in parallelo.
Differenze tra coroutines e task
Caratteristica | Coroutine | Task |
---|---|---|
Metodo di definizione | async def | asyncio.create_task() |
Modo di esecuzione | await o asyncio.run() | Viene pianificato nel ciclo degli eventi e eseguito automaticamente |
Esecuzione parallela | Definisce una singola operazione asincrona | Consente l’esecuzione parallela di più operazioni asincrone |
Quando utilizzare coroutine e task
- Coroutine vengono utilizzate per definire operazioni asincrone semplici.
- Task vengono utilizzati quando si vuole eseguire più operazioni asincrone in parallelo.
Applicazione: Esecuzione parallela con i task
Ecco un esempio che utilizza i task per eseguire più operazioni asincrone:
import asyncio
async def fetch_data(url):
print(f"Fetching data from {url}")
await asyncio.sleep(2) # Simula un'attesa di rete
print(f"Finished fetching data from {url}")
async def main():
urls = ["https://example.com", "https://example.org", "https://example.net"]
# Crea i task
tasks = [asyncio.create_task(fetch_data(url)) for url in urls]
# Attendi che tutti i task siano completati
await asyncio.gather(*tasks)
asyncio.run(main())
Questo programma crea e esegue più task contemporaneamente utilizzando la comprensione delle liste per generare i task.
Considerazioni
- Non è garantito l’ordine di esecuzione dei task, quindi non è adatto per operazioni che dipendono dall’ordine.
- I task vengono pianificati nel ciclo degli eventi, quindi non possono essere utilizzati al di fuori di questo.
Comprendere correttamente la differenza tra coroutine e task, e utilizzarli in modo appropriato, ti permetterà di massimizzare l’efficienza dei programmi asincroni.
Vantaggi e limiti della programmazione asincrona
La programmazione asincrona è molto utile per migliorare le prestazioni, in particolare nelle applicazioni che richiedono molte operazioni di I/O, ma non è una panacea. In questa sezione esploreremo i vantaggi e i limiti della programmazione asincrona, fornendo una guida su come utilizzarla in modo appropriato.
Vantaggi della programmazione asincrona
1. Velocità ed efficienza
- Utilizzo delle risorse durante l’attesa di I/O: Mentre la programmazione sincrona ferma l’esecuzione durante l’attesa, la programmazione asincrona consente di eseguire altre operazioni, ottimizzando l’uso delle risorse.
- Alta capacità di gestione: È ideale per server che devono gestire molte richieste contemporaneamente o per client che devono eseguire più operazioni di rete in parallelo.
2. Miglioramento della reattività
- Miglioramento dell’esperienza utente: Applicando la programmazione asincrona, è possibile eseguire operazioni in background senza bloccare l’interfaccia utente, migliorando la reattività.
- Riduzione dei tempi di attesa: Grazie all’I/O asincrono, è possibile ridurre i tempi di attesa complessivi eseguendo operazioni in parallelo.
3. Flessibilità e scalabilità
- Progettazione scalabile: I programmi asincroni consumano risorse di sistema in modo più efficiente rispetto a quelli basati su thread o processi.
- Multitasking: L’esecuzione asincrona consente al sistema di gestire un carico elevato tramite l’efficiente gestione del cambio tra i task.
Limiti della programmazione asincrona
1. Complessità del programma
La programmazione asincrona può risultare meno intuitiva e più difficile da debuggare e mantenere rispetto a quella sincrona, soprattutto in alcuni scenari:
- Condizioni di gara: Quando più task accedono alla stessa risorsa, può essere difficile mantenere la consistenza dei dati.
- Callback hell: La gestione di dipendenze complesse può rendere il codice difficile da leggere e mantenere.
2. Inefficienza per task CPU-bound
La programmazione asincrona è ottimizzata principalmente per task legati a I/O. Per operazioni intensamente legate alla CPU, come calcoli complessi, la programmazione asincrona potrebbe non migliorare le prestazioni, a causa delle limitazioni del GIL (Global Interpreter Lock).
3. Necessità di una progettazione adeguata
Per far funzionare efficacemente la programmazione asincrona, è fondamentale una progettazione adeguata e la scelta dei giusti strumenti e librerie. Una progettazione inadeguata può portare a problematiche come:
- Deadlock: Una situazione in cui i task si bloccano aspettando reciprocamente la conclusione.
- Incoerenza nella pianificazione: Una pianificazione inefficiente può far sì che il programma impieghi più tempo del previsto.
Strategie per sfruttare al meglio la programmazione asincrona
1. Uso appropriato in base al contesto
- Utilizzo per operazioni I/O-bound: È efficace per operazioni di database, comunicazioni di rete, operazioni su file e altre attività che coinvolgono l’attesa di I/O.
- Utilizzo di thread o processi per task CPU-bound: Combinando la programmazione asincrona con tecniche di parallelizzazione basate su thread e processi, si possono ottenere ottimi risultati.
2. Sfruttare strumenti e librerie di alta qualità
- asyncio: Lo strumento base della libreria standard per gestire la programmazione asincrona.
- aiohttp: Una libreria per la gestione asincrona delle comunicazioni HTTP.
- Quart e FastAPI: Framework web che supportano la programmazione asincrona.
3. Debugging e monitoraggio accurati
- Usa i log per tracciare il comportamento dei task e aiutare nel debugging.
- Abilitando la modalità di debug di
asyncio
, puoi ottenere informazioni dettagliate sugli errori.
La programmazione asincrona, se progettata correttamente, può migliorare notevolmente le prestazioni delle tue applicazioni, ma è importante comprendere anche i suoi limiti e fare una progettazione adeguata.
Scrivere funzioni asincrone in pratica
Per implementare la programmazione asincrona in Python, definisci funzioni asincrone usando async
e await
. In questa sezione, vedremo come creare funzioni asincrone e comprendere il flusso di base della programmazione asincrona.
Struttura di base di una funzione asincrona
Le funzioni asincrone sono definite con async def
. All’interno di queste funzioni, puoi invocare altre operazioni asincrone usando await
.
Esempio di funzione asincrona di base
import asyncio
async def greet():
print("Hello,")
await asyncio.sleep(1) # Attende asincronicamente per 1 secondo
print("World!")
# Esecuzione della funzione asincrona
asyncio.run(greet())
In questo esempio, await asyncio.sleep(1)
è il punto in cui la funzione asincrona si ferma per aspettare. Durante questo tempo, altre operazioni possono essere eseguite.
Collegamento di funzioni asincrone
È possibile chiamare più funzioni asincrone e farle collaborare tra loro.
Esempio di collegamento di funzioni asincrone
async def task1():
print("Task 1 started")
await asyncio.sleep(2)
print("Task 1 finished")
async def task2():
print("Task 2 started")
await asyncio.sleep(1)
print("Task 2 finished")
async def main():
# Esecuzione sequenziale delle funzioni asincrone
await task1()
await task2()
asyncio.run(main())
Qui la funzione main
è definita come asincrona e invoca altre due funzioni asincrone (task1
e task2
) in sequenza.
Funzioni asincrone e esecuzione parallela
Per eseguire funzioni asincrone in parallelo, puoi usare asyncio.create_task
.
Esempio di esecuzione parallela
async def task1():
print("Task 1 started")
await asyncio.sleep(2)
print("Task 1 finished")
async def task2():
print("Task 2 started")
await asyncio.sleep(1)
print("Task 2 finished")
async def main():
# Creazione di task per esecuzione parallela
task1_coroutine = asyncio.create_task(task1())
task2_coroutine = asyncio.create_task(task2())
# Attendi che entrambe le coroutine finiscano
await task1_coroutine
await task2_coroutine
asyncio.run(main())
In questo esempio, task1
e task2
vengono eseguiti contemporaneamente. task2
termina prima, seguito da task1
.
Esempio avanzato: Un semplice contatore asincrono
Ecco un esempio che usa funzioni asincrone per contare in parallelo. Ogni contatore esegue la sua operazione senza bloccare gli altri.
async def count(number):
for i in range(1, 4):
print(f"Counter {number}: {i}")
await asyncio.sleep(1) # Attende asincronicamente per 1 secondo
async def main():
# Esecuzione parallela dei contatori
await asyncio.gather(count(1), count(2), count(3))
asyncio.run(main())
Risultato dell’esecuzione
Counter 1: 1
Counter 2: 1
Counter 3: 1
Counter 1: 2
Counter 2: 2
Counter 3: 2
Counter 1: 3
Counter 2: 3
Counter 3: 3
Utilizzando la programmazione asincrona, possiamo osservare che ogni contatore è eseguito indipendentemente dagli altri.
Punti importanti e considerazioni
- La programmazione asincrona riduce il consumo inefficiente di risorse di sistema, migliorando la gestione dei task.
- Usa correttamente
asyncio.gather
easyncio.create_task
per gestire i task in parallelo. - Quando esegui funzioni asincrone, ricordati di usare sempre
asyncio.run
o un ciclo degli eventi.
Praticando la creazione di funzioni asincrone, aumenterai la tua capacità di applicare la programmazione asincrona in modo efficace.
Metodi di implementazione del parallelismo: utilizzo di gather e wait
Nel trattamento asincrono di Python, asyncio.gather
e asyncio.wait
vengono utilizzati per eseguire più attività in parallelo in modo efficiente. Comprendendo le caratteristiche e l’uso di ciascuno, è possibile costruire programmi asincroni più flessibili.
Panoramica di asyncio.gather e esempio di utilizzo
asyncio.gather
esegue più attività asincrone in parallelo e attende fino al completamento di tutte le attività. Al termine, restituisce i risultati come una lista.
Esempio di base
import asyncio
async def task1():
await asyncio.sleep(1)
return "Task 1 complete"
async def task2():
await asyncio.sleep(2)
return "Task 2 complete"
async def main():
results = await asyncio.gather(task1(), task2())
print(results)
asyncio.run(main())
Risultato di esecuzione
['Task 1 complete', 'Task 2 complete']
Caratteristiche
- Attende il completamento delle attività in parallelo e restituisce i risultati come lista.
- Se si verifica un’eccezione,
gather
interrompe tutte le attività e propaga l’eccezione alla funzione chiamante.
Panoramica di asyncio.wait e esempio di utilizzo
asyncio.wait
esegue più attività in parallelo e restituisce un set con le attività completate e quelle non completate.
Esempio di base
import asyncio
async def task1():
await asyncio.sleep(1)
print("Task 1 complete")
async def task2():
await asyncio.sleep(2)
print("Task 2 complete")
async def main():
tasks = [task1(), task2()]
done, pending = await asyncio.wait(tasks)
print(f"Done tasks: {len(done)}, Pending tasks: {len(pending)}")
asyncio.run(main())
Risultato di esecuzione
Task 1 complete
Task 2 complete
Done tasks: 2, Pending tasks: 0
Caratteristiche
- Possibilità di monitorare lo stato delle attività (completato/non completato) in modo dettagliato.
- Anche se un’attività termina prima, è possibile gestire le attività non completate.
- Utilizzando l’opzione
return_when
diasyncio.wait
, è possibile controllare la terminazione delle attività in base a determinate condizioni.
Esempio di opzione return_when
done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
FIRST_COMPLETED
: Ritorna quando la prima attività è completata.FIRST_EXCEPTION
: Ritorna quando si verifica la prima eccezione.ALL_COMPLETED
: Attende che tutte le attività siano completate (impostazione predefinita).
Differenze nell’uso di gather e wait
- Quando si desidera raccogliere i risultati tutti insieme: usare
asyncio.gather
. - Quando si vuole gestire separatamente lo stato delle attività: usare
asyncio.wait
. - Quando si desidera terminare anticipatamente o gestire le eccezioni:
asyncio.wait
è la scelta migliore.
Esempio avanzato: chiamate API parallele
Di seguito un esempio che mostra come chiamare più API in parallelo e raccogliere le risposte:
import asyncio
async def fetch_data(api_name, delay):
print(f"Fetching from {api_name}...")
await asyncio.sleep(delay) # Simulated wait
return f"Data from {api_name}"
async def main():
apis = [("API_1", 2), ("API_2", 1), ("API_3", 3)]
tasks = [fetch_data(api, delay) for api, delay in apis]
# Parallel processing with gather, collecting results
results = await asyncio.gather(*tasks)
for result in results:
print(result)
asyncio.run(main())
Risultato di esecuzione
Fetching from API_1...
Fetching from API_2...
Fetching from API_3...
Data from API_2
Data from API_1
Data from API_3
Punti da considerare
- Gestione delle eccezioni: In caso di eccezione durante l’esecuzione delle attività parallele, è necessario catturare e gestire correttamente l’eccezione utilizzando
try
/except
. - Annullo delle attività: Se un’attività non è più necessaria, può essere annullata con
task.cancel()
. - Attenzione al deadlock: È necessario progettare in modo da evitare situazioni in cui le attività si aspettano a vicenda.
Utilizzando correttamente asyncio.gather
e asyncio.wait
, è possibile massimizzare la flessibilità e l’efficienza del trattamento asincrono.
Esempi di I/O asincrono: operazioni su file e rete
L’I/O asincrono è una tecnica utilizzata per ottimizzare operazioni che comportano attese, come la gestione di file o le comunicazioni di rete. Utilizzando asyncio
, è possibile implementare facilmente l’I/O asincrono. In questa sezione, esploreremo l’uso di base dell’I/O asincrono attraverso esempi pratici.
Operazioni sui file asincrone
Per eseguire operazioni sui file asincrone, utilizziamo la libreria aiofiles
, che estende le operazioni sui file della libreria standard per eseguirle in modo asincrono.
Esempio: Lettura e scrittura asincrona dei file
import aiofiles
import asyncio
async def read_file(filepath):
async with aiofiles.open(filepath, mode='r') as file:
contents = await file.read()
print(f"Contents of {filepath}:")
print(contents)
async def write_file(filepath, data):
async with aiofiles.open(filepath, mode='w') as file:
await file.write(data)
print(f"Data written to {filepath}")
async def main():
filepath = 'example.txt'
await write_file(filepath, "Hello, Async File IO!")
await read_file(filepath)
asyncio.run(main())
Punti chiave
- Le operazioni sui file asincrone possono essere gestite con
aiofiles.open
. - Usare la sintassi
async with
per gestire in modo sicuro i file. - Anche durante l’elaborazione dei file, altre attività possono proseguire.
Operazioni di rete asincrone
Per le operazioni di rete, possiamo utilizzare la libreria aiohttp
per inviare richieste HTTP in modo asincrono.
Esempio: Richieste HTTP asincrone
import aiohttp
import asyncio
async def fetch_url(session, url):
async with session.get(url) as response:
print(f"Fetching {url}")
content = await response.text()
print(f"Content from {url}: {content[:100]}...")
async def main():
urls = [
"https://example.com",
"https://example.org",
"https://example.net"
]
async with aiohttp.ClientSession() as session:
tasks = [fetch_url(session, url) for url in urls]
await asyncio.gather(*tasks)
asyncio.run(main())
Punti chiave
- Le comunicazioni HTTP asincrone vengono gestite con
aiohttp.ClientSession
. - Utilizzare la sintassi
async with
per gestire le sessioni e inviare le richieste in modo sicuro. - Eseguiamo richieste parallele usando
asyncio.gather
per ottimizzare le operazioni.
Combinazione di operazioni su file e rete asincrone
Combinando operazioni asincrone sui file e sulla rete, è possibile raccogliere e memorizzare i dati in modo efficiente.
Esempio: Salvare i dati scaricati in modo asincrono
import aiohttp
import aiofiles
import asyncio
async def fetch_and_save(session, url, filepath):
async with session.get(url) as response:
print(f"Fetching {url}")
content = await response.text()
async with aiofiles.open(filepath, mode='w') as file:
await file.write(content)
print(f"Content from {url} saved to {filepath}")
async def main():
urls = [
("https://example.com", "example_com.txt"),
("https://example.org", "example_org.txt"),
("https://example.net", "example_net.txt")
]
async with aiohttp.ClientSession() as session:
tasks = [fetch_and_save(session, url, filepath) for url, filepath in urls]
await asyncio.gather(*tasks)
asyncio.run(main())
Esempio di risultato dell’esecuzione
- Il contenuto di
https://example.com
viene salvato nel fileexample_com.txt
. - Allo stesso modo, i contenuti di altri URL vengono salvati nei rispettivi file.
Considerazioni sull’uso dell’I/O asincrono
- Gestione delle eccezioni
Prevedere una gestione adeguata delle eccezioni in caso di errori di rete o di scrittura dei file.
try:
# Async task
except Exception as e:
print(f"An error occurred: {e}")
- Implementazione del throttling
Eseguire troppi task asincroni contemporaneamente potrebbe sovraccaricare il sistema o il server. È possibile limitare il numero di task in parallelo usandoasyncio.Semaphore
.
semaphore = asyncio.Semaphore(5) # Limit to 5 concurrent tasks
async with semaphore:
await some_async_task()
- Impostazione dei timeout
Per evitare che i processi che non rispondono rimangano in attesa per troppo tempo, è consigliabile impostare un timeout.
try:
await asyncio.wait_for(some_async_task(), timeout=10)
except asyncio.TimeoutError:
print("Task timed out")
L’utilizzo corretto dell’I/O asincrono consente di migliorare notevolmente l’efficienza e la capacità di elaborazione delle applicazioni.
Esempio avanzato: costruzione di un crawler Web asincrono
Il parallelismo asincrono consente di creare crawler Web veloci ed efficienti. Utilizzando l’I/O asincrono, è possibile raccogliere numerose pagine web in parallelo, massimizzando la velocità di crawling. In questa sezione esploreremo come implementare un crawler Web asincrono in Python.
Struttura di base di un crawler Web asincrono
In un crawler Web asincrono, tre elementi principali sono cruciali:
- Gestione della lista degli URL: Gestire in modo efficiente gli URL da analizzare.
- Comunicazioni HTTP asincrone: Recuperare le pagine web utilizzando la libreria asincrona
aiohttp
. - Salvataggio dei dati: Salvare i dati recuperati utilizzando operazioni asincrone sui file.
Esempio di codice: Crawler Web asincrono
Di seguito viene mostrato un esempio di base di un crawler Web asincrono:
import aiohttp
import aiofiles
import asyncio
from bs4 import BeautifulSoup
async def fetch_page(session, url):
try:
async with session.get(url) as response:
if response.status == 200:
html = await response.text()
print(f"Fetched {url}")
return html
else:
print(f"Failed to fetch {url}: {response.status}")
return None
except Exception as e:
print(f"Error fetching {url}: {e}")
return None
async def parse_and_save(html, url, filepath):
if html:
soup = BeautifulSoup(html, 'html.parser')
title = soup.title.string if soup.title else "No Title"
async with aiofiles.open(filepath, mode='a') as file:
await file.write(f"URL: {url}\nTitle: {title}\n\n")
print(f"Saved data for {url}")
async def crawl(urls, output_file):
async with aiohttp.ClientSession() as session:
tasks = []
for url in urls:
tasks.append(process_url(session, url, output_file))
await asyncio.gather(*tasks)
async def process_url(session, url, output_file):
html = await fetch_page(session, url)
await parse_and_save(html, url, output_file)
async def main():
urls = [
"https://example.com",
"https://example.org",
"https://example.net"
]
output_file = "crawl_results.txt"
# Inizializzazione: pulire il file di output
async with aiofiles.open(output_file, mode='w') as file:
await file.write("")
await crawl(urls, output_file)
asyncio.run(main())
Spiegazione del funzionamento del codice
fetch_page
function
Recupera il codice HTML della pagina Web in modo asincrono. Verifica il codice di stato della risposta e gestisce gli errori.parse_and_save
function
Analizza l’HTML con BeautifulSoup e salva il titolo della pagina nel file di output.crawl
function
Elenco degli URL che vengono elaborati in parallelo. Utilizzaasyncio.gather
per gestire tutte le attività in parallelo.process_url
function
Combinazione delle funzionifetch_page
eparse_and_save
per elaborare un URL completo.
Esempio di risultato dell’esecuzione
Il file crawl_results.txt
contiene i seguenti dati:
URL: https://example.com
Title: Example Domain
URL: https://example.org
Title: Example Domain
URL: https://example.net
Title: Example Domain
Ottimizzazione delle prestazioni
- Limitazione dei task paralleli
Quando si eseguono molte operazioni di crawling, è possibile limitare il numero di task paralleli per evitare sovraccarichi sul server.
semaphore = asyncio.Semaphore(10)
async def limited_process_url(semaphore, session, url, output_file):
async with semaphore:
await process_url(session, url, output_file)
- Aggiunta della funzionalità di retry
In caso di errori temporanei nelle richieste, si può implementare una logica di retry per aumentare l’affidabilità.
Considerazioni finali
- Verifica della legalità
Quando si utilizza un crawler Web, è fondamentale rispettare ilrobots.txt
e i termini di servizio dei siti web. - Gestione delle eccezioni
Assicurarsi che errori di rete o di parsing HTML non interrompano il funzionamento del crawler. - Impostazione di timeout
Impostare i timeout per evitare che le richieste rimangano in attesa indefinitamente.
async with session.get(url, timeout=10) as response:
Un crawler Web asincrono, se progettato correttamente, può raccogliere dati in modo efficiente ed è altamente scalabile.
Riepilogo
In questo articolo, abbiamo esplorato come sfruttare la sintassi async/await
di Python per la programmazione asincrona, dalla teoria di base agli esempi avanzati. Comprendendo l’uso di asyncio
, gather
e wait
, e attraverso esempi pratici di I/O asincrono e costruzione di un crawler Web asincrono, è possibile migliorare significativamente l’efficienza delle operazioni di I/O.
Un’implementazione corretta della programmazione asincrona aiuta a costruire sistemi efficienti e scalabili, ma è importante considerare sempre l’eccezione e la legalità nell’uso di queste tecniche.