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.
- Conceptos básicos de la sintaxis async/await
- Descripción general y papel de la biblioteca asyncio
- Diferencias y uso de corutinas y tareas
- Ventajas y limitaciones de la programación asíncrona
- Escribir funciones asíncronas en la práctica
- Métodos para realizar procesamiento en paralelo: uso de gather y wait
- Ejemplos de I/O asíncrono: Operaciones con archivos y redes
- Construcción de un Web Crawler Asíncrono
- Resumen
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 usandoawait
oasyncio.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ística | Corutina | Tarea |
---|---|---|
Forma de definición | async def para definir | asyncio.create_task() para crear |
Forma de ejecución | await o asyncio.run() para ejecutar | Programada y ejecutada automáticamente por el bucle de eventos |
Ejecución concurrente | Escribe operaciones asíncronas individuales | Permite 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
yasyncio.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
deasyncio.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 archivoexample_com.txt
. - De manera similar, los contenidos de otras URLs se guardarán en sus respectivos archivos.
Consideraciones al usar I/O asíncrono
- 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}")
- Implementación de limitación de tareas
Al ejecutar muchas tareas asíncronas a la vez, puedes poner carga en el sistema o servidor. Usaasyncio.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()
- 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:
- Gestión de la lista de URLs: Gestiona eficientemente las URLs a rastrear.
- Comunicaciones HTTP asíncronas: Utiliza la librería
aiohttp
para obtener páginas web. - 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
fetch_page
function
Obtiene la página web de forma asíncrona usando solicitudes HTTP. Verifica el código de estado y maneja errores.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.crawl
function
Toma una lista de URLs y procesa cada una en paralelo. Usaasyncio.gather
para ejecutar las tareas simultáneamente.process_url
function
Combinafetch_page
yparse_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
- Verificación de legalidad
Al operar un crawler web, asegúrate de cumplir con las políticas de uso del sitio, comorobots.txt
. - Manejo exhaustivo de errores
Diseña el crawler para manejar adecuadamente los errores de red y análisis HTML, sin detener su funcionamiento. - 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.