Ricerca nel sito web

Creazione di un web scraper Puppeteer con Docker sulla piattaforma app


Come appassionato di ultramaratona, spesso affronto una sfida comune: come posso stimare il mio tempo finale per gare più lunghe che non ho ancora tentato? Discutendo di questo con il mio allenatore, mi ha suggerito un approccio pratico: guarda i corridori che hanno completato sia una gara che ho fatto sia la gara a cui miro. Questa correlazione potrebbe fornire preziose informazioni sui potenziali tempi di finitura. Ma la ricerca manuale tra i risultati delle gare richiederebbe un tempo incredibilmente lungo.

Ciò mi ha portato a creare Race Time Insights, uno strumento che confronta automaticamente i risultati delle gare trovando gli atleti che hanno completato entrambi gli eventi. L'applicazione raccoglie i risultati delle gare da piattaforme come UltraSignup e Pacific Multisports, consentendo ai corridori di inserire due URL di gara e vedere come si sono comportati gli altri atleti in entrambi gli eventi.

La creazione di questo strumento mi ha mostrato quanto potente possa essere la piattaforma app di DigitalOcean. Utilizzando Puppeteer con Chrome headless nei contenitori Docker, ho potuto concentrarmi sulla risoluzione del problema per i corridori mentre App Platform gestiva tutta la complessità dell'infrastruttura. Il risultato è stata una soluzione solida e scalabile che aiuta la comunità dei podisti a prendere decisioni basate sui dati sugli obiettivi di gara.

Dopo aver creato Race Time Insights, volevo creare una guida che mostrasse ad altri sviluppatori come sfruttare queste stesse tecnologie: Puppeteer, contenitori Docker e piattaforma app DigitalOcean. Naturalmente, quando si lavora con dati esterni, è necessario tenere presente aspetti come la limitazione della velocità e i termini di servizio.

Inserisci il Progetto Gutenberg. Con la sua vasta collezione di libri di pubblico dominio e termini di servizio chiari, è un candidato ideale per dimostrare queste tecnologie. In questo post esploreremo come creare un'applicazione per la ricerca di libri utilizzando Puppeteer in un contenitore Docker, distribuito sulla piattaforma app, seguendo le best practice per l'accesso ai dati esterni.

Ricerca di libri del progetto Gutenberg

Ho creato e condiviso un'applicazione web che estrae in modo responsabile le informazioni sui libri dal Progetto Gutenberg. L'app, che puoi trovare in questo repository GitHub, consente agli utenti di effettuare ricerche tra migliaia di libri di pubblico dominio, visualizzare informazioni dettagliate su ciascun libro e accedere a vari formati di download. Ciò che lo rende particolarmente interessante è il modo in cui dimostra pratiche responsabili di web scraping fornendo allo stesso tempo un valore reale agli utenti.

Essere un buon cittadino digitale

Quando si costruisce un web scraper, è fondamentale seguire le buone pratiche e rispettare i limiti sia tecnici che legali. Il Progetto Gutenberg è un eccellente esempio per apprendere questi principi perché:

  1. Ha termini di servizio chiari
  2. Fornisce le linee guida per il file robots.txt
  3. Il suo contenuto è esplicitamente di pubblico dominio
  4. Beneficia di una maggiore accessibilità alle sue risorse

La nostra implementazione include diverse best practice:

Limitazione della velocità

A scopo dimostrativo, implementiamo un semplice limitatore di velocità che garantisce almeno 1 secondo tra le richieste:

// A naive rate limiting implementation
const rateLimiter = {
    lastRequest: 0,
    minDelay: 1000, // 1 second between requests
    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();
    }
};

Questa implementazione è intenzionalmente semplificata per l'esempio. Presuppone una singola istanza dell'applicazione e memorizza lo stato in memoria, il che non sarebbe adatto all'uso in produzione. Soluzioni più robuste potrebbero utilizzare Redis per la limitazione della velocità distribuita o implementare sistemi basati su code per una migliore scalabilità.

Questo limitatore di velocità viene utilizzato prima di ogni richiesta al Progetto Gutenberg:

async searchBooks(query, page = 1) {
    await this.initialize();
    await rateLimiter.wait();  // Enforce rate limit
    // ... rest of search logic
}

async getBookDetails(bookUrl) {
    await this.initialize();
    await rateLimiter.wait();  // Enforce rate limit
    // ... rest of details logic
}

Cancella identificazione bot

Uno user-agent personalizzato aiuta gli amministratori del sito web a capire chi sta accedendo al loro sito e perché. Questa trasparenza consente loro di:

  1. Contattarti se ci sono problemi
  2. Monitora e analizza il traffico dei bot separatamente dagli utenti umani
  3. Fornire potenzialmente un migliore accesso o supporto per gli scraper legittimi
await browserPage.setUserAgent('GutenbergScraper/1.0 (Educational Project)');

Gestione efficiente delle risorse

Chrome può richiedere un utilizzo intensivo della memoria, soprattutto quando si eseguono più istanze. La chiusura corretta delle pagine del browser dopo l'uso previene perdite di memoria e garantisce che l'applicazione venga eseguita in modo efficiente, anche quando si gestiscono molte richieste:

try {
    // ... scraping logic
} finally {
    await browserPage.close();  // Free up memory and system resources
}

Seguendo queste pratiche, creiamo uno scraper che sia efficace e rispettoso delle risorse a cui accede. Ciò è particolarmente importante quando si lavora con preziose risorse pubbliche come il Progetto Gutenberg.

Web Scraping nel Cloud

L'applicazione sfrutta la moderna architettura cloud e la containerizzazione attraverso la piattaforma app di DigitalOcean. Questo approccio fornisce un perfetto equilibrio tra semplicità di sviluppo e affidabilità di produzione.

La potenza della piattaforma app

La piattaforma app semplifica il processo di distribuzione gestendo:

  • Configurazione del server web
  • Gestione dei certificati SSL
  • Aggiornamenti di sicurezza
  • Bilanciamento del carico
  • Monitoraggio delle risorse

Questo ci consente di concentrarci sul codice dell'applicazione mentre App Platform gestisce l'infrastruttura.

Cromo senza testa in un contenitore

Il nucleo della nostra funzionalità di scraping utilizza Puppeteer, che fornisce un'API di alto livello per controllare Chrome in modo programmatico. Ecco come configuriamo e utilizziamo Puppeteer nella nostra applicazione:

const puppeteer = require('puppeteer');

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

    async initialize() {
        if (!this.browser) {
            // Add environment info logging for debugging
            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);
        }
    }

    // Example of scraping with Puppeteer
    async searchBooks(query, page = 1) {
        await this.initialize();
        await rateLimiter.wait();

        const browserPage = await this.browser.newPage();
        try {
            // Set headers to mimic a real browser and identify our 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' });
            
            // ... rest of search logic
        } finally {
            await browserPage.close();  // Always clean up
        }
    }
}

Questa configurazione ci consente di:

  • Esegui Chrome in modalità headless (non è necessaria la GUI)
  • Esegui JavaScript nel contesto delle pagine web
  • Gestisci in modo sicuro le risorse del browser
  • Lavora in modo affidabile in un ambiente containerizzato

La configurazione include anche diverse configurazioni importanti per l'esecuzione in un ambiente containerizzato:

  1. Argomenti Chrome corretti: flag essenziali come --no-sandbox e --disable-dev-shm-usage per l'esecuzione in contenitori
  2. Percorso compatibile con l'ambiente: utilizza il percorso binario di Chrome corretto dalle variabili di ambiente
  3. Gestione delle risorse: imposta le dimensioni dell'area visibile e disabilita le funzionalità non necessarie
  4. Identità bot professionale: cancella lo user agent e le intestazioni HTTP che identificano il nostro scraper
  5. Gestione degli errori: pulizia adeguata delle pagine del browser per evitare perdite di memoria

Sebbene Puppeteer semplifichi il controllo di Chrome a livello di programmazione, l'esecuzione in un contenitore richiede dipendenze e configurazioni di sistema adeguate. Diamo un'occhiata a come lo configuriamo nel nostro ambiente Docker.

Docker: garantire ambienti coerenti

Una delle maggiori sfide nell'implementazione dei web scraper è garantire che funzionino allo stesso modo nello sviluppo e nella produzione. Il tuo scraper potrebbe funzionare perfettamente sul tuo computer locale ma non funzionare nel cloud a causa di dipendenze mancanti o diverse configurazioni di sistema. Docker risolve questo problema confezionando tutto ciò di cui l'applicazione ha bisogno, da Node.js a Chrome stesso, in un unico contenitore che funziona in modo identico ovunque.

Il nostro Dockerfile configura questo ambiente coerente:

FROM node:18-alpine
Install Chromium and dependencies
RUN apk add --no-cache \
    chromium \
    nss \
    freetype \
    harfbuzz \
    ca-certificates \
    ttf-freefont \
    dumb-init
Set environment variables
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true \
    PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser \
    PUPPETEER_DISABLE_DEV_SHM_USAGE=true

L'immagine basata su Alpine mantiene il nostro contenitore leggero includendo tutte le dipendenze necessarie. Quando esegui questo contenitore, sia sul tuo laptop che sulla piattaforma app di DigitalOcean, ottieni esattamente lo stesso ambiente con tutte le versioni e configurazioni corrette per l'esecuzione di Chrome headless.

Dallo sviluppo alla distribuzione

Esaminiamo come rendere operativo questo progetto:

1. Sviluppo locale

Innanzitutto, esegui il fork del repository di esempio sul tuo account GitHub. Questo ti dà la tua copia con cui lavorare e da cui distribuire. Quindi clona il tuo fork localmente:

# Clone your fork
git clone https://github.com/YOUR-USERNAME/doappplat-puppeteer-sample.git
cd doappplat-puppeteer-sample
Build and run with Docker
docker build -t gutenberg-scraper .
docker run -p 8080:8080 gutenberg-scraper

2. Comprendere il Codice

L'applicazione è strutturata attorno a tre componenti principali:

  1. Servizio libri: gestisce il web scraping e l'estrazione dei dati

    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}`;
     
     // ... scraping logic
    }
    
  2. Express Server: gestisce i percorsi e i modelli di rendering

    app.get('/book/:url(*)', async (req, res) => {
     try {
         const bookUrl = req.params.url;
         const bookDetails = await bookService.getBookDetails(bookUrl);
         res.render('book', { book: bookDetails, error: null });
     } catch (error) {
         // Error handling
     }
    });
    
  3. Visualizzazioni frontend: interfaccia utente pulita e reattiva utilizzando 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>
         <!-- ... more UI elements ... -->
     </div>
    </div>
    

3. Distribuzione a DigitalOcean

Ora che hai il fork del repository, la distribuzione su DigitalOcean App Platform è semplice:

  1. Creare una nuova applicazione della piattaforma app
  2. Connettiti al tuo rappresentante biforcato
  3. Sulle risorse, elimina la seconda risorsa (che non è un Dockerfile); questo è generato automaticamente dalla piattaforma app e non è necessario
  4. Distribuisci facendo clic su Crea risorse

L'applicazione verrà creata e distribuita automaticamente, con la piattaforma app che gestirà tutti i dettagli dell'infrastruttura.

Conclusione

Questo scraper del progetto Gutenberg dimostra come creare un'applicazione web pratica utilizzando le moderne tecnologie cloud. Combinando Puppeteer per il web scraping, Docker per la containerizzazione e la piattaforma app di DigitalOcean per la distribuzione, abbiamo creato una soluzione solida e di facile manutenzione.

Il progetto funge da modello per le tue applicazioni di web scraping, mostrando come gestire l'automazione del browser, gestire le risorse in modo efficiente e distribuirle nel cloud. Che tu stia creando uno strumento di raccolta dati o semplicemente imparando a conoscere le applicazioni containerizzate, questo esempio fornisce una solida base su cui costruire.

Dai un'occhiata al progetto su GitHub per saperne di più e distribuire la tua istanza!