디지털오션 앱 플랫폼에서 도커를 활용한 퍼펫티어 웹 스크레이퍼 구축

초장거리 마라톤 애호가로서 나는 종종 마주하는 공통된 도전 과제가 있습니다: 아직 시도해 보지 않은 더 긴 레이스의 완주 시간을 어떻게 예측할 수 있을까요? 나의 코치와 이에 대해 논의할 때, 그는 실용적인 방법을 제안했습니다 – 내가 완주한 레이스와 나의 목표로 하는 레이스 둘 다를 완주한 러너들을 살펴보는 것입니다. 이 상관 관계는 잠재적인 완주 시간에 대한 소중한 통찰을 제공할 수 있습니다. 그러나 레이스 결과를 수동으로 검색하는 것은 매우 시간이 많이 소요될 것입니다.

이러한 이유로 나는 Race Time Insights를 개발하게 되었습니다. 이 도구는 두 이벤트를 모두 완주한 선수들을 찾아 레이스 결과를 자동으로 비교합니다. 이 애플리케이션은 UltraSignup 및 Pacific Multisports와 같은 플랫폼에서 레이스 결과를 스크랩하며, 러너들이 두 레이스 URL을 입력하고 다른 선수들이 두 이벤트에서 어떻게 성과를 냈는지 확인할 수 있습니다.

이 도구를 만들면서 얼마나 강력한 DigitalOcean의 App Platform인지 알 수 있었습니다. Puppeteer와 헤드리스 Chrome을 Docker 컨테이너에서 사용함으로써, 러너들을 위한 문제 해결에 집중할 수 있었고 App Platform이 모든 인프라 복잡성을 처리해주었습니다. 결과적으로, 우리는 단단하고 확장 가능한 솔루션을 만들어 러닝 커뮤니티가 레이스 목표에 대한 데이터 기반 결정을 내릴 수 있게 도와주었습니다.

Race Time Insights를 개발한 후, 나는 다른 개발자들이 Puppeteer, Docker 컨테이너 및 DigitalOcean App Platform을 활용하는 방법에 대한 안내서를 작성하고자 했습니다. 물론, 외부 데이터를 처리할 때는 요청 속도 제한 및 서비스 약관과 같은 사항을 주의 깊게 고려해야 합니다.

프로젝트 구텐베르크에 들어가 보세요. 방대한 공공 도메인 도서 컬렉션과 명확한 서비스 약관을 갖춘 이곳은 이러한 기술을 시연하기에 이상적인 후보입니다. 이 게시물에서는 Puppeteer를 사용하여 Docker 컨테이너에서 책 검색 애플리케이션을 구축하는 방법을 탐구하고, 외부 데이터 접근에 대한 모범 사례를 따르겠습니다.

저는 프로젝트 구텐베르크에서 책 정보를 책임감 있게 스크랩하는 웹 애플리케이션을 구축하고 공유했습니다. 이 애플리케이션은 GitHub 저장소에서 찾을 수 있으며, 사용자가 수천 권의 공공 도메인 도서를 검색하고 각 도서에 대한 자세한 정보를 조회하며 다양한 다운로드 형식에 접근할 수 있도록 합니다. 특히 흥미로운 점은 사용자가 진정한 가치를 제공하면서 책임감 있는 웹 스크래핑 관행을 시연한다는 것입니다.

좋은 디지털 시민이 되기

웹 스크래퍼를 구축할 때는 좋은 관행을 따르고 기술적 및 법적 경계를 존중하는 것이 중요합니다. 프로젝트 구텐베르크는 이러한 원칙을 배우기에 훌륭한 예시입니다. 그 이유는:

  1. 명확한 서비스 약관을 가지고 있기 때문입니다.
  2. 로봇.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를 사용하거나 확장 가능성을 높이기 위해 대기열 기반 시스템을 구현할 수 있습니다.

이 속도 제한기는 Project Gutenberg에 대한 모든 요청 전에 사용됩니다:

async searchBooks(query, page = 1) {
    await this.initialize();
    await rateLimiter.wait();  // 속도 제한 강제
    // ... 검색 로직의 나머지 부분
}

async getBookDetails(bookUrl) {
    await this.initialize();
    await rateLimiter.wait();  // 속도 제한 강제
    // ... 세부 로직의 나머지 부분
}

봇 식별 정보 지우기

사용자 지정 User-Agent는 웹 사이트 관리자가 누가 사이트에 접속하고 왜 그렇게 하는지 이해할 수 있게 돕습니다. 이 투명성을 통해 그들은 다음을 할 수 있습니다:

  1. 문제가 발생할 경우 연락
  2. 봇 트래픽과 인간 사용자를 별도로 모니터링하고 분석
  3. 합법적인 스크레이퍼에 대해 더 나은 액세스 또는 지원 제공 가능성
await browserPage.setUserAgent('GutenbergScraper/1.0 (Educational Project)');

효율적인 자원 관리

Chrome은 여러 인스턴스를 실행할 때 특히 메모리를 많이 사용합니다. 사용 후 브라우저 페이지를 올바르게 닫음으로써 메모리 누수를 방지하고 응용 프로그램이 효율적으로 실행되도록 보장합니다. 많은 요청을 처리할 때에도 효율적으로 작동합니다:

try {
    // ... 스크레이핑 로직
} finally {
    await browserPage.close();  // 메모리와 시스템 자원 해제
}

이러한 관행을 따르면 접근하는 자원을 효과적으로 활용하면서도 존중하는 스크레이퍼를 만들 수 있습니다. 이는 Project Gutenberg와 같은 소중한 공공 자원과 작업할 때 특히 중요합니다.

클라우드에서의 웹 스크레이핑

이 응용 프로그램은 DigitalOcean의 App Platform을 통해 현대적인 클라우드 아키텍처와 컨테이너화를 활용합니다. 이 접근 방식은 개발의 간편함과 운영 신뢰성 사이의 완벽한 균형을 제공합니다.

App Platform의 강점

App Platform은 다음을 처리함으로써 배포 프로세스를 간소화합니다:

  • 웹 서버 구성
  • SSL 인증서 관리
  • 보안 업데이트
  • 부하 분산
  • 리소스 모니터링

이를 통해 App Platform이 인프라를 관리하면서 우리는 응용 프로그램 코드에 집중할 수 있습니다.

컨테이너 내 Headless Chrome

저희 스크래핑 기능의 핵심은 Puppeteer를 사용하는데 있습니다. 이는 Chrome을 프로그래밍 방식으로 제어하기 위한 고수준 API를 제공합니다. 우리의 애플리케이션에서 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 실행(그래픽 사용 불필요)
  • 웹 페이지 컨텍스트에서 JavaScript 실행
  • 브라우저 리소스 안전하게 관리
  • 컨테이너화된 환경에서 안정적으로 작업

이 설정은 컨테이너화된 환경에서 실행하기 위한 여러 중요한 구성을 포함합니다:

  1. 적절한 Chrome 인수: 컨테이너에서 실행하기 위한 --no-sandbox--disable-dev-shm-usage와 같은 필수 플래그
  2. 환경 인식 경로: 환경 변수에서 올바른 Chrome 실행 파일 경로 사용
  3. 리소스 관리: 뷰포트 크기 설정 및 불필요한 기능 비활성화
  4. 전문적인 봇 식별: 스크래퍼를 식별하는 명확한 사용자 에이전트 및 HTTP 헤더 설정
  5. 오류 처리: 메모리 누수를 방지하기 위한 브라우저 페이지의 적절한 정리

Puppeteer는 Chrome을 프로그래밍 방식으로 쉽게 제어할 수 있게 해줍니다. 그러나 컨테이너에서 실행하려면 적절한 시스템 종속성과 구성이 필요합니다. 이제 우리의 도커 환경에서 이를 어떻게 설정하는지 살펴보겠습니다.

도커: 일관된 환경 보장

웹 스크레이퍼를 배포하는 가장 큰 어려움 중 하나는 개발 환경과 프로덕션 환경에서 동일하게 작동함을 보장하는 것입니다. 로컬 컴퓨터에서 스크레이퍼가 완벽히 작동할 수 있지만 클라우드에서는 종속성이 누락되거나 시스템 구성이 다르기 때문에 실패할 수 있습니다. 도커는 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

알파인 기반 이미지는 모든 필요한 종속성을 포함하면서도 컨테이너를 가볍게 유지합니다. 이 컨테이너를 실행할 때, 노트북에서든 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. 익스프레스 서버: 라우트를 관리하고 템플릿을 렌더링합니다

    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. 프론트엔드 뷰: 부트스트랩을 사용한 깔끔하고 반응형 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. 디지털오션에 배포하기

저장소의 포크가 준비되었으니, 디지털오션 앱 플랫폼에 배포하는 것은 간단합니다:

  1. 새로운 앱 플랫폼 애플리케이션 만들기
  2. 포크한 저장소에 연결하기
  3. 리소스에서 두 번째 리소스(도커파일이 아닌)를 삭제하세요; 이는 앱 플랫폼에 의해 자동 생성된 것으로 필요하지 않습니다
  4. 리소스 생성 클릭하여 배포하기

애플리케이션은 자동으로 빌드되고 배포되며, 앱 플랫폼이 모든 인프라 세부 사항을 처리합니다.

결론

이 Project Gutenberg 스크레이퍼는 현대적인 클라우드 기술을 활용하여 실용적인 웹 응용 프로그램을 구축하는 방법을 보여줍니다. 웹 스크래핑을 위해 Puppeteer, 컨테이너화를 위해 Docker, 그리고 배포를 위해 DigitalOcean의 앱 플랫폼을 결합하여 견고하고 유지 보수가 쉬운 솔루션을 만들었습니다.

이 프로젝트는 당신의 웹 스크래핑 응용 프로그램을 위한 템플릿으로 작용하며, 브라우저 자동화를 처리하는 방법, 자원을 효율적으로 관리하는 방법, 그리고 클라우드로 배포하는 방법을 보여줍니다. 데이터 수집 도구를 구축하거나 컨테이너화된 응용 프로그램을 학습하고 있다면, 이 예는 튼튼한 기반을 제공하여 발전시킬 수 있습니다.

자세한 내용 및 자신의 인스턴스를 배포하는 방법은 GitHub의 프로젝트를 확인하세요!

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