Construir un rastreador web distribuido en Node.js: Colas y deduplicación
Senior Web Scraping Engineer
TL;DR:
- Un rastreador web distribuido necesita una frontera de URL duradera, trabajadores sin estado, claves de URL canónicas, presupuestos de solicitud por host y un camino de cuarentena terminal explícito.
- Mantén a Node.js responsable de descubrimiento, programación, estado y almacenamiento. Delegar la renderización de JavaScript y la adquisición de páginas a una capa de ejecución gestionada cuando la fuente lo requiera.
- Usa el hash de la URL canónica tanto como el ID del trabajo en la cola como la clave de idempotencia del almacenamiento. Esto bloquea el trabajo duplicado antes de que llegue a un trabajador.
- La concurrencia global de trabajadores y la cadencia por host resuelven problemas diferentes. Escala el primero con capacidad; establece el segundo a partir del permiso de la fuente y el comportamiento observado del servidor.
- Comienza con un pequeño conjunto de fuentes autorizadas, mide las páginas aceptadas en lugar de las intentadas y escala solo después de que la profundidad de la cola, el retraso de frescura y el rechazo del esquema sean visibles.
Un rastreador de un solo proceso falla de maneras predecibles: su cola en memoria desaparece al reiniciar, los enlaces duplicados se multiplican, un host lento ocupa el bucle de eventos y la ejecución del navegador consume la misma máquina que debería programar el trabajo.
Un rastreador web distribuido separa esas responsabilidades. Node.js posee el plano de control: la frontera de la URL, el estado del trabajo, la deduplicación y las decisiones de almacenamiento. Los trabajadores independientes poseen la ruta de datos. Para las páginas públicas renderizadas en JavaScript, un servicio gestionado puede ejecutar la página y devolver Markdown o HTML sin colocar procesos de navegador dentro de cada contenedor de trabajador.
Define los requisitos y modos de fallo
Antes de elegir una cola, escribe el contrato de rastreo:
- ¿Cuáles dominios y rutas están autorizados?
- ¿Cuántas páginas y niveles puede descubrir cada rastreo?
- ¿Qué ventana de frescura necesita cada fuente?
- ¿Qué formatos de respuesta y campos requeridos definen una página aceptada?
- ¿Qué ritmo de solicitud se permite por host?
- ¿A dónde van los resultados inválidos, vacíos o inesperados?
- ¿Cuánto tiempo deben retenerse las versiones de trabajos y páginas?
La primera versión también debe tener condiciones de detención: profundidad máxima, páginas aceptadas máximas, URL descubiertas máximas y una fecha límite. Estos límites previenen que un archivo de calendario, navegación facetada o un parámetro de seguimiento conviertan un pequeño rastreo en una caminata gráfica ilimitada.
Los modos de fallo comunes son arquitectónicos:
| Fallo | Causa raíz | Control |
|---|---|---|
| Frontera perdida | Cola en memoria | Trabajos duraderos respaldados por Redis |
| Páginas duplicadas | URLs sin procesar usadas como claves | Hash de URL canónica |
| Un host sobrecargado | Solo se estableció la concurrencia global | Cola y ritmo por host |
| Contenido vacío almacenado | Éxito de transporte tratado como éxito de datos | Contrato de aceptación de contenido |
| Los trabajadores no pueden escalar | Estado del navegador local | Trabajadores sin estado y ejecución gestionada |
| Trabajos envenenados ciclan indefinidamente | Sin estado terminal | Cola de cuarentena con liberación manual |
| El rastreo parece saludable pero es obsoleto | Solo se mide el rendimiento | Retraso de frescura y métricas de páginas aceptadas |
Separar el plano de control de la ejecución de la página
El rastreador puede representarse como dos planos:
Plano de control: semillas → normalización de URL → deduplicación → colas de Redis → estado del trabajo → metadatos de almacenamiento
Plano de ejecución: trabajador → adquisición de página → validación de contenido → extracción de enlaces → registro aceptado o cuarentena
Esta frontera es importante porque la programación y la ejecución del navegador escalan de manera diferente. Las operaciones de cola son pequeñas y con estado. La ejecución de páginas es intensiva en red y puede requerir JavaScript, enrutamiento regional o una sesión de navegador aislada.
Scrapeless Crawl admite la colección de una sola página, por lotes y de sitios enlazados con formatos que incluyen Markdown, HTML, enlaces, metadatos y capturas de pantalla. Cuando la ruta de ejecución necesita un navegador interactivo, el Scrapeless Scraping Browser mantiene ese tiempo de ejecución fuera del contenedor del trabajador. La aplicación Node puede seguir siendo el sistema de registro para el alcance y el estado mientras que Scrapeless maneja la adquisición.
Para una introducción conceptual al descubrimiento y la extracción, lee qué es un rastreador web. El diseño a continuación comienza donde un rastreador de máquina única se detiene: colas compartidas, encolado idempotente y múltiples trabajadores.
Construir la frontera de URL respaldada por Redis
BullMQ implementa la ejecución de trabajos distribuida en Redis. Su documentación oficial cubre colas, trabajadores, eventos, trabajos retrasados, limitación de tasa y estado del trabajo.
La frontera debe almacenar una pequeña carga útil de trabajo:
| Campo | Propósito |
|---|---|
url |
URL canónica a recolectar |
host |
Partición de cola y búsqueda de política |
depth |
Límite de descubrimiento |
crawlId |
Correlación y cancelación |
parentUrl |
Procedencia para descubrimiento |
schemaVersion |
Contrato aguas abajo |
No coloques el HTML de la página en Redis. Almacena resultados grandes en un almacén de objetos o base de datos y mantiene solo identificadores, estados y metadatos compactos en la cola.
Utiliza una cola separada por host aprobado cuando diferentes dominios requieran diferentes presupuestos de solicitud. Los trabajadores asignados a docs-example-com pueden usar entonces un limitador, mientras que otro host obtiene su propio ritmo. Una cola global es más simple, pero su limitador no puede expresar políticas independientes por host.
Haz que la inserción en cola sea idempotente
Dos páginas pueden ser equivalentes mientras que sus URL originales difieran:
- un fragmento apunta a una ubicación dentro del mismo documento;
- los parámetros de seguimiento cambian sin cambiar el contenido;
- los parámetros de consulta aparecen en un orden diferente;
- los puertos por defecto o las barras finales varían;
- los enlaces relativos se resuelven a la misma página absoluta.
Normaliza antes de la inserción en cola. El Estándar de URL de WHATWG define el modelo de análisis implementado por la clase URL de Node.js.
Una política segura es específica de la fuente. Eliminar cada parámetro de consulta puede fusionar páginas genuinamente diferentes. Mantén una lista blanca o negra para parámetros conocidos y preserva cualquier parámetro que cambie el recurso.
Después de la normalización, haz un hash de la URL con SHA-256. Utiliza ese digest hexadecimal como:
- el ID del trabajo de BullMQ;
- la clave upsert de la base de datos;
- el enlace de procedencia entre el trabajo y la página almacenada.
BullMQ ignora un nuevo trabajo cuando otro trabajo con el mismo ID ya existe en esa cola. Los registros de trabajos eliminados por retención ya no proporcionan esa protección, por lo que el almacenamiento duradero debe mantener la misma clave de idempotencia.
Proyecto Node.js mínimo con versión fija
Este proyecto es intencionalmente compacto:
distributed-crawler/
├── package.json
└── src/
└── crawler.mjs
Las versiones de las dependencias se verificaron en sus registros de paquetes en el momento de la redacción. El ejemplo es un bloque de brecha de prerequisito: requiere Node.js, Redis, una SCRAPELESS_API_KEY y un nombre de host público autorizado establecido en ALLOWED_HOST. Está diseñado para un solo host, por lo que el limitador de la cola es realmente específico del host.
json
{
"name": "distributed-crawler-example",
"private": true,
"type": "module",
"scripts": {
"start": "node src/crawler.mjs"
},
"dependencies": {
"@scrapeless-ai/sdk": "1.3.1",
"bullmq": "5.79.3"
},
"engines": {
"node": ">=22"
}
}
El trabajador a continuación tiene cuatro resultados terminales: aceptado, sin cambios, rechazado y en cuarentena. No crea un bucle de falla automático. Un operador puede inspeccionar el registro de la cuarentena, arreglar su causa y volver a poner en cola la URL canónica.
javascript
import { createHash } from "node:crypto";
import { Queue, Worker } from "bullmq";
import { ScrapingCrawl } from "@scrapeless-ai/sdk";
const redis = {
host: process.env.REDIS_HOST ?? "127.0.0.1",
port: Number(process.env.REDIS_PORT ?? 6379)
};
const allowedHost = process.env.ALLOWED_HOST ?? "example.com";
const queueName = `crawl-${hostKey(allowedHost)}`;
const frontier = new Queue(queueName, { connection: redis });
const quarantine = new Queue(`${queueName}-quarantine`, { connection: redis });
const crawl = new ScrapingCrawl({
apiKey: process.env.SCRAPELESS_API_KEY
});
function sha256(value) {
return createHash("sha256").update(value).digest("hex");
}
function hostKey(host) {
return host.toLowerCase().replaceAll(".", "-");
}
function canonicalize(input) {
const url = new URL(input);
url.hash = "";
url.hostname = url.hostname.toLowerCase();
for (const key of ["utm_source", "utm_medium", "utm_campaign"]) {
url.searchParams.delete(key);
}
url.searchParams.sort();
return url.href;
}
async function enqueue(url, crawlId, depth = 0, parentUrl = null) {
const canonicalUrl = canonicalize(url);
const parsed = new URL(canonicalUrl);
if (parsed.hostname !== allowedHost) {
throw new Error(`Host fuera del ámbito de rastreo: ${parsed.hostname}`);
}
const key = sha256(canonicalUrl);
await frontier.add(
"collect-page",
{
url: canonicalUrl,
host: parsed.hostname,
depth,
crawlId,
parentUrl,
schemaVersion: "crawl-page-v1"
},
{
jobId: key,
removeOnComplete: 1000,
removeOnFail: false
}
);
return key;
}
const worker = new Worker(
queueName,
async (job) => {
try {
const result = await crawl.scrapeUrl(job.data.url, {
formats: ["markdown", "links"],
onlyMainContent: true,
timeout: 15000
});
const markdown = result.markdown ?? result.data?.markdown;
if (typeof markdown !== "string" || markdown.length < 200) {
return { state: "rechazado", reason: "contrato-de-contenido", url: job.data.url };
}
const record = {
key: job.id,
url: job.data.url,
crawlId: job.data.crawlId,
depth: job.data.depth,
contentHash: sha256(markdown),
markdown
};
console.log(JSON.stringify({ state: "accepted", ...record }));
return { state: "accepted", key: record.key, contentHash: record.contentHash };
} catch (error) {
await quarantine.add("inspect-page", {
...job.data,
sourceJobId: job.id,
reason: error instanceof Error ? error.message : "desconocido"
});
return { state: "quarantined", key: job.id };
}
},
{
connection: redis,
concurrency: 4,
limiter: { max: 2, duration: 1000 }
}
);
worker.on("completed", (job, result) => {
console.log(JSON.stringify({ event: "completed", jobId: job.id, result }));
});
worker.on("failed", (job, error) => {
console.error(JSON.stringify({
event: "worker-failed",
jobId: job?.id,
message: error.message
}));
});
await enqueue(`https://${allowedHost}/`, "demo-crawl");
El ejemplo imprime registros aceptados para hacer visible el contrato de datos. Reemplace console.log con un upsert de base de datos indexado por record.key. Compare contentHash con el valor almacenado antes de escribir una nueva versión de página o reconstruir un índice.
Entender la máquina de estados de la tarea
Un rastreador necesita un modelo de estado que los operadores puedan explicar:
| Estado | Significado | Próxima acción |
|---|---|---|
queued |
La URL canónica está esperando | El trabajador la reclama |
active |
Un trabajador posee el alquiler | Adquirir y validar |
accepted |
El contenido pasó el contrato | Almacenar, indexar, descubrir enlaces |
unchanged |
El hash del contenido coincide con la versión almacenada | Actualizar los metadatos de frescura |
rejected |
La respuesta se completó pero el contenido es inválido | Revisar validador o fuente |
quarantined |
La ejecución no pudo producir una decisión | Inspeccionar y liberar manualmente |
cancelled |
El alcance de rastreo o el plazo terminó | Retener metadatos de auditoría |
Mantenga failed como un evento de infraestructura informado por la cola, no como el único estado comercial. Un trabajo que devuelve un shell de aplicación vacío se completa técnicamente pero debería ser rejected. Una página fuera de alcance debería ser cancelled antes de la adquisición. Estas distinciones hacen que los paneles de control sean accionables.
Controlar la concurrencia por host
La concurrencia de trabajadores responde: “¿Cuántos trabajos puede manejar este proceso?” Un presupuesto de solicitudes por host responde: “¿Cuánto tráfico puede recibir este origen?” Deben configurarse de forma independiente.
Para un dominio autorizado:
- Leer las reglas de robots y los límites contractuales.
- Establecer un ritmo conservador por host.
- Ejecutar múltiples trabajadores solo cuando la cola y el presupuesto del host lo permitan.
- Rastrear el estado de la respuesta, la aceptación del contenido y la latencia del servidor.
- Reducir el presupuesto del host cuando la fuente muestra angustia o el acuerdo cambia.
Los trabajadores de BullMQ pueden compartir una cola a través de procesos y máquinas. El limitador de cola coordina la cola seleccionada, por eso el ejemplo usa una cola específica para el host. Para muchos dominios, genere colas a partir de un registro aprobado y limite el número de objetos de trabajadores activos.
El Protocolo de Exclusión de Robots está estandarizado por RFC 9309. Las reglas de robots no son un otorgamiento de permiso y no reemplazan los términos del sitio, los deberes de privacidad o la ley aplicable.
Delegar la adquisición de páginas sin perder control
El plano de control de Node.js debe decidir qué puede ser recolectado. La capa de ejecución debe decidir cómo obtener la representación de página permitida.
El Scrapeless Crawl quickstart documenta el estado de rastreo asíncrono y los resultados a nivel de página. Para páginas que requieren interacción más amplia o ejecución de JavaScript, use las opciones del navegador de Crawl mientras la cola mantiene el alcance, la identidad del trabajo y las decisiones de almacenamiento.
Valide el contenido devuelto en lugar de asumir que el ejecutor tomó la decisión comercial. Exija texto esperado, campos, idioma, URL y contenido mínimo. Almacene la ruta de adquisición en la proveniencia para que una auditoría posterior pueda explicar cómo cada página ingresó al conjunto de datos.
Descubrir enlaces sin escapar del alcance
El descubrimiento de enlaces pertenece después de la aceptación del contenido. Analice solo las páginas que cumplieron con el contrato de contenido, luego aplique estos filtros antes de encolar:
- nombre de host permitido;
- prefijos de ruta permitidos;
- esquemas HTTP compatibles;
- profundidad máxima y recuento de páginas;
- canonicalización y búsqueda de ID de trabajo;
- exclusiones por tipo de archivo;
- política de parámetros de consulta específica de la fuente.
No permita que las redirecciones amplíen silenciosamente el conjunto de hosts permitidos. Registre la URL final, compárela con el alcance y rechace los resultados entre dominios a menos que el registro de origen los autorice explícitamente.
Para los sitemaps, trate cada URL como entrada descubierta en lugar de salida confiable. Canonícelas, filtre y elimine duplicados a través del mismo camino de frontera que un enlace HTML.
Almacenar versiones de páginas y proveniencia de rastreo
Un modelo de almacenamiento útil tiene tres registros:
- Rastreo: alcance, URLs semilla, fecha límite, versión de política y estado general.
- Página: clave canónica, URL de origen, hash aceptado más reciente, tiempo de colección y versión de esquema.
- Versión de página: hash de contenido, ubicación de carga útil, metadatos y procedencia de adquisición.
La cola no es la base de datos a largo plazo. Las políticas de limpieza de trabajos pueden eliminar entradas completadas, mientras que la tabla de páginas debe mantener claves idempotentes e historial de versiones de acuerdo con la política de retención.
Si una página no cambia, actualiza la marca de tiempo de frescura sin duplicar la carga útil. Si cambia, escribe una nueva versión inmutable de la página, apunta el registro de la página a ella y notifica al indexado en cadena a través de un evento separado.
Observar el rastreo
La longitud de la cola por sí sola puede ser engañosa. Un rastreador puede vaciar su frontera mientras rechaza cada página. Rastrear:
| Señal | Pregunta respondida |
|---|---|
| Páginas aceptadas por minuto | ¿Está llegando datos útiles? |
| Retraso de descubrimiento a aceptación | ¿Qué tan desactualizado está el pipeline? |
| Tasa de duplicados en cola | ¿Es efectiva la canonicalización? |
| Tasa de rechazo por razón | ¿Cambió el marcado de origen o la validación? |
| Edad de cuarentena | ¿Está acumulando deuda operativa? |
| Ritmo de solicitudes al host | ¿Se está siguiendo la política? |
| Tasa de cambio de contenido | ¿Es adecuado el programa de actualización? |
| Percentil de edad de la cola | ¿Es suficiente la capacidad del trabajador? |
OpenTelemetry describe trazas, métricas, registros y equipaje en su guía de señales de telemetría. Utiliza un ID de rastreo en todos los eventos de productor, cola, llamada de adquisición, escritura en almacenamiento y eventos de índice en cadena.
Alerta sobre datos aceptados desactualizados y entradas antiguas en cuarentena, no solo sobre fallos de proceso. Un proceso puede ser saludable mientras el conjunto de datos deja de cambiar silenciosamente.
Lista de verificación de implementación
- Ejecuta Redis con persistencia, autenticación, controles de red y copias de seguridad apropiadas para la carga de trabajo.
- Mantén a los trabajadores sin estado y despliega la misma imagen en todas las instancias.
- Almacena las claves de API en un gestor de secretos o inyección de entorno, nunca en las cargas útiles de los trabajos.
- Define la retención de la cola por separado de la retención de las páginas.
- Limita la profundidad del rastreo, las páginas, el tiempo y el alcance del host.
- Utiliza una fuente de política de host compartida por productores y trabajadores.
- Drena los trabajadores durante la implementación para que los arrendamientos activos no se abandonen.
- Prueba la cancelación, la liberación de cuarentena, la idempotencia de almacenamiento y los cambios de política de origen.
- Registra las versiones de Node.js, BullMQ, el SDK de Scrapeless y el esquema de página.
Comienza con un host aprobado y un límite pequeño de páginas. Agrega dominios solo después de que los tableros muestren que los datos aceptados, la frescura y la política de solicitudes permanecen dentro de sus objetivos.
Conclusión: escalar la frontera, no la incertidumbre
Un rastreador web distribuido se vuelve confiable cuando cada URL tiene una identidad canónica, cada host tiene un presupuesto explícito y cada página termina en un estado significativo. Node.js y BullMQ pueden poseer la frontera y la coordinación de trabajadores; Scrapeless puede encargarse de la ejecución de páginas donde se requieren renderización gestionada y manejo de red.
Crea una prueba de concepto acotada con un dominio público autorizado, verifica la página de precios de Scrapeless, luego crea una cuenta de Scrapeless. Enruta la adquisición a través de Crawl y mide las páginas aceptadas y la frescura antes de aumentar el número de trabajadores.
Preguntas Frecuentes
¿Qué hace que un rastreador web sea distribuido?
Su frontera de URL y estado de trabajo se comparten entre múltiples procesos o máquinas de trabajo. Los trabajadores pueden reclamar trabajos independientes, escribir resultados en almacenamiento compartido y escalar sin depender de la memoria de un proceso.
¿Por qué usar BullMQ para un rastreador Node.js?
BullMQ proporciona colas respaldadas por Redis, trabajadores distribuidos, controles de concurrencia, eventos e identificadores de trabajos. El rastreador aún necesita su propia política de URL, contrato de contenido, almacenamiento duradero y observabilidad.
¿Cómo funciona la deduplicación de URL entre trabajadores?
Normaliza la URL antes de encolarla, hashea la forma canónica y utiliza el digest como el ID de trabajo de la cola y la clave de almacenamiento. Redis coordina la creación de trabajos, mientras que la base de datos preserva la idempotencia después de que la retención de la cola elimina trabajos antiguos.
¿Debería cada trabajador iniciar su propio navegador?
No necesariamente. Los navegadores locales aumentan el tamaño del contenedor, el uso de memoria y el trabajo operativo. Una capa de adquisición gestionada puede devolver contenido renderizado mientras los trabajadores se centran en la programación, validación, descubrimiento y almacenamiento.
¿Cómo se deben manejar los trabajos de página fallidos?
Sepa separar los resultados comerciales de los eventos de infraestructura. El contenido no válido puede ser rechazado; errores de ejecución no resueltos pueden entrar en una cola de cuarentena para inspección y liberación explícita. Evita un bucle automático sin límites.
En Scrapeless, solo accedemos a datos disponibles públicamente y cumplimos estrictamente con las leyes, regulaciones y políticas de privacidad del sitio web aplicables. El contenido de este blog es sólo para fines de demostración y no implica ninguna actividad ilegal o infractora. No ofrecemos garantías y renunciamos a toda responsabilidad por el uso de la información de este blog o enlaces de terceros. Antes de realizar cualquier actividad de scraping, consulte a su asesor legal y revise los términos de servicio del sitio web de destino u obtenga los permisos necesarios.



