Construyendo un Web Scraper con Puppeteer en Docker en la Plataforma de Aplicaciones de DigitalOcean

Como entusiasta de los ultra maratones, a menudo enfrento un desafío común: ¿cómo puedo estimar mi tiempo de finalización para carreras más largas que aún no he intentado? Al discutir esto con mi entrenador, sugirió un enfoque práctico: mirar a los corredores que han completado tanto una carrera que he hecho como la carrera que estoy apuntando. Esta correlación podría proporcionar información valiosa sobre los posibles tiempos de finalización. Pero buscar manualmente entre los resultados de las carreras sería increíblemente que lleva mucho tiempo.

Esto me llevó a construir Race Time Insights, una herramienta que compara automáticamente los resultados de las carreras al encontrar atletas que han completado ambos eventos. La aplicación extrae resultados de carreras de plataformas como UltraSignup y Pacific Multisports, permitiendo a los corredores ingresar dos URL de carreras y ver cómo se desempeñaron otros atletas en ambos eventos.

Construir esta herramienta me mostró cuán poderosa podría ser la App Platform de DigitalOcean. Usando Puppeteer con Chrome sin cabeza en contenedores Docker, pude concentrarme en resolver el problema para los corredores mientras que App Platform manejaba toda la complejidad de la infraestructura. El resultado fue una solución robusta y escalable que ayuda a la comunidad de corredores a tomar decisiones basadas en datos sobre sus objetivos de carrera.

Después de construir Race Time Insights, quería crear una guía que mostrara a otros desarrolladores cómo aprovechar estas mismas tecnologías: Puppeteer, contenedores Docker y la App Platform de DigitalOcean. Por supuesto, al trabajar con datos externos, debes tener en cuenta cosas como la limitación de tasas y los términos de servicio.

Entra en Project Gutenberg. Con su vasta colección de libros de dominio público y términos de servicio claros, es un candidato ideal para demostrar estas tecnologías. En esta publicación, exploraremos cómo construir una aplicación de búsqueda de libros utilizando Puppeteer en un contenedor Docker, desplegada en App Platform, mientras seguimos las mejores prácticas para el acceso a datos externos.

He construido y compartido una aplicación web que raspa información de libros de Project Gutenberg de manera responsable. La aplicación, que puedes encontrar en este repositorio de GitHub, permite a los usuarios buscar entre miles de libros de dominio público, ver información detallada sobre cada libro y acceder a varios formatos de descarga. Lo que hace que esto sea particularmente interesante es cómo demuestra prácticas responsables de web scraping mientras proporciona un valor genuino a los usuarios.

Siendo un Buen Ciudadano Digital

Al construir un raspador web, es crucial seguir buenas prácticas y respetar tanto los límites técnicos como legales. Project Gutenberg es un excelente ejemplo para aprender estos principios porque:

  1. Tiene términos de servicio claros.
  2. Proporciona directrices de robots.txt
  3. Su contenido está explícitamente en dominio público
  4. Se beneficia de una mayor accesibilidad a sus recursos

Nuestra implementación incluye varias mejores prácticas:

Limitación de velocidad

Para fines de demostración, implementamos un limitador de velocidad simple que asegura al menos 1 segundo entre solicitudes:

// Una implementación ingenua de limitación de velocidad
const rateLimiter = {
    lastRequest: 0,
    minDelay: 1000, // 1 segundo entre solicitudes
    async wait() {
        const now = Date.now();
        const timeToWait = Math.max(0, this.lastRequest + this.minDelay - now);
        if (timeToWait > 0) {
            await new Promise(resolve => setTimeout(resolve, timeToWait));
        }
        this.lastRequest = Date.now();
    }
};

Esta implementación está intencionalmente simplificada para el ejemplo. Supone una única instancia de aplicación y almacena el estado en memoria, lo cual no sería adecuado para uso en producción. Soluciones más robustas podrían usar Redis para limitación de velocidad distribuida o implementar sistemas basados en colas para una mejor escalabilidad.

Este limitador de velocidad se utiliza antes de cada solicitud a Project Gutenberg:

async searchBooks(query, page = 1) {
    await this.initialize();
    await rateLimiter.wait();  // Aplicar límite de velocidad
    // ... resto de la lógica de búsqueda
}

async getBookDetails(bookUrl) {
    await this.initialize();
    await rateLimiter.wait();  // Aplicar límite de velocidad
    // ... resto de la lógica de detalles
}

Identificación Clara de Bots

Un User-Agent personalizado ayuda a los administradores de sitios web a entender quién está accediendo a su sitio y por qué. Esta transparencia les permite:

  1. Contactarte si hay problemas
  2. Monitorear y analizar el tráfico de bots por separado de los usuarios humanos
  3. Proporcionar potencialmente mejor acceso o soporte para raspadores legítimos
await browserPage.setUserAgent('GutenbergScraper/1.0 (Educational Project)');

Gestión Eficiente de Recursos

Chrome puede ser intensivo en memoria, especialmente al ejecutar múltiples instancias. Cerrar correctamente las páginas del navegador después de usarlas previene fugas de memoria y asegura que tu aplicación funcione de manera eficiente, incluso al manejar muchas solicitudes:

try {
    // ... lógica de raspado
} finally {
    await browserPage.close();  // Liberar memoria y recursos del sistema
}

Al seguir estas prácticas, creamos un raspador que es tanto efectivo como respetuoso con los recursos a los que accede. Esto es particularmente importante al trabajar con valiosos recursos públicos como el Proyecto Gutenberg.

Raspado Web en la Nube

La aplicación aprovecha la arquitectura moderna en la nube y la contenedorización a través de la Plataforma de Aplicaciones de DigitalOcean. Este enfoque proporciona un equilibrio perfecto entre la simplicidad en el desarrollo y la fiabilidad en producción.

El Poder de la Plataforma de Aplicaciones

La Plataforma de Aplicaciones simplifica el proceso de despliegue al manejar:

  • Configuración del servidor web
  • Gestión de certificados SSL
  • Actualizaciones de seguridad
  • Balanceo de carga
  • Monitoreo de recursos

Esto nos permite centrarnos en el código de la aplicación mientras la Plataforma de Aplicaciones gestiona la infraestructura.

Chrome Sin Cabeza en un Contenedor

El núcleo de nuestra funcionalidad de scraping utiliza Puppeteer, que proporciona una API de alto nivel para controlar Chrome programáticamente. Así es como configuramos y usamos Puppeteer en nuestra aplicación:

const puppeteer = require('puppeteer');

class BookService {
    constructor() {
        this.baseUrl = 'https://www.gutenberg.org';
        this.browser = null;
    }

    async initialize() {
        if (!this.browser) {
            // Agregar registro de información del entorno para depuración
            console.log('Environment details:', {
                PUPPETEER_EXECUTABLE_PATH: process.env.PUPPETEER_EXECUTABLE_PATH,
                CHROME_PATH: process.env.CHROME_PATH,
                NODE_ENV: process.env.NODE_ENV
            });

            const options = {
                headless: 'new',
                args: [
                    '--no-sandbox',
                    '--disable-setuid-sandbox',
                    '--disable-dev-shm-usage',
                    '--disable-gpu',
                    '--disable-extensions',
                    '--disable-software-rasterizer',
                    '--window-size=1280,800',
                    '--user-agent=GutenbergScraper/1.0 (+https://github.com/wadewegner/doappplat-puppeteer-sample) Chromium/120.0.0.0'
                ],
                executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || '/usr/bin/chromium-browser',
                defaultViewport: {
                    width: 1280,
                    height: 800
                }
            };

            this.browser = await puppeteer.launch(options);
        }
    }

    // Ejemplo de scraping con Puppeteer
    async searchBooks(query, page = 1) {
        await this.initialize();
        await rateLimiter.wait();

        const browserPage = await this.browser.newPage();
        try {
            // Establecer encabezados para imitar un navegador real e identificar nuestro bot
            await browserPage.setExtraHTTPHeaders({
                'Accept-Language': 'en-US,en;q=0.9',
                'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
                'Connection': 'keep-alive',
                'Upgrade-Insecure-Requests': '1',
                'X-Bot-Info': 'GutenbergScraper - A tool for searching Project Gutenberg'
            });

            const searchUrl = `${this.baseUrl}/ebooks/search/?query=${encodeURIComponent(query)}&start_index=${(page - 1) * 24}`;
            await browserPage.goto(searchUrl, { waitUntil: 'networkidle0' });
            
            // ... resto de la lógica de búsqueda
        } finally {
            await browserPage.close();  // Siempre limpiar
        }
    }
}

Esta configuración nos permite:

  • Ejecutar Chrome en modo sin cabeza (sin GUI necesaria)
  • Ejecutar JavaScript en el contexto de páginas web
  • Manejar de manera segura los recursos del navegador
  • Trabajar de manera confiable en un entorno en contenedores

La configuración también incluye varias configuraciones importantes para ejecutarse en un entorno en contenedores:

  1. Argumentos de Chrome adecuados: Banderas esenciales como --no-sandbox y --disable-dev-shm-usage para ejecutarse en contenedores
  2. Ruta consciente del entorno: Utiliza la ruta binaria correcta de Chrome desde las variables de entorno
  3. Gestión de recursos: Establece el tamaño de la ventana de visualización y desactiva características innecesarias
  4. Identidad profesional del bot: Agente de usuario claro y encabezados HTTP que identifican nuestro scraper
  5. Manejo de Errores: Limpieza adecuada de las páginas del navegador para prevenir fugas de memoria

Mientras que Puppeteer facilita el control de Chrome programáticamente, ejecutarlo en un contenedor requiere las dependencias y configuraciones del sistema adecuadas. Veamos cómo configuramos esto en nuestro entorno Docker.

Docker: Asegurando Entornos Consistentes

Uno de los mayores desafíos al desplegar raspadores web es asegurarse de que funcionen de la misma manera en desarrollo y producción. Tu raspador puede funcionar perfectamente en tu máquina local pero fallar en la nube debido a dependencias faltantes o diferentes configuraciones del sistema. Docker soluciona esto empaquetando todo lo que la aplicación necesita – desde Node.js hasta Chrome mismo – en un único contenedor que se ejecuta de manera idéntica en todas partes.

Nuestro Dockerfile establece este entorno consistente:

FROM node:18-alpine

# Instalar Chromium y dependencias
RUN apk add --no-cache \
    chromium \
    nss \
    freetype \
    harfbuzz \
    ca-certificates \
    ttf-freefont \
    dumb-init

# Establecer variables de entorno
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true \
    PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser \
    PUPPETEER_DISABLE_DEV_SHM_USAGE=true

La imagen basada en Alpine mantiene nuestro contenedor ligero mientras incluye todas las dependencias necesarias. Cuando ejecutas este contenedor, ya sea en tu laptop o en la App Platform de DigitalOcean, obtienes el mismo entorno exacto con todas las versiones y configuraciones correctas para ejecutar Chrome sin cabeza.

Desarrollo a Despliegue

Vamos a repasar cómo poner este proyecto en marcha:

1. Desarrollo Local

Primero, bifurca el repositorio de ejemplo a tu cuenta de GitHub. Esto te da tu propia copia para trabajar y desplegar. Luego clona tu bifurcación localmente:

# Clona tu bifurcación
git clone https://github.com/YOUR-USERNAME/doappplat-puppeteer-sample.git
cd doappplat-puppeteer-sample

# Construye y ejecuta con Docker
docker build -t gutenberg-scraper .
docker run -p 8080:8080 gutenberg-scraper

2. Entendiendo el Código

La aplicación está estructurada en torno a tres componentes principales:

  1. Servicio de Libros: Maneja el web scraping y la extracción de datos

    async searchBooks(query, página = 1) {
     await this.initialize();
     await rateLimiter.wait();
    
     const elementosPorPágina = 24;
     const urlDeBúsqueda = `${this.baseUrl}/ebooks/search/?query=${encodeURIComponent(query)}&start_index=${(página - 1) * elementosPorPágina}`;
     
     // ... lógica de scraping
    }
    
  2. Servidor Express: Gestiona rutas y renderiza plantillas

    app.get('/libro/:url(*)', async (req, res) => {
     try {
         const urlLibro = req.params.url;
         const detallesLibro = await servicioLibro.obtenerDetallesLibro(urlLibro);
         res.render('libro', libro: detallesLibro, error: null });
     } catch (error) {
         // Manejo de errores
     }
    });
    
  3. Vistas Frontend: Interfaz limpia y responsiva utilizando Bootstrap

    <div class="card book-card h-100">
     <div class="card-body">
         <span class="badge bg-secondary downloads-badge">
             <%= book.downloads.toLocaleString() %> descargas
         </span>
         <h5 class="card-title"><%= book.title %></h5>
         <!-- ... más elementos de UI ... -->
     </div>
    </div>
    

3. Despliegue en DigitalOcean

Ahora que tienes tu bifurcación del repositorio, el despliegue en la plataforma de aplicaciones de DigitalOcean es sencillo:

  1. Crea una nueva aplicación en la plataforma de aplicaciones
  2. Conéctate a tu repositorio bifurcado
  3. En recursos, elimina el segundo recurso (que no es un Dockerfile); esto es generado automáticamente por la plataforma de aplicaciones y no es necesario
  4. Despliega haciendo clic en Crear Recursos

La aplicación se construirá y desplegará automáticamente, con la plataforma de aplicaciones manejando todos los detalles de infraestructura.

Conclusión

Este scraper de Project Gutenberg demuestra cómo construir una aplicación web práctica utilizando tecnologías modernas de la nube. Al combinar Puppeteer para el scraping web, Docker para la contenedorización y la App Platform de DigitalOcean para el despliegue, hemos creado una solución que es tanto robusta como fácil de mantener.

El proyecto sirve como plantilla para tus propias aplicaciones de scraping web, mostrando cómo manejar la automatización del navegador, gestionar recursos de manera eficiente y desplegar en la nube. Ya sea que estés construyendo una herramienta de recolección de datos o simplemente aprendiendo sobre aplicaciones contenedorizadas, este ejemplo proporciona una base sólida sobre la cual construir.

¡Consulta el proyecto en GitHub para aprender más y desplegar tu propia instancia!

Source:
https://www.digitalocean.com/community/tutorials/build-a-puppeteer-web-scrapper-with-docker-and-app-platform