Asyncio para Web Scraping: Peticiones Concurrentes Hechas Correctamente
TL;DR
asyncio mejora el rendimiento para muchas esperas de red independientes; no hace más rápido el análisis de HTML ni otorga permiso para aumentar la tasa de solicitudes de un sitio.
Reutiliza una aiohttp.ClientSession, establece tiempos de espera totales y de conexión, y limita tanto la concurrencia global como por host.
Un semáforo limita las tareas simultáneas, no las solicitudes por segundo. Añade un ritmo explícito por host cuando la fuente lo requiera.
Vuelve a intentar solo fallos transitorios como 429, 502, 503, y 504, con un retraso limitado y manejo de Retry-After; no vuelvas a intentar ciegamente cada respuesta 4xx.
Para sitios renderizados por JavaScript o descubrimiento recursivo, llama a una API de renderizado y Crawl gestionada en lugar de esperar que aiohttp ejecute JavaScript.
¿Cuándo Deberías Usar Asyncio para Web Scraping?
Usa asyncio cuando el scraper pasa la mayor parte de su tiempo esperando muchas respuestas HTTP permitidas. Un trabajador puede gestionar múltiples solicitudes en vuelo sin un hilo por solicitud. El beneficio depende de la latencia de la fuente, la tasa de solicitud permitida, el costo de análisis y el almacenamiento posterior. Si la fuente permite solo una solicitud cada pocos segundos, un cliente asíncrono no puede legítimamente convertirlo en un rastreo de alta tasa. El producto Nstdata Crawl es una opción gestionada para los pasos de adquisición y conversión discutidos aquí.
Descubre Nstproxy Crawl - Empieza Tu Prueba Gratuita Hoy
La documentación de tareas de Python asyncio explica cómo se programan las corutinas. La guía del cliente aiohttp recomienda reutilizar una sesión en lugar de crear una por solicitud. Una sesión mantiene un grupo de conexiones y una configuración compartida. Usa este tutorial solo para fuentes a las que estés autorizado a recolectar.
Crea una lista corta de URLs aprobadas, anota la tasa de solicitud permitida de cada host y elige un User-Agent contactable. Para Nstdata Crawl, obtén una cuenta y una clave API; el encabezado de autenticación documentado es x-api-key. No necesitas valores de Host, Puerto, Nombre de Usuario, Contraseña o Protocolo de proxy para una solicitud estándar de Crawl gestionado porque Nstdata maneja el enrutamiento de proxy.
¿Cómo Difere la Concurrencia del Limitado de Tasa?
La concurrencia es el número de solicitudes en vuelo a la vez. La tasa es con qué frecuencia comienzan las nuevas solicitudes. Un semáforo de tres puede producir tres solicitudes en el mismo instante, luego otras tres tan pronto como terminen las respuestas. Eso puede violar el ritmo deseado de una fuente, aunque solo tres solicitudes estén activas. Usa un intervalo de inicio por host o un token bucket cuando un host requiera una tasa máxima.
Establece límites en tres niveles: el número total de tareas activas, el limit_per_host del grupo de conexiones y el intervalo de inicio por host. Mantenlos conservadores durante un piloto y ajusta según la política de la fuente, las respuestas 429, la latencia y la tasa de errores. El ejemplo a continuación utiliza un segundo entre inicios por host y un máximo de tres conexiones por host; estas son configuraciones ilustrativas, no una tasa segura universal.
¿Cómo Recuperas Páginas Concurrentemente con Aiohttp?
Método 1: Ejecutar un Recuperador de HTML Limitado
Mejor para: Varias páginas estáticas cuyos datos aparecen en la respuesta HTTP inicial.
Paso 1. Guarda el script a continuación como async_scrape.py. Reemplaza URLS con URLs aprobadas y ajusta MIN_HOST_INTERVAL a la política real de la fuente.
import asyncio
import time
from collections import defaultdict
from urllib.parse import urlparse
import aiohttp
from bs4 import BeautifulSoup
URLS =["https://example.com/"]# Reemplaza con URLs aprobadas.MAX_CONCURRENT =8MAX_PER_HOST =3MIN_HOST_INTERVAL =1.0RETRYABLE ={429,502,503,504}classHostPacer:def__init__(self, interval:float): self.interval = interval
self.locks = defaultdict(asyncio.Lock) self.next_start = defaultdict(float)asyncdefwait(self, host:str):asyncwith self.locks[host]: delay = self.next_start[host]- time.monotonic()if delay >0:await asyncio.sleep(delay) self.next_start[host]= time.monotonic()+ self.interval
defretry_delay(header, attempt:int)->float:if header and header.isdigit():returnmin(float(header),60.0)returnmin(2** attempt,30.0)asyncdeffetch(session, pacer, semaphore, url): host = urlparse(url).hostname
si not host o urlparse(url).scheme notin{"http","https"}:raise ValueError(f"URL no soportada: {url}")for attempt inrange(3):await pacer.wait(host)try:asyncwith semaphore:asyncwith session.get(url)as response:if response.status in RETRYABLE: delay = retry_delay(response.headers.get("Retry-After"), attempt)else: response.raise_for_status() html =await response.text() soup = BeautifulSoup(html,"html.parser") title = soup.title.get_text(" ", strip=True)if soup.title elseNoneifnot title:raise ValueError(f"Título faltante: {url}")return{"url":str(response.url),"status": response.status,"title": title}except(aiohttp.ClientError, asyncio.TimeoutError):if attempt ==2:raise delay =min(2** attempt,30.0)if attempt ==2:raise RuntimeError(f"Límite de reintentos alcanzado: {url}")await asyncio.sleep(delay)asyncdefmain(): timeout = aiohttp.ClientTimeout(total=30, connect=10) connector = aiohttp.TCPConnector(limit=MAX_CONCURRENT, limit_per_host=MAX_PER_HOST) headers ={"User-Agent":"ExampleResearchBot/1.0 (contacto: team@example.org)"} pacer = HostPacer(MIN_HOST_INTERVAL) semaphore = asyncio.Semaphore(MAX_CONCURRENT)asyncwith aiohttp.ClientSession(timeout=timeout, connector=connector, headers=headers)as session: results =await asyncio.gather(*(fetch(session, pacer, semaphore, url)for url in URLS), return_exceptions=True,)for url, result inzip(URLS, results):print(url, result)if __name__ =="__main__": asyncio.run(main())
Paso 2. Ejecutar python async_scrape.py e inspeccionar cada resultado o excepción. Una respuesta exitosa de example.com debe contener el título de su documento; el script no afirma que cada página objetivo tenga un elemento <title>.
Paso 3. Agregar persistencia solo después de validar los campos extraídos. Almacenar URL, tiempo de recuperación, versión del analizador y razón de rechazo. Reemplazar print con una escritura idempotente clave a una URL canónica o ID de origen.
Límite: El ayudante Retry-After anterior maneja segundos numéricos. Un valor de fecha HTTP necesita un analizador de fechas antes de su uso en producción. El ejemplo también mantiene el bloqueo de espaciado por host mientras espera, serializando intencionadamente los inicios de solicitudes para ese host.
La especificación de semántica HTTP define el comportamiento de encabezados relacionados con el estado y reintentos que el cliente debe interpretar.
¿Cómo procesas miles de URLs sin crear miles de tareas a la vez?
Método 2: Usar una cola de trabajo limitada
Mejor para: Listas grandes de URL o páginas descubiertas continuamente.
Paso 1. Colocar URLs aprobadas en una cola duradera o base de datos con una clave única, contador de intentos y próximo tiempo de recuperación elegible. Una asyncio.Queue en memoria es útil dentro de un proceso, pero no puede recuperar un fallo de máquina.
Paso 2. Iniciar un número fijo de trabajadores que cada uno extraiga una URL, llame al recuperador limitado, valide el registro y reconozca la tarea solo después de escribir la salida duradera.
Paso 3. Registrar fallos con una razón y tiempo de reintento. URLs de carta muerta que agoten reintentos o violen políticas. Aplicar presión de retroceso cuando el analizador o la tier de almacenamiento se queden atrás.
Límite:asyncio.gather sobre un millón de corutinas de URL consume memoria incluso con un semáforo. La cola limita la creación de tareas y proporciona un lugar para verificar el trabajo.
Paso 1. Obtener una clave API de Nstdata y definir el alcance de URLs aprobadas. La API de rastreo documenta la autenticación x-api-key y trabajos de sitio con maxDepth, maxPages, includeUrls, y excludeUrls.
Paso 2. Envía un trabajo limitado a Nstdata Crawl. Consulta el estado y recupera las páginas completadas a través de los endpoints documentados. Nstdata se encarga del descubrimiento de páginas, rastreo recursivo, renderizado de JavaScript y enrutamiento de proxies.
Paso 3. Analiza el Markdown, HTML, datos en bruto, enlaces, capturas de pantalla o PDF devueltos según las necesidades de tu aplicación. Valida el contenido y almacena la procedencia antes de indexar o entrenar.
Límite: La API administrada no reemplaza tus permisos de origen, programador entre sitios o extracción de esquemas de producto arbitrarios. No la utilices para evadir restricciones de acceso.
¿Por qué fallan o devuelven datos incorrectos las solicitudes concurrentes?
429 significa que el origen está limitando la tasa; reduce los inicios por host y respeta la guía de reintentos de la respuesta. Un tiempo de espera puede indicar problemas de red o un origen lento; reintenta unas pocas veces y registra el fallo en lugar de reintentar indefinidamente. Un 200 con un título vacío puede ser un shell de JavaScript, una página de consentimiento o un desajuste de parser. Revisa el HTML devuelto y el comportamiento de la red del navegador antes de cambiar la concurrencia.
Monitorea los registros aceptados por segundo, la latencia mediana y en cola, la tasa 429, el conteo de reintentos y la antigüedad de la cola por host. Un alto conteo de solicitudes con bajo rendimiento de registros aceptados es un problema de calidad. Si un dominio comienza a fallar, pausa ese dominio mientras otras fuentes aprobadas continúan.
¿Cómo haces seguro para producción el ejemplo?
Mueve la entrada de URL a una cola duradera y almacena un resultado terminal por URL. Una gran llamada gather crea una corutina para cada entrada antes de que se ejecute el semáforo, por lo que no es adecuada para un manifiesto muy grande. Un grupo de trabajadores fijo que lea de una cola mantiene la memoria limitada y permite que el arrendamiento de un trabajador que falla expire. La confirmación debe seguir al almacenamiento duradero del registro aceptado o del resultado de fallo.
Agrega un registro de política específico del origen. El intervalo de un segundo y tres conexiones por host del ejemplo son ilustrativos. Los ajustes reales deben provenir del patrón de acceso permitido del sitio y de un piloto. Diferentes hosts pueden necesitar diferentes intervalos de inicio. Un semáforo global protege tu proceso, pero no evita que varios trabajadores distribuidos excedan colectivamente el presupuesto de un host. Para rastreos distribuidos, coloca el estado de la tasa en un programador compartido o asigna a cada host a un grupo de trabajadores coordinado.
Mejora el manejo de reintentos antes de grandes ejecuciones. Los valores numéricos Retry-After son compatibles en el ejemplo; los valores de fecha HTTP necesitan ser analizados. Agrega un retroceso con variabilidad para evitar que muchos trabajadores reintenten al mismo instante. Establece un tiempo máximo transcurrido y reintentos, luego envía a carta muerta las tareas que no puedan tener éxito. No reintentes 401, 403 o URLs mal formadas como si más intentos otorgaran acceso.
Finalmente, mantén el análisis de HTML fuera del bucle de eventos si se vuelve intensivo en CPU. Las pequeñas operaciones de Beautiful Soup pueden ser aceptables, pero analizar páginas muy grandes o realizar extracciones complejas en el mismo bucle puede retrasar todas las solicitudes. Mide la demora del bucle de eventos y mueve el análisis pesado a un proceso de trabajador o a una etapa separada cuando sea importante. La entrada/salida de red asíncrona y el trabajo en paralelo de CPU resuelven diferentes cuellos de botella.
¿Qué deberías monitorear en un scraper concurrente?
Registra la latencia y los resultados por host. Un promedio global oculta el hecho de que una fuente está agotando el tiempo o devolviendo páginas de error suaves. Realiza un seguimiento de las solicitudes en vuelo, los inicios de solicitudes por host, la tasa 429, los tiempos de espera, los bytes de respuesta, el rendimiento de registros aceptados, los intentos de reintentos y la antigüedad de la cola. Alerta sobre una degradación sostenida de la calidad, no meramente sobre fallos de proceso.
Utiliza una muestra de aceptación después de cambiar la concurrencia. Un mayor rendimiento puede aumentar las respuestas incompletas, páginas desafiantes o carga de origen. Compara los registros aceptados por segundo y por costo unitario, y verifica que las reglas del lado del origen aún se respeten. Si el rendimiento aceptado cae mientras el conteo de solicitudes aumenta, reduce la tasa e inspecciona una muestra antes de escalar más.
Mantén separados los conteos en bruto y aceptados en el panel de control. Un estallido de respuestas 200 no es éxito si la mayoría de las páginas contienen una plantilla de error. Valida un marcador de página esperado, título, tipo de contenido y campos requeridos antes de contar un registro como aceptado. Esto le da al equipo de operaciones una señal para pausar un origen mientras el resto de la cola continúa.
No. Un semáforo limita el trabajo simultáneo. Utiliza un mecanismo de ritmo por host para controlar los inicios de solicitudes a lo largo del tiempo.
P: ¿Puede aiohttp rastrear una página renderizada por JavaScript?aiohttp recupera respuestas HTTP pero no ejecuta JavaScript de la página. Utilice un método de renderización de navegador o un camino de Crawl gestionado para el contenido generado después de la carga.
P: ¿Debería usar asyncio.gather para un millón de URLs?
No. Alimente un conjunto limitado de trabajadores desde una cola duradera para que el uso de memoria y la recuperación de trabajos permanezcan controlados.
Este tutorial de Python pasa de una página estática a registros validados y renderizado gestionado. Incluye ejemplos ejecutables, diagnóstico de fallos, controles de versión y un contrato de datos listo para inteligencia artificial.
Marcus Chen
Oct. 29th 2026
Rastrea sitios web completos con una sola solicitud API
99,8% de éxito con renderizado JavaScript
Obtén datos limpios, listos para LLM, en múltiples formatos
Convierte cualquier sitio web en Markdown, HTML, JSON, enlaces, PDF y más, sin gestionar infraestructura de rastreo.