Motor de Extracción Multimedia con Cheerio y Node.js: Web Scraping de Alto Rendimiento y Resiliente

JAVASCRIPT 7 de abril de 2026 133 lecturas
Construye un motor de extracción de recursos multimedia con Cheerio y Node.js, optimizado para evitar bloqueos por lazy loading y gestionar URLs relativas con tolerancia a fallos.

El Desafío Estratégico del Web Scraping Multimedia en el Servidor

En el desarrollo de productos digitales modernos, la necesidad de indexar, auditar o migrar activos multimedia desde múltiples fuentes externas es una constante. Sin embargo, intentar ejecutar este proceso directamente desde la interfaz web del cliente (frontend) desencadena de inmediato barreras de seguridad insuperables, principalmente por las restricciones impuestas por las políticas de Intercambio de Recursos de Origen Cruzado (CORS) y las políticas de seguridad de contenido (CSP). Trasladar el procesamiento al servidor no solo es la mejor práctica de arquitectura, sino la única alternativa viable para construir pipelines de datos robustos y escalables.

Durante años, la respuesta por defecto para el raspado de datos ha sido el empleo de navegadores headless impulsados por motores como Puppeteer, Playwright o Selenium. Aunque estas herramientas son indispensables cuando se requiere la ejecución de scripts del cliente (JavaScript dinámico) o la interacción con aplicaciones de página única (SPA), representan un coste prohibitivo en términos de infraestructura. Levantar una instancia Chromium para extraer únicamente URLs de imágenes o videos consume cientos de megabytes de memoria RAM y genera picos severos de utilización de CPU. Cuando la meta es procesar miles de páginas por minuto, la arquitectura basada en navegadores sin cabeza se vuelve insostenible y financieramente ineficiente.

Para resolver este cuello de botella, he diseñado un motor de extracción multimedia ultrarrápido respaldado por Cheerio sobre el runtime de Node.js. Cheerio implementa un subconjunto optimizado del núcleo de jQuery sobre el parseador de HTML htmlparser2, lo que le permite manipular y recorrer la estructura jerárquica del Document Object Model (DOM) directamente en memoria, sin el overhead de rendering visual, pintado de layouts o evaluación de scripts de cliente. El resultado es un procesamiento que ejecuta operaciones en una fracción de milisegundo por documento.

Arquitectura del Motor y Desafíos Técnicos de Implementación

La construcción de un Harvester (cosechador) de medios eficiente requiere ir más allá de la simple búsqueda de etiquetas <img> o <video>. En el desarrollo de esta solución, he enfrentado y resuelto cuatro retos técnicos fundamentales que suelen arruinar los sistemas de extracción en entornos de producción:


Casos de Uso Empresariales y Escalabilidad

Este motor constituye la columna vertebral para una variedad de soluciones de software de clase empresarial:


A continuación, presento la implementación completa y optimizada con soporte para TypeScript / JSDoc, manejo defensivo de excepciones y desduplicación inteligente.

import * as cheerio from 'cheerio';

/**
 * @typedef {Object} MediaItem
 * @property {string} url
 * @property {string} alt
 */

/**
 * @typedef {Object} ExtractedMedia
 * @property {MediaItem[]} images
 * @property {string[]} videos
 * @property {string[]} audios
 */

/**
 * Resuelve URLs relativas y maneja errores de parseo de forma segura.
 * @param {string|undefined} relativeOrAbsoluteUrl 
 * @param {string} baseUrl 
 * @returns {string|null}
 */
const safeResolveUrl = (relativeOrAbsoluteUrl, baseUrl) => {
  if (!relativeOrAbsoluteUrl || typeof relativeOrAbsoluteUrl !== 'string') return null;
  
  const cleanUrl = relativeOrAbsoluteUrl.trim();
  if (cleanUrl.startsWith('data:')) return cleanUrl;

  try {
    return new URL(cleanUrl, baseUrl).href;
  } catch {
    return null;
  }
};

/**
 * Parsea cadenas srcset para obtener la URL de mejor candidato.
 * @param {string|undefined} srcset 
 * @returns {string|null}
 */
const parseSrcset = (srcset) => {
  if (!srcset) return null;
  const candidates = srcset.split(',').map((item) => item.trim().split(' ')[0]);
  return candidates.pop() || null;
};

/**
 * Motor Principal de Extracción Multimedia
 * @param {string} html - Documento HTML crudo.
 * @param {string} baseUrl - URL base para resolver rutas relativas.
 * @returns {ExtractedMedia}
 */
export const extractMedia = (html, baseUrl) => {
  if (!html || typeof html !== 'string') {
    return { images: [], videos: [], audios: [] };
  }

  const $ = cheerio.load(html);
  
  const images = [];
  const imageSet = new Set();
  const videoSet = new Set();
  const audioSet = new Set();

  // 1. Extracción de Imágenes con soporte de Lazy Loading y srcset
  $('img').each((_, el) => {
    const node = $(el);
    const rawSrc = 
      node.attr('src') || 
      node.attr('data-src') || 
      node.attr('data-lazy-src') || 
      node.attr('data-original') ||
      parseSrcset(node.attr('srcset'));

    const resolvedUrl = safeResolveUrl(rawSrc, baseUrl);

    if (resolvedUrl && !imageSet.has(resolvedUrl)) {
      imageSet.add(resolvedUrl);
      images.push({
        url: resolvedUrl,
        alt: node.attr('alt')?.trim() || 'Sin descripción'
      });
    }
  });

  // 2. Extracción de Videos (Evita duplicados con Set)
  $('video, video source').each((_, el) => {
    const rawSrc = $(el).attr('src');
    const resolvedUrl = safeResolveUrl(rawSrc, baseUrl);

    if (resolvedUrl) {
      videoSet.add(resolvedUrl);
    }
  });

  // 3. Extracción de Audios
  $('audio, audio source').each((_, el) => {
    const rawSrc = $(el).attr('src');
    const resolvedUrl = safeResolveUrl(rawSrc, baseUrl);

    if (resolvedUrl) {
      audioSet.add(resolvedUrl);
    }
  });

  return {
    images,
    videos: Array.from(videoSet),
    audios: Array.from(audioSet)
  };
};
¿Qué te pareció?
🔥 Brillante 0
💡 Me sirvió 0
🚀 A otro nivel 0

¿Te resultó útil este snippet? Explora más código y soluciones en AndresSY.dev.

Volver a Snippets