Arquitectura de Raspado Web a Gran Escala: Una Guía de Campo

Unos cientos de páginas es un script. Millones es un sistema distribuido. Aquí está la arquitectura que te lleva allí: colas, trabajadores asíncronos, niveles de proxy, reintentos, deduplicación y verificaciones de calidad de datos, con código funcional.

Raspar unas pocas cientos de páginas es un script. Raspar millones es un sistema distribuido. Una vez que tu objetivo pasa de "se ejecuta durante la noche en mi portátil" a "necesita terminar esta semana sin colapsar", la parte difícil deja de ser el análisis y se convierte en todo lo que lo rodea: cómo encolas el trabajo, lo distribuyes, evitas bloqueos, reintentas fallos y almacenas y verificas el resultado. Esto es raspado web a gran escala como una arquitectura, no un fragmento, con código funcional en cada etapa.

La escala es concurrencia, no un bucle más grande

Una cifra lo hace concreto. Toma una categoría con 20,000 páginas de listado, 20 artículos cada una: 400,000 páginas para obtener. A un realista de 2.5 segundos por página, una ejecución estrictamente secuencial es aproximadamente 1,000,000 segundos, aproximadamente 11.5 días de espera en cargas de página antes de analizar un solo campo. Maneja 200 páginas en paralelo y esos 11.5 días se colapsan hacia una hora de tiempo de reloj. El tiempo es la restricción a escala, y la concurrencia es cómo lo recuperas. Todo lo demás en la arquitectura existe para hacer que esa concurrencia sea sostenible.

Diagrama de estadísticas que muestra 400,000 páginas tomando 11.5 días secuencialmente versus aproximadamente una hora a 200 en paralelo, con 10,000 fallos por millón al 1 por ciento
La concurrencia convierte una ejecución de 11.5 días en una hora, pero con un millón de solicitudes, incluso una tasa de fallos del 1% son 10,000 páginas fallidas.

La arquitectura de un vistazo

Un raspador que sobrevive a millones de páginas es un pequeño sistema distribuido con un puñado de partes nombradas, cada una resolviendo un problema que solo aparece en volumen:

Primero la cola: desacoplar el descubrimiento de la obtención

La decisión estructural más importante es poner una cola entre "qué raspar" y "hacer el raspado". Un productor enumera URLs; un grupo de trabajadores las vacía. Ningún lado sabe qué tan rápido corre el otro, y puedes agregar trabajadores sin tocar al productor. En Python, esto es Celery o RQ sobre Redis; en Node, BullMQ; a mayor escala, RabbitMQ o Kafka. El patrón en un archivo:

import asyncio, aiohttp

CONCURRENCY = 50
queue = asyncio.Queue()

async def worker(session):
    while True:
        url = await queue.get()
        try:
            async with session.get(url, timeout=20) as resp:
                await handle(url, await resp.text(), resp.status)
        except Exception as err:
            await on_failure(url, err)
        finally:
            queue.task_done()

async def run(urls):
    for u in urls:
        queue.put_nowait(u)
    async with aiohttp.ClientSession() as session:
        tasks = [asyncio.create_task(worker(session)) for _ in range(CONCURRENCY)]
        await queue.join()
        for t in tasks:
            t.cancel()

El control que importa es CONCURRENCY. Demasiado bajo desperdicia el paralelismo que hace posible la escala; demasiado alto abruma tanto al objetivo como a tu propio egreso. Encuentras el valor correcto observando cómo aumentan las tasas de error, que es exactamente por qué el monitoreo es una parte de primera clase del sistema, no una ocurrencia tardía.

Diagrama de flujo de una tubería de raspado a gran escala: cola, trabajadores asíncronos, nivel de proxy, análisis y verificaciones de calidad, almacenamiento
Ocho partes nombradas. Las capas de proxy, anti-bot y renderizado son las más difíciles de mantener saludables, y el lugar natural para comprar.

El nivel de proxy se rompe primero

A bajo volumen apenas notas las defensas anti-bot; a escala, rompen la ejecución primero. Envía unos cientos de miles de solicitudes desde una IP y te limitan la tasa, luego te desafían, luego te bloquean. La solución es la rotación a través de muchas direcciones. Nivélalo: IPs de centros de datos baratas para objetivos y APIs indulgentes, IPs residenciales rotativas para objetivos comerciales difíciles que esperan tráfico de usuarios reales. Pero la rotación por sí sola no es suficiente: las defensas modernas también leen las huellas digitales de TLS y el orden de los encabezados, por lo que el tráfico debe parecerse a un navegador, no solo provenir de una IP nueva.

Reintentos: el fallo es el estado constante

Con un millón de solicitudes, una tasa de fallos transitorios del 1% son 10,000 páginas fallidas. El fallo no es un caso límite a este volumen, es rutinario, y la tubería debe tratar una obtención fallida como normal en lugar de fatal. Reintenta con retroceso exponencial y un límite, luego mueve la URL a una cola de mensajes muertos en lugar de bloquear la ejecución. Lee por qué falló: un tiempo de espera o 503 vale la pena reintentar, un 404 duro no.

import asyncio, random

async def fetch_with_retry(session, url, tries=4):
    for attempt in range(tries):
        try:
            async with session.get(url, timeout=20) as r:
                if r.status == 404:
                    return None                # don't retry a hard 404
                if r.status < 400:
                    return await r.text()
        except Exception:
            pass
        await asyncio.sleep(2 ** attempt + random.random())  # backoff + jitter
    await dead_letter(url)                     # give up after the cap
    return None

Deduplicación: no rastrees la misma página dos veces

El descubrimiento a escala produce duplicados constantemente: el mismo producto accesible desde tres rutas, parámetros de seguimiento que hacen que una página parezca diez. Normaliza las URLs antes de que entren en la cola, luego mantén un conjunto visto (un conjunto Redis, o un filtro Bloom una vez que el conjunto alcanza cientos de millones):

from urllib.parse import urlsplit, urlunsplit, parse_qsl, urlencode

seen = set()

def normalize(url):
    s = urlsplit(url.lower())
    q = [(k, v) for k, v in parse_qsl(s.query) if not k.startswith("utm_")]
    return urlunsplit((s.scheme, s.netloc, s.path.rstrip("/"), urlencode(sorted(q)), ""))

def enqueue(url):
    key = normalize(url)
    if key not in seen:
        seen.add(key)
        queue.put_nowait(key)

Almacenamiento y calidad de datos

Dos hábitos específicos de escala: escribe en lotes para que el almacenamiento no sea tu cuello de botella, y separa los datos en bruto de los analizados para que puedas volver a analizar sin volver a rastrear cuando cambien los selectores. Luego agrega la mitad del monitoreo que la mayoría de los equipos omiten: verificaciones de calidad de datos. Una ejecución puede reportar un 100% de éxito HTTP y aún así producir basura si el diseño se desvió y tus selectores ahora no coinciden con nada. Asegúrate de que los campos requeridos no estén vacíos y que los valores sean sensatos:

def validate(row):
    assert row.get("title"), "empty title — selector may have drifted"
    price = row.get("price")
    assert isinstance(price, (int, float)) and 0 < price < 1_000_000, "bad price"
    return row

# fail loud on page 5,000, not silently after 5,000,000 empty rows

Descarga proxies, anti-bot y renderizado a una API

Renderiza solo cuando debas

Un navegador sin cabeza es la operación más costosa en la tubería: CPU, memoria y segundos por página, que dominan todo en un millón de páginas. Muchos sitios aún envían datos en el HTML inicial o un endpoint JSON; una obtención simple más un analizador es un orden de magnitud más barato. Prueba primero el camino barato, confirma que los campos están presentes y escala al renderizado solo para las páginas que lo necesiten. Nuestro desglose de costo de renderizado versus HTTP pone números reales en esa brecha.

Construir vs comprar las capas difíciles

Todo lo anterior es construible, así que la pregunta honesta es qué partes merecen tu tiempo de ingeniería. El modelo de datos, la lógica de análisis, las verificaciones de calidad y el esquema de almacenamiento son específicos de tu proyecto, solo tú puedes construirlos bien. El grupo de proxies, el manejo anti-bot, la flota de renderizado sin cabeza y la cola de reintentos y entrega son infraestructura genérica que es costosa de construir y un trabajo arduo de mantener saludable a medida que los objetivos evolucionan. Esa es la línea en la que se sitúa una Scraper API administrada: alquila las partes que son iguales para todos. Nuestro desglose de construir versus comprar trabaja el impuesto de mantenimiento en detalle, y ejecutar raspadores como software de producción cubre mantener todo observable.

Preguntas frecuentes

¿Cómo raspas millones de páginas?

Con concurrencia, no un bucle más grande. Pon una cola entre el descubrimiento de URLs y la obtención, vacíala con un grupo de trabajadores asíncronos o distribuidos, rota IPs para evitar bloqueos, reintenta fallos transitorios con retroceso, deduplica URLs y escribe en lotes al almacenamiento. Una ejecución secuencial de un millón de páginas lleva días; el mismo trabajo a través de un grupo de trabajadores concurrentes se termina en horas.

¿Cuál es la mejor arquitectura para el raspado web a gran escala?

Una tubería de cola y trabajadores: un productor enumera URLs en una cola (Redis, RabbitMQ o Kafka), los trabajadores obtienen de manera concurrente a través de una capa de proxy rotativa, renderizan solo las páginas que necesitan JavaScript, reintentan fallos en una cola de mensajes muertos, deduplican con un conjunto visto y almacenan datos en bruto y analizados por separado. Envuélvelo en monitoreo con verificaciones de calidad de datos para que los desvíos surjan temprano.

¿Cuántas solicitudes puedes ejecutar en paralelo?

Depende del objetivo y tu egreso, no un número fijo. El raspado está limitado por IO, por lo que una caja modesta puede manejar muchos cientos de solicitudes en curso. Comienza con una concurrencia de alrededor de 50, observa la tasa de error y aumenta hasta que los fallos aumenten, ese es tu techo. Más allá de los límites de una máquina, agrega trabajadores distribuidos en lugar de presionar más un solo nodo.

¿Cómo manejas los fallos a escala?

Asume el fallo: con un millón de solicitudes, incluso una tasa de error del 1% son 10,000 páginas fallidas. Reintenta errores transitorios (tiempos de espera, 503) con retroceso exponencial y dispersión, limita los intentos y mueve los fallos persistentes a una cola de mensajes muertos en lugar de bloquear la ejecución. No reintentes 404 duros. Aleatorizar el orden de recolección también distribuye los fallos para que no falles en las mismas páginas en cada ejecución.

La escala es principalmente las partes que no son divertidas de construir: rotación de IPs, anti-bot, renderizado sin cabeza, colas y reintentos. Posee las piezas específicas de tus datos: el modelo, los analizadores, las verificaciones de calidad, y alquila la infraestructura genérica que es el mismo trabajo arduo para todos. Obtén la cola y la concurrencia correctas primero; todo lo demás es hacer que esa concurrencia sobreviva al contacto con un millón de páginas reales.

Impulsa la capa de obtención con IPs residenciales rotativas