在 DigitalOcean 應用平台上使用 Docker 建立 Puppeteer 網頁爬蟲

作為一名超馬愛好者,我經常面臨一個常見的挑戰:如何估算我尚未嘗試過的長距離比賽的完賽時間?在與我的教練討論這個問題時,他建議了一個實用的方法——查看那些完成過我參加的比賽以及我目標比賽的跑者。這種相關性可以提供潛在完賽時間的寶貴見解。但手動搜索比賽結果將會非常耗時。

這促使我建立了比賽時間洞察,這是一個自動比較比賽結果的工具,通過找到同時參加過這兩個比賽的運動員。該應用程序從像 UltraSignup 和 Pacific Multisports 這樣的平台抓取比賽結果,允許跑者輸入兩個比賽的網址,查看其他運動員在這兩個比賽中的表現。

建立這個工具讓我深刻體會到DigitalOcean 的應用平台是多麼強大。使用 Puppeteer 和無頭 Chrome 在 Docker 容器中,我可以專注於解決跑者的問題,而應用平台則處理所有基礎設施的複雜性。最終結果是一個強大、可擴展的解決方案,幫助跑步社區根據數據做出有根據的比賽目標決策。

在建立比賽時間洞察之後,我想創建一個指南,向其他開發者展示如何利用這些相同的技術——Puppeteer、Docker 容器和 DigitalOcean 應用平台。當然,在使用外部數據時,您需要注意像速率限制和服務條款這樣的問題。

進入古騰堡計畫。憑藉其龐大的公共領域書籍收藏和明確的服務條款,它是展示這些技術的理想候選者。在這篇文章中,我們將探討如何使用 Puppeteer 在 Docker 容器中構建一本書的搜索應用程序,並在 App Platform 上部署,同時遵循對外部數據訪問的最佳實踐。

我已經構建並分享了一個網絡應用程序,負責任地從古騰堡計畫抓取書籍信息。該應用程序可以在這個 GitHub 倉庫 中找到,允許用戶搜索數千本公共領域書籍,查看每本書的詳細信息,並訪問各種下載格式。這特別有趣的是,它展示了負責任的網絡爬蟲實踐,同時為用戶提供了真正的價值。

成為一個良好的數位公民

在構建網絡爬蟲時,遵循良好的實踐並尊重技術和法律界限至關重要。古騰堡計畫是一個學習這些原則的絕佳範例,因為:

  1. 它有明確的服務條款
  2. 它提供了robots.txt指南
  3. 其內容明確屬於公共領域
  4. 它受益於資源可及性的提高

我們的實現包括幾個最佳實踐:

速率限制

為了演示目的,我們實現了一個簡單的速率限制器,確保請求之間至少間隔1秒:

// 一個簡單的速率限制實現
const rateLimiter = {
    lastRequest: 0,
    minDelay: 1000, // 請求之間的間隔為1秒
    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進行分佈式速率限制或實現基於隊列的系統以提高可擴展性。

這個速率限制器在每次請求到達古騰堡計畫之前使用:

async searchBooks(query, page = 1) {
    await this.initialize();
    await rateLimiter.wait();  // 強制執行速率限制
    // ... 其餘的搜索邏輯
}

async getBookDetails(bookUrl) {
    await this.initialize();
    await rateLimiter.wait();  // 強制執行速率限制
    // ... 其餘的詳細邏輯
}

清晰的機器人識別

自定義的用戶代理幫助網站管理員了解誰正在訪問他們的網站以及原因。這種透明度使他們能夠:

  1. 在出現問題時與您聯繫
  2. 監控並分析機器人流量與人類用戶分開
  3. 可能為合法的網頁爬蟲提供更好的訪問或支持
await browserPage.setUserAgent('GutenbergScraper/1.0 (Educational Project)');

高效的資源管理

Chrome可能會消耗大量內存,特別是在運行多個實例時。在使用後正確關閉瀏覽器頁面可以防止內存洩漏,確保應用程序運行高效,即使在處理許多請求時也是如此:

try {
    // ... 網頁爬蟲邏輯
} finally {
    await browserPage.close();  // 釋放內存和系統資源
}

通過遵循這些實踐,我們可以創建一個既有效又尊重訪問的資源的網頁爬蟲。在處理像古騰堡計劃這樣的寶貴公共資源時,這一點尤為重要。

在雲端中的網頁爬蟲

該應用程式通過DigitalOcean的App平台利用現代雲架構和容器化技術。這種方法在開發簡單性和生產可靠性之間取得了完美平衡。

App平台的強大功能

App平台通過處理以下事項來簡化部署流程:

  • 網頁伺服器配置
  • SSL憑證管理
  • 安全更新
  • 負載平衡
  • 資源監控

這使我們可以專注於應用程式代碼,而App平台管理基礎設施。

容器中的無頭Chrome

我們的抓取功能核心使用了Puppeteer,這是一個高級API,用於以編程方式控制Chrome。以下是我們在應用中設置和使用Puppeteer的方法:

const puppeteer = require('puppeteer');

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

    async initialize() {
        if (!this.browser) {
            // 添加環境信息日誌以便調試
            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);
        }
    }

    // 使用Puppeteer進行抓取的示例
    async searchBooks(query, page = 1) {
        await this.initialize();
        await rateLimiter.wait();

        const browserPage = await this.browser.newPage();
        try {
            // 設置標頭以模擬真實瀏覽器並識別我們的機器人
            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' });
            
            // ... 其餘搜尋邏輯
        } finally {
            await browserPage.close();  // 總是做好清理工作
        }
    }
}

這個設置使我們能夠:

  • 以無頭模式運行Chrome(不需要GUI)
  • 在網頁上下文中執行JavaScript
  • 安全管理瀏覽器資源
  • 在容器化環境中可靠工作

該設置還包括幾個在容器化環境中運行的重要配置:

  1. 適當的Chrome參數:在容器中運行所需的基本標誌,如--no-sandbox--disable-dev-shm-usage
  2. 環境感知路徑:使用環境變數中的正確Chrome二進制路徑
  3. 資源管理:設置視口大小並禁用不必要的功能
  4. 專業機器人身份:清晰的用戶代理和HTTP標頭,以識別我們的抓取程序
  5. 錯誤處理: 正確清理瀏覽器頁面以預防內存洩漏

Puppeteer讓以程式方式控制Chrome變得容易,但在容器中運行它需要正確的系統依賴和配置。讓我們看看在我們的Docker環境中如何設置這個。

Docker: 確保一致的環境

部署網頁爬蟲的最大挑戰之一是確保它們在開發和生產環境中的運行方式相同。您的爬蟲在本機上可能運行完美,但由於缺少依賴項或不同的系統配置,在雲端可能失敗。Docker通過將應用程序所需的一切(從Node.js到Chrome本身)打包到一個容器中,在任何地方運行時都保持完全相同。

我們的Dockerfile設置了這個一致的環境:

FROM node:18-alpine

# 安裝Chromium和依賴項
RUN apk add --no-cache \
    chromium \
    nss \
    freetype \
    harfbuzz \
    ca-certificates \
    ttf-freefont \
    dumb-init

# 設置環境變量
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true \
    PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser \
    PUPPETEER_DISABLE_DEV_SHM_USAGE=true

基於Alpine的映像使我們的容器保持輕量級,同時包含所有必要的依賴項。當您在這個容器上運行時,無論是在您的筆記本電腦上還是在DigitalOcean的應用平台上,您都會得到相同的環境,所有正確的版本和配置都適用於運行無頭Chrome。

開發到部署

讓我們一起來看看如何將這個專案啟動和運行:

1. 本地開發

首先,將範例庫複製到你的GitHub帳戶中。這樣你就擁有了自己的副本可以進行開發和部署。然後將你的副本克隆到本地:

# 克隆你的副本
git clone https://github.com/YOUR-USERNAME/doappplat-puppeteer-sample.git
cd doappplat-puppeteer-sample

# 使用Docker構建和運行
docker build -t gutenberg-scraper .
docker run -p 8080:8080 gutenberg-scraper

2. 理解代碼

該應用程序的結構圍繞著三個主要組件:

  1. 書籍服務: 處理網絡爬蟲和數據提取

    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}`;
     
     // ... 爬取邏輯
    }
    
  2. Express 伺服器: 管理路由並渲染模板

    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) {
         // 錯誤處理
     }
    });
    
  3. 前端視圖: 使用 Bootstrap 的乾淨且響應式 UI

    <div class="card book-card h-100">
     <div class="card-body">
         <span class="badge bg-secondary downloads-badge">
             <%= book.downloads.toLocaleString() %> 下載次數
         </span>
         <h5 class="card-title"><%= book.title %></h5>
         <!-- ... 更多 UI 元素 ... -->
     </div>
    </div>
    

3. 部署到 DigitalOcean

現在您已經擁有了您的代碼庫分支,將其部署到 DigitalOcean App Platform 非常簡單:

  1. 創建一個新的 App Platform 應用程序
  2. 連接到您的分支代碼庫
  3. 在資源上,刪除第二個資源(那不是 Dockerfile);這是由 App Platform 自動生成的,不需要
  4. 通過點擊創建資源進行部署

該應用程序將自動構建和部署,App Platform 將處理所有基礎架構細節。

結論

這個 Project Gutenberg 抓取器展示了如何使用現代雲技術構建一個實用的網絡應用程序。通過將 Puppeteer 用於網頁抓取、Docker 用於容器化,以及 DigitalOcean 的 App Platform 用於部署,我們創建了一個既穩健又易於維護的解決方案。

該項目作為您自己網頁抓取應用程序的模板,展示了如何處理瀏覽器自動化、有效管理資源以及部署到雲端。無論您是在構建數據收集工具還是僅僅學習容器化應用程序,這個範例都提供了一個堅實的基礎來進行構建。

查看 GitHub 上的項目 以了解更多信息並部署您自己的實例!

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