Het bouwen van een Puppeteer Web Scraper met Docker op het DigitalOcean App Platform

Als enthousiaste ultramarathonloper sta ik vaak voor een veelvoorkomende uitdaging: hoe kan ik mijn eindtijd inschatten voor langere wedstrijden die ik nog niet heb geprobeerd? Toen ik hierover sprak met mijn coach, stelde hij een praktische aanpak voor: kijk naar lopers die zowel een race hebben voltooid die ik heb gelopen als de race waarop ik mik. Deze correlatie kan waardevolle inzichten bieden in mogelijke eindtijden. Maar handmatig zoeken door wedstrijduitslagen zou ontzettend tijdrovend zijn.

Dit leidde ertoe dat ik Race Time Insights heb ontwikkeld, een tool die automatisch wedstrijduitslagen vergelijkt door atleten te vinden die beide evenementen hebben voltooid. De applicatie schraapt wedstrijduitslagen van platforms zoals UltraSignup en Pacific Multisports, waardoor hardlopers twee race-URL’s kunnen invoeren en kunnen zien hoe andere atleten hebben gepresteerd bij beide evenementen.

Het bouwen van deze tool liet me zien hoe krachtig het App Platform van DigitalOcean kon zijn. Door Puppeteer te gebruiken met headless Chrome in Docker-containers kon ik me richten op het oplossen van het probleem voor hardlopers terwijl het App Platform alle infrastructurele complexiteit afhandelde. Het resultaat was een robuuste, schaalbare oplossing die de hardloopgemeenschap helpt bij het nemen van op data gebaseerde beslissingen over hun wedstrijddoelen.

Na het bouwen van Race Time Insights wilde ik een handleiding maken waarin andere ontwikkelaars leren hoe ze dezelfde technologieën kunnen benutten – Puppeteer, Docker-containers en het DigitalOcean App Platform. Uiteraard moet je bij het werken met externe gegevens letten op zaken als het hanteren van het aantal verzoeken en de gebruiksvoorwaarden.

Betreed Project Gutenberg. Met zijn uitgebreide collectie van boeken in het publieke domein en duidelijke gebruiksvoorwaarden is het een ideaal kandidaat om deze technologieën te demonstreren. In deze post zullen we verkennen hoe je een boekzoektoepassing kunt bouwen met behulp van Puppeteer in een Docker-container, ingezet op App Platform, terwijl we de beste praktijken volgen voor toegang tot externe data.

Ik heb een webtoepassing gebouwd en gedeeld die verantwoordelijk boekinformatie van Project Gutenberg scrapet. De app, die je kunt vinden in deze GitHub-opslagplaats, stelt gebruikers in staat om te zoeken door duizenden boeken in het publieke domein, gedetailleerde informatie over elk boek te bekijken en verschillende downloadformaten te openen. Wat dit bijzonder interessant maakt, is hoe het verantwoorde web scrapenpraktijken demonstreert en tegelijkertijd echte waarde biedt aan gebruikers.

Een Goede Digitale Burger zijn

Als je een web scraper bouwt, is het cruciaal om goede praktijken te volgen en zowel technische als juridische grenzen te respecteren. Project Gutenberg is een uitstekend voorbeeld om deze principes te leren omdat:

  1. Het heeft duidelijke gebruiksvoorwaarden.
  2. Het biedt robots.txt richtlijnen
  3. Zijn inhoud is expliciet in het publieke domein
  4. Het profiteert van een verbeterde toegankelijkheid tot zijn bronnen

Onze implementatie omvat verschillende best practices:

Rate Limiting

Voor demonstratiedoeleinden implementeren we een eenvoudige rate limiter die ervoor zorgt dat er minimaal 1 seconde tussen verzoeken zit:

// Een eenvoudige rate limiting implementatie
const rateLimiter = {
    lastRequest: 0,
    minDelay: 1000, // 1 seconde tussen verzoeken
    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();
    }
};

Deze implementatie is opzettelijk vereenvoudigd voor het voorbeeld. Het gaat uit van een enkele toepassingsinstantie en slaat de status op in het geheugen, wat niet geschikt zou zijn voor productiegebruik. Meer robuuste oplossingen zouden Redis kunnen gebruiken voor gedistribueerd rate limiting of queue-gebaseerde systemen implementeren voor betere schaalbaarheid.

Deze rate limiter wordt gebruikt vóór elk verzoek naar Project Gutenberg:

async searchBooks(query, page = 1) {
    await this.initialize();
    await rateLimiter.wait();  // Handhaaf rate limiet
    // ... rest van zoeklogica
}

async getBookDetails(bookUrl) {
    await this.initialize();
    await rateLimiter.wait();  // Handhaaf rate limiet
    // ... rest van detaillogica
}

Duidelijke Bot Identificatie

Een aangepaste User-Agent helpt websitebeheerders te begrijpen wie hun site bezoekt en waarom. Deze transparantie stelt hen in staat om:

  1. contact met u op te nemen als er problemen zijn
  2. botverkeer apart van menselijke gebruikers te monitoren en analyseren
  3. mogelijk betere toegang of ondersteuning te bieden aan legitieme scrapers
await browserPage.setUserAgent('GutenbergScraper/1.0 (Educational Project)');

Efficiënt Resourcebeheer

Chrome kan geheugenintensief zijn, vooral bij het uitvoeren van meerdere exemplaren. Het correct sluiten van browserpagina’s na gebruik voorkomt geheugenlekken en zorgt ervoor dat uw toepassing efficiënt draait, zelfs bij het verwerken van veel verzoeken:

try {
    // ... scraping logica
} finally {
    await browserPage.close();  // Maak geheugen en systeembronnen vrij
}

Door deze werkwijzen te volgen, creëren we een scraper die zowel effectief is als respectvol ten opzichte van de resources die het gebruikt. Dit is met name belangrijk bij het werken met waardevolle openbare bronnen zoals Project Gutenberg.

Web Scraping in de Cloud

De applicatie maakt gebruik van moderne cloudarchitectuur en containerisatie via het App Platform van DigitalOcean. Deze aanpak zorgt voor een perfecte balans tussen ontwikkelingssimpliciteit en productiebetrouwbaarheid.

De kracht van het App Platform

Het App Platform stroomlijnt het implementatieproces door het volgende te beheren:

  • Webserverconfiguratie
  • SSL-certificaatbeheer
  • Beveiligingsupdates
  • Load balancing
  • Resource monitoring

Dit stelt ons in staat om ons te richten op de applicatiecode terwijl het App Platform de infrastructuur beheert.

Headless Chrome in een Container

De kern van onze schrapende functionaliteit maakt gebruik van Puppeteer, dat een API op hoog niveau biedt om Chrome programmatisch te besturen. Zo stellen we Puppeteer in en gebruiken we het in onze toepassing:

const puppeteer = require('puppeteer');

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

    async initialize() {
        if (!this.browser) {
            // Voeg omgevingsinformatie toe voor het debuggen
            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);
        }
    }

    // Voorbeeld van schrapen met Puppeteer
    async searchBooks(query, page = 1) {
        await this.initialize();
        await rateLimiter.wait();

        const browserPage = await this.browser.newPage();
        try {
            // Stel headers in om een echte browser na te bootsen en onze bot te identificeren
            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 van de zoeklogica
        } finally {
            await browserPage.close();  // Altijd opruimen
        }
    }
}

Deze setup stelt ons in staat om:

  • Chrome uit te voeren in headless modus (geen GUI nodig)
  • JavaScript uit te voeren in de context van webpagina’s
  • Browserresources veilig te beheren
  • Betrouwbaar te werken in een gecontaineriseerde omgeving

De setup bevat ook verschillende belangrijke configuraties voor het uitvoeren in een gecontaineriseerde omgeving:

  1. Correcte Chrome-argumenten: Essentiële vlaggen zoals --no-sandbox en --disable-dev-shm-usage voor het uitvoeren in containers
  2. Omgevingsbewust pad: Gebruikt het juiste pad naar het Chrome-binair vanuit omgevingsvariabelen
  3. Resourcebeheer: Stelt viewportgrootte in en schakelt onnodige functies uit
  4. Professionele botidentiteit: Duidelijke gebruikersagent en HTTP-headers die onze scraper identificeren
  5. Foutafhandeling: Juiste opruiming van browserpagina’s om geheugenlekken te voorkomen

Hoewel Puppeteer het gemakkelijk maakt om Chrome programmatisch te besturen, vereist het uitvoeren ervan in een container de juiste systeemafhankelijkheden en configuratie. Laten we eens bekijken hoe we dit opzetten in onze Docker-omgeving.

Docker: Zorgen voor consistente omgevingen

Een van de grootste uitdagingen bij het implementeren van webscrapers is ervoor zorgen dat ze op dezelfde manier werken in ontwikkeling en productie. Uw scraper kan perfect werken op uw lokale machine, maar falen in de cloud vanwege ontbrekende afhankelijkheden of verschillende systeemconfiguraties. Docker lost dit op door alles wat de applicatie nodig heeft – van Node.js tot Chrome zelf – te verpakken in een enkele container die overal identiek draait.

Ons Docker-bestand stelt deze consistente omgeving in:

FROM node:18-alpine

# Installeer Chromium en afhankelijkheden
RUN apk add --no-cache \
    chromium \
    nss \
    freetype \
    harfbuzz \
    ca-certificates \
    ttf-freefont \
    dumb-init

# Stel omgevingsvariabelen in
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true \
    PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser \
    PUPPETEER_DISABLE_DEV_SHM_USAGE=true

De op Alpine gebaseerde image houdt onze container lichtgewicht terwijl het alle noodzakelijke afhankelijkheden bevat. Wanneer u deze container uitvoert, of het nu op uw laptop is of in het App Platform van DigitalOcean, krijgt u dezelfde omgeving met alle juiste versies en configuraties om headless Chrome uit te voeren.

Van Ontwikkeling tot Implementatie

Laten we doornemen hoe we dit project aan de praat krijgen:

1. Lokale Ontwikkeling

Ten eerste, fork de voorbeeldrepository naar je GitHub-account. Dit geeft je je eigen kopie om mee te werken en van te implementeren. Clone vervolgens je fork lokaal:

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

# Bouw en draai met Docker
docker build -t gutenberg-scraper .
docker run -p 8080:8080 gutenberg-scraper

2. Het Begrijpen van de Code

De applicatie is opgebouwd rond drie hoofdcomponenten:

  1. Boekenservice: Beheert web scraping en gegevensextractie

    async zoekBoeken(query, pagina = 1) {
     await this.initialiseer();
     await rateLimiter.wacht();
    
     const itemsPerPagina = 24;
     const zoekUrl = `${this.baseUrl}/ebooks/zoeken/?query=${encodeURIComponent(query)}&start_index=${(pagina - 1) * itemsPerPagina}`;
     
     // ... scraping logica
    }
    
  2. Express Server: Beheert routes en rendert sjablonen

    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) {
         // Foutafhandeling
     }
    });
    
  3. Frontend Weergaven: Schone, responsieve UI met behulp van 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>
         <!-- ... meer UI-elementen ... -->
     </div>
    </div>
    

3. Implementatie naar DigitalOcean

Nu je een fork van het repository hebt, is implementeren naar het DigitalOcean App Platform eenvoudig:

  1. Maak een nieuwe App Platform-toepassing
  2. Verbind met je geforkte repo
  3. Verwijder in resources de tweede resource (dat is geen Dockerfile); dit wordt automatisch gegenereerd door App Platform en is niet nodig
  4. Implementeer door op Resources maken te klikken

De toepassing wordt automatisch gebouwd en geïmplementeerd, waarbij App Platform alle infrastructuurdetails afhandelt.

Conclusie

Deze Project Gutenberg-scraper toont hoe je een praktische webtoepassing bouwt met behulp van moderne cloudtechnologieën. Door Puppeteer voor webscraping, Docker voor containerisatie en DigitalOcean’s App Platform voor implementatie te combineren, hebben we een oplossing gecreëerd die zowel robuust als gemakkelijk te onderhouden is.

Het project dient als een sjabloon voor jouw eigen webscraping-toepassingen, waarbij wordt getoond hoe je browserautomatisering kunt beheren, resources efficiënt kunt beheren en naar de cloud kunt implementeren. Of je nu een gegevensverzamelingsinstrument bouwt of gewoon meer te weten wilt komen over gecontaineriseerde toepassingen, dit voorbeeld biedt een solide basis om op voort te bouwen.

Bekijk het project op GitHub om meer te leren en je eigen exemplaar te implementeren!

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