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

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

Isso me levou a desenvolver Corrida Time Insights, uma ferramenta que compara automaticamente resultados de corrida encontrando atletas que completaram ambos os eventos. O aplicativo coleta resultados de corrida de plataformas como UltraSignup e Pacific Multisports, permitindo que corredores insiram dois URLs de corrida e vejam como outros atletas se saíram em ambos os eventos.

A construção dessa ferramenta me mostrou o quão poderosa Plataforma de Aplicativos da DigitalOcean poderia ser. Usando Puppeteer com Chrome headless em contêineres Docker, pude focar em resolver o problema para os corredores enquanto a Plataforma de Aplicativos 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 desenvolver o Corrida Time Insights, eu quis criar um guia mostrando a outros desenvolvedores como aproveitar essas mesmas tecnologias – Puppeteer, contêineres Docker e Plataforma de Aplicativos da DigitalOcean. Claro, ao trabalhar com dados externos, é preciso estar atento a coisas como limites de taxa e termos de serviço.

Acesse o Project Gutenberg. Com sua vasta coleção de livros em 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.

Criei e compartilhei uma aplicação web que extrai informações de livros do Project Gutenberg de forma responsável. O aplicativo, que você pode encontrar neste repositório do GitHub, permite aos usuários pesquisar milhares de livros em domínio público, visualizar informações detalhadas sobre cada livro e acessar vários formatos de download. O que torna isso particularmente interessante é como ele demonstra práticas responsáveis de web scraping ao fornecer valor genuíno aos usuários.

Sendo um Bom Cidadão Digital

Ao construir um web scraper, é crucial seguir boas práticas e respeitar tanto os limites técnicos quanto legais. O Project Gutenberg é um excelente exemplo para aprender esses princípios porque:

  1. Ele possui termos de serviço claros
  2. Ele fornece diretrizes robots.txt
  3. Seu conteúdo está explicitamente em domínio público
  4. Ele se beneficia 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 solicitações:

// Uma implementação ingênua de limitação de taxa
const rateLimiter = {
    lastRequest: 0,
    minDelay: 1000, // 1 segundo entre as solicitaçõ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. Ela pressupõe 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 o Redis para limitação de taxa distribuída ou implementar sistemas baseados em filas para melhor escalabilidade.

Este limitador de taxa é usado antes de cada solicitação ao Project Gutenberg:

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

async getBookDetails(bookUrl) {
    await this.initialize();
    await rateLimiter.wait();  // Aplicar 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 que eles:

  1. Entrar em contato com você se houver problemas
  2. Monitorar e analisar o tráfego de bots separadamente dos usuários humanos
  3. Eventualmente fornecer 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 é tanto eficaz quanto respeitoso com os recursos aos quais acessa. Isso é particularmente importante ao trabalhar com recursos públicos valiosos como o Project Gutenberg.

Web Scraping na Nuvem

A aplicação utiliza arquitetura em nuvem moderna e containerização através da Plataforma de Aplicativos da DigitalOcean. Esse enfoque oferece um equilíbrio perfeito entre simplicidade no desenvolvimento e confiabilidade em produção.

O Poder da Plataforma de Aplicativos

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

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

Isso 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 de nossa funcionalidade de raspagem usa Puppeteer, que fornece uma API de alto nível para controlar o Chrome programaticamente. Aqui está como configuramos e usamos 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 log de informações de ambiente para 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 raspagem 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 busca
        } finally {
            await browserPage.close();  // Sempre limpar
        }
    }
}

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 recursos do navegador com segurança
  • Trabalhar de forma confiável em um ambiente containerizado

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

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

Embora o Puppeteer facilite o controle do Chrome programaticamente, executá-lo em um contêiner requer dependências e configurações do sistema 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 maneira 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 empacotando tudo o que a aplicação precisa – desde o Node.js até o próprio Chrome – em um único contêiner que é executado de forma idêntica em todos os lugares.

Nosso Dockerfile configura esse ambiente consistente:

FROM node:18-alpine

# Instalar Chromium e 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 no Alpine mantém nosso contêiner leve, incluindo todas as dependências necessárias. Ao executar 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 headless.

Desenvolvimento para Implantação

Vamos seguir as etapas para colocar este projeto em funcionamento:

1. Desenvolvimento Local

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

# Clone o 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: Manipula web scraping e extração de dados

    async searchBooks(query, página = 1) {
     await this.initialize();
     await rateLimiter.wait();
    
     const itensPorPágina = 24;
     const urlDeBusca = `${this.baseUrl}/ebooks/search/?query=${encodeURIComponent(query)}&start_index=${(página - 1) * itensPorPágina}`;
     
     // ... lógica de scraping
    }
    
  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.getDetalhesLivro(urlLivro);
         res.render('livro', livro: detalhesLivro, erro: null });
     } catch (erro) {
         // Tratamento de erro
     }
    });
    
  3. Visõ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. Criar um novo aplicativo na App Platform
  2. Conectar ao seu repositório forkado
  3. Nos recursos, excluir o segundo recurso (que não é um Dockerfile); este é gerado automaticamente pela App Platform e não é necessário
  4. Implantar clicando em Criar Recursos

O aplicativo será construído e implantado automaticamente, com a App Platform lidando com todos os detalhes de infraestrutura.

Conclusão

Este raspador do Projeto 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 containers, este exemplo fornece uma base sólida para construir.

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