Construindo um Web Scraper com Puppeteer usando Docker na Plataforma de Aplicativos DigitalOcean

Como entusiasta de ultramaratonas, frequentemente enfrento um desafio comum: como estimar meu tempo de chegada para corridas mais longas que ainda não tentei? Ao discutir isso com meu treinador, ele sugeriu uma abordagem prática — olhar para corredores que completaram tanto uma corrida que eu fiz quanto a corrida que estou visando. Essa correlação poderia fornecer insights valiosos sobre tempos de chegada potenciais. Mas pesquisar manualmente os resultados das corridas seria incrivelmente demorado.

Isso me levou a construir Race Time Insights, uma ferramenta que compara automaticamente os resultados das corridas, encontrando atletas que completaram ambos os eventos. A aplicação extrai resultados de corridas de plataformas como UltraSignup e Pacific Multisports, permitindo que os corredores insiram duas URLs de corridas e vejam como outros atletas se saíram em ambos os eventos.

Construir essa ferramenta me mostrou quão poderosa a App Platform da DigitalOcean pode ser. Usando Puppeteer com Chrome headless em contêineres Docker, eu pude me concentrar em resolver o problema para os corredores enquanto a App Platform lidava com toda a complexidade da infraestrutura. O resultado foi uma solução robusta e escalável que ajuda a comunidade de corredores a tomar decisões baseadas em dados sobre seus objetivos de corrida.

Após construir o Race Time Insights, eu queria criar um guia mostrando a outros desenvolvedores como aproveitar essas mesmas tecnologias — Puppeteer, contêineres Docker e DigitalOcean App Platform. Claro, ao trabalhar com dados externos, você precisa estar atento a coisas como limitação de taxa e termos de serviço.

Acesse o Projeto Gutenberg. Com sua vasta coleção de livros de domínio público e termos de serviço claros, é um candidato ideal para demonstrar essas tecnologias. Neste post, exploraremos como construir um aplicativo de busca de livros usando o Puppeteer em um contêiner Docker, implantado na Plataforma de Aplicativos, seguindo as melhores práticas para acesso a dados externos.

Construí e compartilhei um aplicativo da web que coleta informações dos livros do Projeto Gutenberg de forma responsável. O aplicativo, que você pode encontrar neste repositório do GitHub, permite aos usuários pesquisar milhares de livros de domínio público, visualizar informações detalhadas sobre cada livro e acessar diversos formatos de download. O que torna isso particularmente interessante é como ele demonstra práticas responsáveis de coleta de dados da web, fornecendo valor genuíno aos usuários.

Sendo um Bom Cidadão Digital

Ao construir um raspador da web, é crucial seguir boas práticas e respeitar limites técnicos e legais. O Projeto Gutenberg é um excelente exemplo para aprender esses princípios porque:

  1. Tem termos de serviço claros
  2. Fornece diretrizes para robots.txt
  3. Seu conteúdo está explicitamente no domínio público
  4. Beneficia-se de maior acessibilidade aos seus recursos

Nossa implementação inclui várias melhores práticas:

Limitação de Taxa

Para fins de demonstração, implementamos um limitador de taxa simples que garante pelo menos 1 segundo entre as requisições:

// Uma implementação ingênua de limitação de taxa
const rateLimiter = {
    lastRequest: 0,
    minDelay: 1000, // 1 segundo entre requisições
    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 implementação é intencionalmente simplificada para o exemplo. Assume uma única instância de aplicativo e armazena o estado na memória, o que não seria adequado para uso em produção. Soluções mais robustas podem usar Redis para limitação de taxa distribuída ou implementar sistemas baseados em fila para melhor escalabilidade.

Este limitador de taxa é usado antes de cada requisição ao Projeto Gutenberg:

async searchBooks(query, page = 1) {
    await this.initialize();
    await rateLimiter.wait();  // Impor limite de taxa
    // ... resto da lógica de busca
}

async getBookDetails(bookUrl) {
    await this.initialize();
    await rateLimiter.wait();  // Impor limite de taxa
    // ... resto da lógica de detalhes
}

Identificação Clara de Bots

Um User-Agent personalizado ajuda os administradores de sites a entender quem está acessando seu site e por quê. Essa transparência permite a eles:

  1. Entrar em contato com você em caso de problemas
  2. Monitorar e analisar o tráfego de bots separadamente dos usuários humanos
  3. potencialmente oferecer melhor acesso ou suporte para raspadores legítimos
await browserPage.setUserAgent('GutenbergScraper/1.0 (Educational Project)');

Gerenciamento Eficiente de Recursos

O Chrome pode ser intensivo em memória, especialmente ao executar várias instâncias. Fechar corretamente as páginas do navegador após o uso evita vazamentos de memória e garante que sua aplicação seja executada de forma eficiente, mesmo ao lidar com muitas solicitações:

try {
    // ... lógica de raspagem
} finally {
    await browserPage.close();  // Liberar memória e recursos do sistema
}

Ao seguir essas práticas, criamos um raspador que é eficaz e respeitoso com os recursos que acessa. Isso é particularmente importante ao trabalhar com recursos públicos valiosos, como o Projeto Gutenberg.

Web Scraping na Nuvem

A aplicação aproveita a arquitetura moderna em nuvem e a containerização através da Plataforma de Aplicativos da DigitalOcean. Esta abordagem fornece um equilíbrio perfeito entre simplicidade no desenvolvimento e confiabilidade na produção.

O Poder da Plataforma de Aplicativos

A Plataforma de Aplicativos simplifica o processo de implantação lidando com:

  • Configuração do servidor web
  • Gerenciamento de certificado SSL
  • Atualizações de segurança
  • Balanço de carga
  • Monitoramento de recursos

Isto nos permite focar no código da aplicação enquanto a Plataforma de Aplicativos gerencia a infraestrutura.

Chrome Headless em um Container

O núcleo da nossa funcionalidade de scraping utiliza o Puppeteer, que fornece uma API de alto nível para controlar o Chrome programaticamente. Aqui está como configuramos e utilizamos o Puppeteer em nossa aplicação:

const puppeteer = require('puppeteer');

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

    async initialize() {
        if (!this.browser) {
            // Adicionar informações do ambiente para registro de depuração
            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);
        }
    }

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

        const browserPage = await this.browser.newPage();
        try {
            // Definir cabeçalhos para imitar um navegador real e identificar nosso 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 da lógica de pesquisa
        } finally {
            await browserPage.close();  // Sempre fazer a limpeza
        }
    }
}

Esta configuração nos permite:

  • Executar o Chrome no modo headless (sem GUI necessária)
  • Executar JavaScript no contexto de páginas da web
  • Gerenciar com segurança os recursos do navegador
  • Trabalhar de forma confiável em um ambiente containerizado

A configuração também inclui várias configurações importantes para rodar em um ambiente containerizado:

  1. Argumentos do Chrome Adequados: Flags essenciais como --no-sandbox e --disable-dev-shm-usage para rodar em containers
  2. Caminho Consciente do Ambiente: Utiliza o caminho binário correto do Chrome a partir de variáveis de ambiente
  3. Gerenciamento de Recursos: Define o tamanho da viewport e desativa recursos desnecessários
  4. Identidade Profissional do Bot: User agent claro e cabeçalhos HTTP identificando nosso scraper
  5. Tratamento de Erro: Limpeza adequada das páginas do navegador para prevenir vazamentos de memória

Embora o Puppeteer torne fácil controlar o Chrome programaticamente, executá-lo em um contêiner requer dependências do sistema e configurações adequadas. Vamos ver como configuramos isso em nosso ambiente Docker.

Docker: Garantindo Ambientes Consistentes

Um dos maiores desafios ao implantar web scrapers é garantir que eles funcionem da mesma forma em desenvolvimento e produção. Seu scraper pode funcionar perfeitamente em sua máquina local, mas falhar na nuvem devido a dependências ausentes ou configurações de sistema diferentes. O Docker resolve isso ao empacotar tudo que a aplicação precisa – desde Node.js até o Chrome em si – em um único contêiner que é executado de forma idêntica em todos os lugares.

Nosso Dockerfile configura este ambiente consistente:

FROM node:18-alpine

# Instalar o Chromium e as dependências
RUN apk add --no-cache \
    chromium \
    nss \
    freetype \
    harfbuzz \
    ca-certificates \
    ttf-freefont \
    dumb-init

# Definir variáveis de ambiente
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true \
    PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser \
    PUPPETEER_DISABLE_DEV_SHM_USAGE=true

A imagem baseada em Alpine mantém nosso contêiner leve, incluindo todas as dependências necessárias. Quando você executa este contêiner, quer seja em seu laptop ou na Plataforma de Aplicativos da DigitalOcean, você obtém o mesmo ambiente exato com todas as versões e configurações corretas para executar o Chrome sem interface gráfica.

Desenvolvimento para Implantação

Vamos passar pelo processo de colocar este projeto em funcionamento:

1. Desenvolvimento Local

Primeiro, faça um fork do repositório de exemplo na sua conta do GitHub. Isso lhe dá uma cópia própria para trabalhar e implantar. Em seguida, clone seu fork localmente:

# Clone seu fork
git clone https://github.com/YOUR-USERNAME/doappplat-puppeteer-sample.git
cd doappplat-puppeteer-sample

# Construa e execute com Docker
docker build -t gutenberg-scraper .
docker run -p 8080:8080 gutenberg-scraper

2. Entendendo o Código

A aplicação é estruturada em torno de três componentes principais:

  1. Serviço de Livros: Lida com raspagem de web e extração de dados

    async searchBooks(query, page = 1) {
     await this.initialize();
     await rateLimiter.wait();
    
     const itemsPerPage = 24;
     const searchUrl = `${this.baseUrl}/ebooks/search/?query=${encodeURIComponent(query)}&start_index=${(page - 1) * itemsPerPage}`;
     
     // ... lógica de raspagem
    }
    
  2. Servidor Express: Gerencia rotas e renderiza templates

    app.get('/livro/:url(*)', async (req, res) => {
     try {
         const urlLivro = req.params.url;
         const detalhesLivro = await servicoLivro.obterDetalhesLivro(urlLivro);
         res.render('livro', { livro: detalhesLivro, erro: null });
     } catch (erro) {
         // Tratamento de erro
     }
    });
    
  3. Visualizações Frontend: UI limpa e responsiva usando Bootstrap

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

3. Implantação no DigitalOcean

Agora que você tem o seu fork do repositório, implantar no DigitalOcean App Platform é simples:

  1. Crie uma nova aplicação na App Platform
  2. Conecte-se ao seu repositório forkado
  3. Nos recursos, exclua o segundo recurso (que não é um Dockerfile); este é gerado automaticamente pela App Platform e não é necessário
  4. Implante clicando em Criar Recursos

A aplicação será construída e implantada automaticamente, com a App Platform lidando com todos os detalhes de infraestrutura.

Conclusão

Este raspador do Project Gutenberg demonstra como construir uma aplicação web prática utilizando tecnologias de nuvem modernas. Ao combinar o Puppeteer para raspagem web, o Docker para containerização e a Plataforma de Aplicativos da DigitalOcean para implantação, criamos uma solução robusta e fácil de manter.

O projeto serve como um modelo para suas próprias aplicações de raspagem web, mostrando como lidar com automação de navegador, gerenciar recursos de forma eficiente e implantar na nuvem. Se você está construindo uma ferramenta de coleta de dados ou apenas aprendendo sobre aplicações em contêineres, este exemplo fornece uma base sólida para construir sobre.

Confira o projeto no GitHub para saber mais e implantar sua própria instância!

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