ウェブサイト検索

App Platform 上の Docker を使用して Puppeteer Web Scraper を構築する


ウルトラマラソン愛好家として、私はよく共通の課題に直面します。それは、まだ挑戦したことのない長距離レースのゴールタイムをどうやって見積もればよいでしょうか?このことについてコーチと話し合ったとき、彼は実践的なアプローチを提案しました。それは、私がこれまでに出場したレースと、私が目標としているレースの両方を完走したランナーを見てみましょう。この相関関係により、潜在的な終了時間についての貴重な洞察が得られる可能性があります。しかし、レース結果を手動で検索すると、信じられないほど時間がかかります。

これが私に、両方のイベントを完走したアスリートを見つけてレース結果を自動的に比較するツール、Race Time Insights を構築するきっかけになりました。このアプリケーションは、UltraSignup や Pacific Multisports などのプラットフォームからレース結果を収集し、ランナーが 2 つのレース URL を入力して、両方のイベントで他のアスリートがどのようにパフォーマンスしたかを確認できるようにします。

このツールを構築することで、DigitalOcean のアプリ プラットフォームがいかに強力であるかを知りました。 Docker コンテナ内のヘッドレス Chrome で Puppeteer を使用すると、App Platform がインフラストラクチャの複雑さをすべて処理しながら、ランナーの問題の解決に集中できました。その結果、ランニング コミュニティがレースの目標についてデータに基づいた意思決定を行うのに役立つ、堅牢でスケーラブルなソリューションが誕生しました。

Race Time Insights を構築した後、同じテクノロジー (Puppeteer、Docker コンテナー、DigitalOcean App Platform) を活用する方法を他の開発者に示すガイドを作成したいと思いました。もちろん、外部データを扱うときは、レート制限やサービス規約などに注意する必要があります。

プロジェクト・グーテンベルクに入ります。パブリックドメインの書籍の膨大なコレクションと明確な利用規約を備えているため、これらのテクノロジーをデモンストレーションするのに最適な候補です。この投稿では、外部データ アクセスのベスト プラクティスに従いながら、App Platform にデプロイされた Docker コンテナーで Puppeteer を使用して書籍検索アプリケーションを構築する方法を検討します。

プロジェクト・グーテンベルクの書籍検索

私は、責任を持って Project Gutenberg から書籍情報を収集する Web アプリケーションを構築して共有しました。この GitHub リポジトリにあるこのアプリを使用すると、ユーザーは何千ものパブリック ドメインの書籍を検索し、各書籍の詳細情報を表示し、さまざまなダウンロード形式にアクセスできます。これが特に興味深いのは、ユーザーに真の価値を提供しながら、責任ある Web スクレイピングの実践をどのように実証しているかということです。

良きデジタル市民になるために

Web スクレイパーを構築するときは、優れた慣行に従い、技術的境界と法的境界の両方を尊重することが重要です。プロジェクト グーテンベルクは、次の理由からこれらの原則を学ぶための優れた例です。

  1. 明確な利用規約がある
  2. robots.txt のガイドラインを提供します
  3. そのコンテンツは明示的にパブリックドメインにあります
  4. リソースへのアクセスが向上することでメリットが得られます

私たちの実装には、いくつかのベスト プラクティスが含まれています。

レート制限

デモンストレーションの目的で、リクエスト間に少なくとも 1 秒を確保する単純なレート リミッターを実装します。

// 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();
    }
};

この実装は例のために意図的に単純化されています。単一のアプリケーション インスタンスを想定し、状態をメモリに保存するため、運用環境での使用には適していません。より堅牢なソリューションでは、分散型レート制限に Redis を使用したり、スケーラビリティを向上させるためにキューベースのシステムを実装したりする場合があります。

このレート リミッターは、Project 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
}

ボットの識別を明確にする

カスタム ユーザー エージェントは、Web サイト管理者がサイトにアクセスしているユーザーとその理由を理解するのに役立ちます。この透明性により、次のことが可能になります。

  1. 問題がある場合は連絡してください
  2. 人間のユーザーとは別にボットのトラフィックを監視および分析する
  3. 正規のスクレイパーに対するアクセスやサポートを向上させる可能性があります
await browserPage.setUserAgent('GutenbergScraper/1.0 (Educational Project)');

効率的なリソース管理

Chrome は、特に複数のインスタンスを実行している場合、メモリを大量に消費する可能性があります。使用後にブラウザ ページを適切に閉じると、メモリ リークが防止され、多くのリクエストを処理する場合でもアプリケーションが効率的に実行されます。

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

これらのプラクティスに従うことで、効率的であり、アクセスするリソースを尊重するスクレイパーを作成します。これは、プロジェクト グーテンベルクのような貴重な公共リソースを扱う場合に特に重要です。

クラウドでの Web スクレイピング

このアプリケーションは、DigitalOcean のアプリ プラットフォームを介した最新のクラウド アーキテクチャとコンテナ化を活用しています。このアプローチにより、開発の簡素化と運用の信頼性の間で完璧なバランスが得られます。

アプリプラットフォームの力

App Platform は、以下を処理することで展開プロセスを合理化します。

  • Webサーバーの設定
  • SSL証明書の管理
  • セキュリティアップデート
  • 負荷分散
  • リソース監視

これにより、App Platform がインフラストラクチャを管理しながら、アプリケーション コードに集中できるようになります。

コンテナ内のヘッドレス Chrome

スクレイピング機能の中核は、Chrome をプログラムで制御するための高レベル API を提供する Puppeteer を使用します。アプリケーションで Puppeteer を設定して使用する方法は次のとおりです。

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
        }
    }
}

この設定により、次のことが可能になります。

  • Chrome をヘッドレス モードで実行します (GUI は必要ありません)
  • Web ページのコンテキストで JavaScript を実行する
  • ブラウザのリソースを安全に管理する
  • コンテナ化された環境で確実に動作する

セットアップには、コンテナ化された環境で実行するためのいくつかの重要な構成も含まれています。

  1. 適切な Chrome 引数: コンテナ内で実行するための --no-sandbox--disable-dev-shm-usage などの必須フラグ
  2. 環境対応パス: 環境変数からの正しい Chrome バイナリ パスを使用します。
  3. リソース管理: ビューポートのサイズを設定し、不要な機能を無効にします。
  4. プロフェッショナル ボット ID: スクレイパーを識別する明確なユーザー エージェントと HTTP ヘッダー
  5. エラー処理: メモリ リークを防ぐためのブラウザ ページの適切なクリーンアップ

Puppeteer を使用すると Chrome をプログラムで簡単に制御できますが、コンテナ内で実行するには適切なシステムの依存関係と構成が必要です。これを Docker 環境でどのように設定するかを見てみましょう。

Docker: 一貫した環境を確保する

Web スクレイパーを導入する際の最大の課題の 1 つは、Web スクレイパーが開発と運用で同じように動作するようにすることです。スクレイパーはローカル マシンでは完全に動作しても、クラウドでは依存関係の欠落やシステム構成の違いにより失敗する可能性があります。 Docker は、Node.js から Chrome 自体まで、アプリケーションに必要なものすべてを、どこでも同じように実行できる単一のコンテナーにパッケージ化することでこの問題を解決します。

私たちの Dockerfile は、この一貫した環境をセットアップします。

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

Alpine ベースのイメージは、必要な依存関係をすべて含めながら、コンテナーを軽量に保ちます。このコンテナをラップトップ上でも DigitalOcean のアプリ プラットフォーム上でも実行すると、ヘッドレス Chrome を実行するための適切なバージョンと構成がすべて揃ったまったく同じ環境が得られます。

開発から展開まで

このプロジェクトを立ち上げて実行する手順を見てみましょう。

1. 地域開発

まず、サンプル リポジトリを GitHub アカウントにフォークします。これにより、作業および展開に使用できる独自のコピーが得られます。次に、ローカルでフォークのクローンを作成します。

# 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. コードを理解する

アプリケーションは、次の 3 つの主要コンポーネントを中心とした構造になっています。

  1. ブックサービス: Web スクレイピングとデータ抽出を処理します。

    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: ルートを管理し、テンプレートをレンダリングします。

    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. フロントエンド ビュー: ブートストラップを使用したクリーンで応答性の高い UI

    <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. DigitalOcean への展開

リポジトリのフォークができたので、DigitalOcean App Platform へのデプロイは簡単です。

  1. 新しいApp Platformアプリケーションを作成する
  2. 分岐した担当者に接続する
  3. リソースで、2 番目のリソース (Dockerfile ではない) を削除します。これはアプリ プラットフォームによって自動生成されるため、必要ありません
  4. 「リソースの作成」をクリックしてデプロイします

アプリケーションは自動的に構築およびデプロイされ、App Platform がすべてのインフラストラクチャの詳細を処理します。

結論

この Project Gutenberg スクレーパーは、最新のクラウド テクノロジを使用して実用的な Web アプリケーションを構築する方法を示します。 Web スクレイピングには Puppeteer、コンテナ化には Docker、デプロイメントには DigitalOcean のアプリ プラットフォームを組み合わせることで、堅牢かつ保守が容易なソリューションを作成しました。

このプロジェクトは、独自の Web スクレイピング アプリケーションのテンプレートとして機能し、ブラウザーの自動化を処理し、リソースを効率的に管理し、クラウドに展開する方法を示します。データ収集ツールを構築している場合でも、単にコンテナ化されたアプリケーションについて学習している場合でも、この例は構築するための強固な基盤を提供します。

詳細を確認し、独自のインスタンスをデプロイするには、GitHub のプロジェクトをチェックしてください。