I fondamenti e le applicazioni della sintassi async/await in Python

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.

Indice

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 tramite await o asyncio.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

CaratteristicaCoroutineTask
Metodo di definizioneasync defasyncio.create_task()
Modo di esecuzioneawait o asyncio.run()Viene pianificato nel ciclo degli eventi e eseguito automaticamente
Esecuzione parallelaDefinisce una singola operazione asincronaConsente 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 e asyncio.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 di asyncio.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 file example_com.txt.
  • Allo stesso modo, i contenuti di altri URL vengono salvati nei rispettivi file.

Considerazioni sull’uso dell’I/O asincrono

  1. 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}")
  1. Implementazione del throttling
    Eseguire troppi task asincroni contemporaneamente potrebbe sovraccaricare il sistema o il server. È possibile limitare il numero di task in parallelo usando asyncio.Semaphore.
   semaphore = asyncio.Semaphore(5)  # Limit to 5 concurrent tasks

   async with semaphore:
       await some_async_task()
  1. 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:

  1. Gestione della lista degli URL: Gestire in modo efficiente gli URL da analizzare.
  2. Comunicazioni HTTP asincrone: Recuperare le pagine web utilizzando la libreria asincrona aiohttp.
  3. 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

  1. fetch_page function
    Recupera il codice HTML della pagina Web in modo asincrono. Verifica il codice di stato della risposta e gestisce gli errori.
  2. parse_and_save function
    Analizza l’HTML con BeautifulSoup e salva il titolo della pagina nel file di output.
  3. crawl function
    Elenco degli URL che vengono elaborati in parallelo. Utilizza asyncio.gather per gestire tutte le attività in parallelo.
  4. process_url function
    Combinazione delle funzioni fetch_page e parse_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

  1. Verifica della legalità
    Quando si utilizza un crawler Web, è fondamentale rispettare il robots.txt e i termini di servizio dei siti web.
  2. Gestione delle eccezioni
    Assicurarsi che errori di rete o di parsing HTML non interrompano il funzionamento del crawler.
  3. 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.

Indice