Conceptos básicos y aplicaciones de la sintaxis async/await en Python

La sintaxis async/await de Python juega un papel crucial en el desarrollo de aplicaciones, especialmente cuando se manejan tareas de entrada/salida (I/O) o múltiples solicitudes. Esta estructura permite escribir código de manera eficiente y concisa para procesar tareas asíncronas. En este artículo, explicaremos desde los conceptos fundamentales hasta las aplicaciones prácticas de esta sintaxis, usando ejemplos de código para entender cómo utilizar la programación asíncrona de manera efectiva.

Índice

Conceptos básicos de la sintaxis async/await


La sintaxis async/await en Python es un conjunto de palabras clave que permiten implementar programación asíncrona de manera sencilla. Usando estas palabras clave, las operaciones lentas (como las operaciones I/O) se pueden gestionar de manera eficiente, mejorando la capacidad de respuesta del programa.

¿Qué es la programación asíncrona?


La programación asíncrona es una técnica que permite que un programa ejecute otras tareas mientras espera la finalización de una tarea en curso. A diferencia de la programación sincrónica, donde las tareas se ejecutan de forma secuencial, la programación asíncrona hace que varias tareas parezcan ejecutarse “simultáneamente”.

El papel de async y await

  • async: Se utiliza para definir una función asíncrona. Esta función es una coroutine (corrutina) y puede llamar a otras operaciones asíncronas utilizando await.
  • await: Se utiliza para esperar el resultado de una operación asíncrona. Mientras se espera con await, otras tareas pueden ejecutarse, mejorando la eficiencia del programa.

Ejemplo básico de uso


A continuación se presenta un ejemplo sencillo del uso de async/await:

import asyncio

async def say_hello():
    print("Hello")
    await asyncio.sleep(1)  # Espera durante 1 segundo
    print("World")

# Ejecutar la función asíncrona
asyncio.run(say_hello())

Este código imprimirá “Hello”, esperará un segundo y luego imprimirá “World”. Mientras espera con await, otras tareas asíncronas pueden ejecutarse.

Características de las corutinas

  • Las funciones definidas con async no se pueden ejecutar directamente, deben ejecutarse usando await o asyncio.run().
  • Para aprovechar al máximo la programación asíncrona, es necesario combinar adecuadamente corutinas y tareas (se explicará más adelante).

Descripción general y papel de la biblioteca asyncio


asyncio es una biblioteca estándar de Python que proporciona un conjunto de herramientas para gestionar de manera eficiente las operaciones asíncronas. Con ella, se pueden implementar fácilmente operaciones I/O y la ejecución simultánea de múltiples tareas.

El papel de asyncio

  • Gestión del bucle de eventos: Su función principal es la planificación y ejecución de las tareas.
  • Gestión de corutinas y tareas: Registra las tareas como corutinas y las ejecuta de manera eficiente.
  • Soporte para operaciones I/O asíncronas: Permite realizar operaciones de entrada/salida como lectura de archivos o comunicación en red sin bloquear el proceso.

¿Qué es un bucle de eventos?


Un bucle de eventos es como un motor que maneja las tareas asíncronas de manera secuencial. En asyncio, el bucle de eventos gestiona las funciones asíncronas y planifica su ejecución eficiente.

import asyncio

async def example_task():
    print("Task started")
    await asyncio.sleep(1)
    print("Task finished")

async def main():
    # Ejecutar tarea dentro del bucle de eventos
    await example_task()

# Iniciar el bucle de eventos y ejecutar main()
asyncio.run(main())

Principales funciones y clases de asyncio

  • asyncio.run(): Inicia el bucle de eventos y ejecuta una función asíncrona.
  • asyncio.create_task(): Registra una corutina como tarea en el bucle de eventos.
  • asyncio.sleep(): Pausa la ejecución asíncrona durante un tiempo determinado.
  • asyncio.gather(): Ejecuta múltiples tareas al mismo tiempo y recoge sus resultados.
  • asyncio.Queue: Una cola para gestionar la transferencia de datos entre tareas asíncronas.

Ejemplo simple de aplicación


A continuación se presenta un ejemplo en el que se ejecutan varias tareas de manera simultánea:

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():
    # Ejecución concurrente
    await asyncio.gather(task1(), task2())

asyncio.run(main())

En este programa, las tareas 1 y 2 se ejecutan en paralelo, y la tarea 2 se completa antes que la tarea 1.

Ventajas de asyncio

  • Permite gestionar eficientemente un gran número de tareas.
  • Mejora el rendimiento de las tareas I/O-bound.
  • Posibilita una planificación flexible de tareas mediante el bucle de eventos.

Al comprender asyncio, se puede aprovechar al máximo la programación asíncrona para mejorar la eficiencia del código.

Diferencias y uso de corutinas y tareas


Las corutinas y las tareas son conceptos fundamentales en la programación asíncrona de Python. Al comprender sus características y funciones, se pueden utilizar correctamente para lograr una programación asíncrona eficiente.

¿Qué es una corutina?


Una corutina es una función especial definida como asíncrona con async def. Puede ejecutar otras operaciones asíncronas utilizando await, y puede pausarse durante su ejecución, reanudándose más tarde.

Ejemplo: Definición y uso de una corutina

import asyncio

async def my_coroutine():
    print("Start coroutine")
    await asyncio.sleep(1)
    print("End coroutine")

# Ejecutar la corutina
asyncio.run(my_coroutine())

¿Qué es una tarea?


Una tarea es una corutina envuelta que se ejecuta en el bucle de eventos. Se crea utilizando asyncio.create_task() y se ejecuta automáticamente una vez que se registra en el bucle de eventos.

Ejemplo de creación y ejecución de tareas

import asyncio

async def my_coroutine(number):
    print(f"Coroutine {number} started")
    await asyncio.sleep(1)
    print(f"Coroutine {number} finished")

async def main():
    # Crear y ejecutar múltiples tareas en paralelo
    task1 = asyncio.create_task(my_coroutine(1))
    task2 = asyncio.create_task(my_coroutine(2))

    # Esperar a que las tareas terminen
    await task1
    await task2

asyncio.run(main())

En este ejemplo, las tareas 1 y 2 se inician simultáneamente y se ejecutan en paralelo.

Diferencias entre corutinas y tareas

CaracterísticaCorutinaTarea
Forma de definiciónasync def para definirasyncio.create_task() para crear
Forma de ejecuciónawait o asyncio.run() para ejecutarProgramada y ejecutada automáticamente por el bucle de eventos
Ejecución concurrenteEscribe operaciones asíncronas individualesPermite la ejecución simultánea de múltiples operaciones asíncronas

Puntos clave para decidir su uso

  • Corutinas se usan para describir tareas asíncronas simples.
  • Tareas son útiles para ejecutar varias operaciones asíncronas en paralelo.

Ejemplo práctico: Procesamiento concurrente usando tareas


A continuación, se muestra un ejemplo que ejecuta varias funciones asíncronas de manera eficiente utilizando tareas:

import asyncio

async def fetch_data(url):
    print(f"Fetching data from {url}")
    await asyncio.sleep(2)  # Simulando espera de red
    print(f"Finished fetching data from {url}")

async def main():
    urls = ["https://example.com", "https://example.org", "https://example.net"]

    # Crear múltiples tareas
    tasks = [asyncio.create_task(fetch_data(url)) for url in urls]

    # Esperar a que todas las tareas terminen
    await asyncio.gather(*tasks)

asyncio.run(main())

Este programa genera múltiples tareas utilizando comprensión de listas y las ejecuta en paralelo.

Consideraciones importantes

  • El orden de ejecución de las tareas no está garantizado, por lo que no es adecuado para tareas con dependencias.
  • Las tareas se registran en el bucle de eventos y no se pueden utilizar fuera de este.

Al comprender las diferencias entre corutinas y tareas, y al utilizarlas correctamente, se puede maximizar la eficiencia de la programación asíncrona.

Ventajas y limitaciones de la programación asíncrona


La programación asíncrona es útil para mejorar el rendimiento en aplicaciones con muchas operaciones I/O, pero no es una solución universal. En esta sección, exploramos los beneficios y limitaciones de la programación asíncrona para aprovecharla de manera efectiva.

Ventajas de la programación asíncrona

1. Mayor velocidad y eficiencia

  • Utilización eficiente de los recursos durante la espera de I/O: En la programación sincrónica, el programa se detiene mientras espera las operaciones I/O. En la programación asíncrona, otras tareas pueden ejecutarse durante ese tiempo.
  • Alta capacidad de procesamiento: Es ideal para servidores que gestionan muchas solicitudes simultáneas o clientes que realizan operaciones de red concurrentes.

2. Mejora de la capacidad de respuesta

  • Mejora la experiencia del usuario: Al aplicar programación asíncrona, los procesos en segundo plano pueden ejecutarse sin bloquear la interfaz de usuario, mejorando la capacidad de respuesta.
  • Reducción del tiempo de espera: Utilizando I/O asíncrono, se puede realizar múltiples tareas al mismo tiempo, reduciendo el tiempo de espera total.

3. Flexibilidad y escalabilidad

  • Diseño escalable: Los programas asíncronos utilizan recursos del sistema de manera eficiente sin consumir demasiados hilos o procesos.
  • Multitarea: La programación asíncrona permite cambiar entre tareas de manera eficiente, lo que ayuda a manejar sistemas con alta carga de trabajo.

Limitaciones de la programación asíncrona

1. Complejidad del programa


La programación asíncrona puede ser difícil de comprender y depurar, especialmente cuando se trata de dependencias complejas entre tareas. Esto puede hacer que el mantenimiento y la depuración sean más complicados.

  • Condiciones de carrera: Es difícil garantizar la coherencia de los datos cuando varias tareas acceden a los mismos recursos.
  • Infierno de callbacks: El uso excesivo de callbacks puede hacer que el código sea difícil de leer y seguir.

2. Ineficiencia en tareas CPU-bound


La programación asíncrona está optimizada principalmente para tareas I/O-bound. Las tareas que requieren mucha CPU, como cálculos intensivos, no siempre se benefician de la programación asíncrona debido al Global Interpreter Lock (GIL) en Python.

3. Requiere un diseño adecuado


Para que la programación asíncrona funcione correctamente, es necesario un diseño adecuado y la selección de bibliotecas adecuadas. Un mal diseño puede dar lugar a problemas como:

  • Deadlocks: Las tareas que se bloquean mutuamente al esperar la finalización de otras.
  • Inconsistencias en la programación: Un mal diseño de la programación puede llevar a un rendimiento ineficiente.

Puntos clave para aprovechar la programación asíncrona

1. Uso adecuado según el tipo de tarea

  • Aplicación en tareas I/O-bound: Es ideal para operaciones de base de datos, comunicación en red y operaciones con archivos.
  • Usar hilos o procesos para tareas CPU-bound: Utilizar técnicas como procesamiento paralelo para complementar las tareas I/O-bound.

2. Uso de herramientas y bibliotecas de alta calidad

  • asyncio: La biblioteca estándar para gestionar tareas asíncronas en Python.
  • aiohttp: Biblioteca especializada en comunicaciones HTTP asíncronas.
  • Quart y FastAPI: Frameworks para desarrollo web asíncrono.

3. Depuración y monitoreo constantes

  • Usar registros (logs) para seguir el comportamiento de las tareas y ayudar en la depuración.
  • Activar el modo de depuración de asyncio para obtener información detallada sobre los errores.

La programación asíncrona puede mejorar significativamente el rendimiento de las aplicaciones si se implementa de manera adecuada, pero es importante comprender sus limitaciones y diseñar el programa correctamente para evitar problemas.

Escribir funciones asíncronas en la práctica


Para implementar programación asíncrona en Python, se definen funciones asíncronas utilizando async y await. En esta sección, veremos cómo crear funciones asíncronas y cómo gestionar el flujo básico de operaciones asíncronas.

Estructura básica de una función asíncrona


Una función asíncrona se define usando async def. Dentro de esta función, se utilizan await para llamar a otras funciones asíncronas.

Ejemplo básico de una función asíncrona

import asyncio

async def greet():
    print("Hello,")
    await asyncio.sleep(1)  # Espera de 1 segundo asíncronamente
    print("World!")

# Ejecutar la función asíncrona
asyncio.run(greet())

En este ejemplo, await asyncio.sleep(1) es el punto donde se ejecuta la operación asíncrona. Durante este tiempo de espera de un segundo, otras tareas pueden continuar.

Conexión entre funciones asíncronas


Es posible conectar varias funciones asíncronas para que trabajen en conjunto.

Ejemplo de conexión entre funciones asíncronas

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():
    # Ejecutar funciones asíncronas en orden
    await task1()
    await task2()

asyncio.run(main())

En este caso, la función main se define como asíncrona y ejecuta las funciones task1 y task2 de forma secuencial.

Funciones asíncronas y procesamiento concurrente


Para ejecutar funciones asíncronas de manera concurrente, utilizamos asyncio.create_task, lo que permite que varias tareas avancen al mismo tiempo.

Ejemplo de procesamiento concurrente

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():
    # Crear tareas para ejecución concurrente
    task1_coroutine = asyncio.create_task(task1())
    task2_coroutine = asyncio.create_task(task2())

    # Esperar a que ambas tareas terminen
    await task1_coroutine
    await task2_coroutine

asyncio.run(main())

Este ejemplo muestra cómo task1 y task2 se ejecutan concurrentemente. task2 termina primero, y luego task1 concluye.

Ejemplo práctico: Contador asíncrono simple


A continuación, se muestra un ejemplo donde varias funciones de conteo se ejecutan de manera concurrente.

async def count(number):
    for i in range(1, 4):
        print(f"Counter {number}: {i}")
        await asyncio.sleep(1)  # Espera asíncrona de 1 segundo

async def main():
    # Ejecutar múltiples tareas de conteo
    await asyncio.gather(count(1), count(2), count(3))

asyncio.run(main())

Resultado de la ejecución

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

Al utilizar programación asíncrona, se puede observar que cada contador avanza independientemente.

Puntos clave y consideraciones

  • El uso de programación asíncrona reduce el desperdicio de recursos del sistema y mejora la gestión eficiente de tareas.
  • Es importante elegir entre asyncio.gather y asyncio.create_task según la situación.
  • Cuando se ejecutan funciones asíncronas, siempre debe usarse asyncio.run o el bucle de eventos.

Practicar con funciones asíncronas desde los fundamentos mejorará la habilidad para implementar programación asíncrona en situaciones complejas.

Métodos para realizar procesamiento en paralelo: uso de gather y wait


En el procesamiento asíncrono de Python, se utilizan asyncio.gather y asyncio.wait para ejecutar múltiples tareas en paralelo de manera eficiente. Al entender sus características y formas de uso, puedes construir programas asíncronos más flexibles.

Resumen y ejemplo de uso de asyncio.gather


asyncio.gather ejecuta múltiples tareas asíncronas de manera simultánea y espera hasta que todas las tareas terminen. Después, devuelve los resultados de cada una en una lista.

Ejemplo básico

import asyncio

async def task1():
    await asyncio.sleep(1)
    return "Tarea 1 completa"

async def task2():
    await asyncio.sleep(2)
    return "Tarea 2 completa"

async def main():
    results = await asyncio.gather(task1(), task2())
    print(results)

asyncio.run(main())

Resultado de ejecución

['Tarea 1 completa', 'Tarea 2 completa']

Características

  • Espera a que las tareas en paralelo finalicen y recibe los resultados como una lista.
  • Si ocurre una excepción, gather detiene todas las tareas y propaga la excepción al llamante.

Resumen y ejemplo de uso de asyncio.wait


asyncio.wait ejecuta múltiples tareas en paralelo y devuelve un conjunto de tareas completadas y un conjunto de tareas pendientes.

Ejemplo básico

import asyncio

async def task1():
    await asyncio.sleep(1)
    print("Tarea 1 completa")

async def task2():
    await asyncio.sleep(2)
    print("Tarea 2 completa")

async def main():
    tasks = [task1(), task2()]
    done, pending = await asyncio.wait(tasks)
    print(f"Tareas completadas: {len(done)}, Tareas pendientes: {len(pending)}")

asyncio.run(main())

Resultado de ejecución

Tarea 1 completa
Tarea 2 completa
Tareas completadas: 2, Tareas pendientes: 0

Características

  • Permite verificar con detalle el estado de las tareas (completadas o pendientes).
  • Si una tarea finaliza antes de tiempo, las tareas pendientes siguen siendo procesadas.
  • Usando la opción return_when de asyncio.wait, se puede controlar el momento en que se finalizan las tareas según ciertas condiciones.

Ejemplo de opción return_when

done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
  • FIRST_COMPLETED: Devuelve cuando la primera tarea se completa.
  • FIRST_EXCEPTION: Devuelve cuando ocurre la primera excepción.
  • ALL_COMPLETED: Espera hasta que todas las tareas hayan terminado (por defecto).

Diferencias entre gather y wait

  • Cuando se desea recibir los resultados juntos: Usar asyncio.gather.
  • Cuando se desea gestionar individualmente el estado de las tareas: Usar asyncio.wait.
  • Cuando se quiere finalizar una tarea a medio camino o manejar excepciones: asyncio.wait es más adecuado.

Ejemplo avanzado: Llamadas API en paralelo


A continuación, un ejemplo que muestra cómo realizar varias llamadas API en paralelo y obtener sus respuestas:

import asyncio

async def fetch_data(api_name, delay):
    print(f"Obteniendo datos de {api_name}...")
    await asyncio.sleep(delay)  # Espera simulada
    return f"Datos de {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]

    # Ejecutar en paralelo con gather y recolectar resultados
    results = await asyncio.gather(*tasks)
    for result in results:
        print(result)

asyncio.run(main())

Resultado de ejecución

Obteniendo datos de API_1...
Obteniendo datos de API_2...
Obteniendo datos de API_3...
Datos de API_2
Datos de API_1
Datos de API_3

Consideraciones

  • Gestión de excepciones: Si ocurren excepciones en tareas paralelas, estas deben ser capturadas y manejadas adecuadamente. Utiliza try/except para ello.
  • Cancelación de tareas: Para cancelar tareas innecesarias, usa task.cancel().
  • Evitar deadlocks: Es necesario diseñar para evitar situaciones donde las tareas se bloqueen mutuamente esperando.

Usar correctamente asyncio.gather y asyncio.wait maximiza la flexibilidad y eficiencia del procesamiento asíncrono.

Ejemplos de I/O asíncrono: Operaciones con archivos y redes


El I/O asíncrono es una técnica para hacer más eficiente el manejo de procesos que implican tiempos de espera, como operaciones con archivos o comunicaciones en red. Utilizando asyncio, puedes implementar fácilmente operaciones I/O asíncronas. En esta sección, explicaremos cómo usarlo con ejemplos concretos.

Operaciones con archivos asíncronas


Las operaciones con archivos asíncronas se realizan usando la librería aiofiles, que extiende las operaciones estándar de archivos para que puedan ejecutarse de forma asíncrona.

Ejemplo: Lectura y escritura de archivos de forma asíncrona

import aiofiles
import asyncio

async def read_file(filepath):
    async with aiofiles.open(filepath, mode='r') as file:
        contents = await file.read()
        print(f"Contenido de {filepath}:")
        print(contents)

async def write_file(filepath, data):
    async with aiofiles.open(filepath, mode='w') as file:
        await file.write(data)
        print(f"Datos escritos en {filepath}")

async def main():
    filepath = 'example.txt'
    await write_file(filepath, "¡Hola, I/O de archivo asíncrono!")
    await read_file(filepath)

asyncio.run(main())

Puntos clave

  • Usando aiofiles.open, puedes manejar archivos de forma asíncrona.
  • La sintaxis async with asegura un manejo seguro de los archivos.
  • Durante las operaciones con archivos, otras tareas pueden seguir ejecutándose.

Operaciones de red asíncronas


En las operaciones de red, la librería aiohttp permite realizar solicitudes HTTP de forma asíncrona.

Ejemplo: Solicitudes HTTP asíncronas

import aiohttp
import asyncio

async def fetch_url(session, url):
    async with session.get(url) as response:
        print(f"Obteniendo {url}")
        content = await response.text()
        print(f"Contenido de {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())

Puntos clave

  • Usando aiohttp.ClientSession, puedes realizar comunicaciones HTTP asíncronas.
  • La sintaxis async with maneja la sesión y asegura que las solicitudes se envíen de manera segura.
  • El uso de asyncio.gather permite realizar múltiples solicitudes en paralelo, mejorando la eficiencia.

Combinación de operaciones de archivos y red asíncronas


Combinar operaciones de archivos asíncronas con operaciones de red permite recolectar y guardar datos de manera eficiente.

Ejemplo: Guardar datos descargados de forma asíncrona

import aiohttp
import aiofiles
import asyncio

async def fetch_and_save(session, url, filepath):
    async with session.get(url) as response:
        print(f"Obteniendo {url}")
        content = await response.text()

        async with aiofiles.open(filepath, mode='w') as file:
            await file.write(content)
            print(f"Contenido de {url} guardado en {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())

Ejemplo de resultado de ejecución

  • El contenido de https://example.com se guardará en el archivo example_com.txt.
  • De manera similar, los contenidos de otras URLs se guardarán en sus respectivos archivos.

Consideraciones al usar I/O asíncrono

  1. Implementación de manejo de excepciones
    Prepárate para posibles errores de red o de escritura de archivos mediante el manejo adecuado de excepciones.
   try:
       # Tarea asíncrona
   except Exception as e:
       print(f"Ocurrió un error: {e}")
  1. Implementación de limitación de tareas
    Al ejecutar muchas tareas asíncronas a la vez, puedes poner carga en el sistema o servidor. Usa asyncio.Semaphore para limitar el número de tareas concurrentes.
   semaphore = asyncio.Semaphore(5)  # Ejecutar hasta 5 tareas en paralelo

   async with semaphore:
       await some_async_task()
  1. Configuración de timeouts
    Configura un timeout para evitar esperar indefinidamente por tareas que no responden.
   try:
       await asyncio.wait_for(some_async_task(), timeout=10)
   except asyncio.TimeoutError:
       print("La tarea ha superado el tiempo de espera")

El uso adecuado de I/O asíncrono puede mejorar significativamente la eficiencia y el rendimiento de tu aplicación.

Construcción de un Web Crawler Asíncrono


Usando procesamiento asíncrono, se pueden crear crawlers web rápidos y eficientes. El I/O asíncrono permite obtener múltiples páginas web en paralelo, maximizando la velocidad de rastreo. En esta sección, explicamos cómo implementar un crawler web asíncrono usando Python.

Estructura básica de un Web Crawler Asíncrono


Un crawler web asíncrono se compone de tres elementos importantes:

  1. Gestión de la lista de URLs: Gestiona eficientemente las URLs a rastrear.
  2. Comunicaciones HTTP asíncronas: Utiliza la librería aiohttp para obtener páginas web.
  3. Guardado de los datos: Usa operaciones de archivo asíncronas para almacenar los datos obtenidos.

Ejemplo de código: Web Crawler Asíncrono


A continuación, un ejemplo básico de un crawler web asíncrono:

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"Página obtenida de {url}")
                return html
            else:
                print(f"No se pudo obtener {url}: {response.status}")
                return None
    except Exception as e:
        print(f"Error al obtener {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 "Sin título"
        async with aiofiles.open(filepath, mode='a') as file:
            await file.write(f"URL: {url}\nTítulo: {title}\n\n")
        print(f"Datos guardados para {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"

    # Inicialización: Vaciar el archivo de resultados
    async with aiofiles.open(output_file, mode='w') as file:
        await file.write("")

    await crawl(urls, output_file)

asyncio.run(main())

Explicación del funcionamiento del código

  1. fetch_page function
    Obtiene la página web de forma asíncrona usando solicitudes HTTP. Verifica el código de estado y maneja errores.
  2. parse_and_save function
    Usa BeautifulSoup para analizar el HTML y extraer el título de la página. Guarda esta información de manera asíncrona en un archivo.
  3. crawl function
    Toma una lista de URLs y procesa cada una en paralelo. Usa asyncio.gather para ejecutar las tareas simultáneamente.
  4. process_url function
    Combina fetch_page y parse_and_save para procesar una URL completamente.

Ejemplo de resultado de ejecución


El archivo crawl_results.txt contendrá datos como los siguientes:

URL: https://example.com
Título: Example Domain

URL: https://example.org
Título: Example Domain

URL: https://example.net
Título: Example Domain

Mejoras de rendimiento

  • Limitación de tareas paralelas
    Al rastrear muchas URLs, limita la cantidad de tareas paralelas para no sobrecargar el servidor.
  semaphore = asyncio.Semaphore(10)

  async def limited_process_url(semaphore, session, url, output_file):
      async with semaphore:
          await process_url(session, url, output_file)
  • Agregando lógica de reintentos
    Si algunas solicitudes fallan, agrega un mecanismo de reintento para aumentar la fiabilidad.

Consideraciones importantes

  1. Verificación de legalidad
    Al operar un crawler web, asegúrate de cumplir con las políticas de uso del sitio, como robots.txt.
  2. Manejo exhaustivo de errores
    Diseña el crawler para manejar adecuadamente los errores de red y análisis HTML, sin detener su funcionamiento.
  3. Configuración de timeouts
    Configura un timeout para las solicitudes y evita esperar indefinidamente.
   async with session.get(url, timeout=10) as response:

Un crawler web asíncrono, con un diseño y control adecuado, permite una recolección de datos eficiente y escalable.

Resumen


Este artículo explicó detalladamente el uso del procesamiento asíncrono en Python utilizando la sintaxis async/await, desde los conceptos básicos hasta ejemplos avanzados. Al comprender y aplicar el procesamiento asíncrono, puedes hacer más eficientes las tareas que dependen de I/O y mejorar el rendimiento de tus aplicaciones.

Aprendimos cómo utilizar la librería asyncio, empleando gather y wait para el procesamiento paralelo, además de ejemplos prácticos de I/O asíncrono y cómo construir un crawler web asíncrono.

Si se diseña correctamente, la programación asíncrona puede ayudar a crear sistemas eficientes y escalables, pero es importante considerar el manejo de excepciones y la legalidad al implementarla. Usa este artículo como referencia para aprender y aprovechar el procesamiento asíncrono en tus proyectos.

Índice