Crawling

Scrapy + Playwright 매뉴얼 - 기본부터 실전까지

bluebamus 2025. 3. 2. 00:51

1. 설치 및 기본 설정

   1.1. 의존성 설치

      - 첫 번째 명령어는 scrapy-playwright 패키지를 설치하고, 두 번째 명령어는 필요한 브라우저(Chromium, Firefox, WebKit)를 설치한다.

pip install scrapy-playwright
playwright install

 

   1.2. Scrapy 설정 파일 (settings.py) 수정

      - Scrapy와 Playwright의 통합을 위해 settings.py에 다음 설정을 추가한다.

# settings.py

DOWNLOADER_MIDDLEWARES = {
    'scrapy_playwright.middleware.PlaywrightMiddleware': 543,
}

# Playwright 미들웨어 활성화
DOWNLOAD_HANDLERS = {
    "http": "scrapy_playwright.handler.PlaywrightDownloadHandler",
    "https": "scrapy_playwright.handler.PlaywrightDownloadHandler",
}

# 사용할 브라우저 종류 (chromium, firefox, webkit 중 선택)
PLAYWRIGHT_BROWSER_TYPE = "chromium"

# Playwright 실행 옵션 설정 (headless 모드, sandbox 옵션 등)
PLAYWRIGHT_LAUNCH_OPTIONS = {
    "headless": True,
    "args": ["--no-sandbox"],
}

# 기본 브라우저 타입 설정 (chromium, firefox, webkit 중 선택)
PLAYWRIGHT_BROWSER_TYPE = "chromium"

# 페이지 로딩이나 요소 대기 시간 등의 기본 타임아웃 설정 (필요 시 추가)
PLAYWRIGHT_DEFAULT_NAVIGATION_TIMEOUT = 30000  # 밀리초 단위
PLAYWRIGHT_DEFAULT_PAGE_WAIT_FOR_TIMEOUT = 10000

 

   1.3. 고급 설정 옵션

      - 브라우저 및 컨텍스트 설정
         - settings.py에서 다양한 브라우저 옵션을 설정할 수 있다.

# 브라우저 시작 옵션
PLAYWRIGHT_LAUNCH_OPTIONS = {
    "headless": False,  # 헤드리스 모드 비활성화
    "slowMo": 50,  # 동작 속도 조절 (밀리초)
    "proxy": {  # 프록시 설정
        "server": "http://proxy.example.com:8080",
        "username": "username",
        "password": "password"
    },
    "args": ["--disable-gpu"],  # 추가 브라우저 인수
}

# 브라우저 컨텍스트 설정
PLAYWRIGHT_BROWSER_CONTEXT_ARGS = {
    "viewport": {"width": 1920, "height": 1080},  # 뷰포트 크기
    "deviceScaleFactor": 1.0,  # 장치 스케일 팩터
    "userAgent": "Custom User Agent",  # 사용자 에이전트 설정
    "locale": "ko-KR",  # 로케일 설정
    "geolocation": {"latitude": 37.5665, "longitude": 126.9780},  # 지오로케이션 설정
    "permissions": ["geolocation"],  # 권한 설정
}

# 기본 타임아웃 설정
PLAYWRIGHT_DEFAULT_NAVIGATION_TIMEOUT = 60000  # 60초

 

   1.4. 로깅 설정

# 로깅 레벨 설정
LOG_LEVEL = "DEBUG"

# Playwright 관련 로그만 필터링
PLAYWRIGHT_LOGGER = "playwright"

 

   1.5. 병렬 처리

# settings.py에서 동시 요청 제한 설정
CONCURRENT_REQUESTS = 4
PLAYWRIGHT_CONTEXTS_LIMIT = 4  # 동시에 유지할 컨텍스트 수 제한

 

2. 기본 사용법

   - Scrapy Spider 내에서 Playwright를 사용하려면 요청 시 meta 옵션에 "playwright": True를 설정해야 한다.

   - 필요한 경우 페이지 객체를 직접 조작할 수 있도록 "playwright_include_page": True도 함께 지정한다.

 

   2.1. 기본 요청 예시

import scrapy

class ExampleSpider(scrapy.Spider):
    name = "example"
    start_urls = ["https://example.com"]

    def start_requests(self):
        for url in self.start_urls:
            yield scrapy.Request(
                url=url,
                meta={
                    "playwright": True,  # Playwright 사용 활성화
                    "playwright_include_page": True,  # 페이지 객체를 meta에 포함 (필요 시)
                },
                callback=self.parse
            )

    def parse(self, response):
        # Playwright를 통해 렌더링된 페이지에서 데이터 추출
        title = response.xpath('//title/text()').get()
        yield {"title": title}

        # 만약 playwright_include_page를 사용했다면 페이지 객체를 반드시 닫아줘야 함
        if "playwright_page" in response.meta:
            response.meta["playwright_page"].close()

 

   2.2. Playwright 페이지 메서드 사용

      - 추가적으로 페이지 상에서 특정 동작(예: 특정 요소 대기, 스크롤, 클릭 등)을 실행할 수 있다.

      - 이를 위해 meta에 "playwright_page_methods" 옵션을 지정한다.

yield scrapy.Request(
    url="https://example.com",
    meta={
        "playwright": True,
        "playwright_page_methods": [
            {"method": "wait_for_selector", "selector": "div.content"}
        ],
    },
    callback=self.parse
)

 

      -  위 예시는 요청한 페이지에서 div.content 요소가 나타날 때까지 대기하도록 한다.

 

3. 설정 옵션 상세 정리

   3.1. PLAYWRIGHT_BROWSER_TYPE

      - 설명:

         - Playwright에서 사용할 브라우저 종류를 지정한다.
      - 사용 가능 값: 

         - "chromium", "firefox", "webkit"
      - 기본값: 

         - 일반적으로 "chromium"이 사용된다.

 

   3.2. PLAYWRIGHT_LAUNCH_OPTIONS

      - 설명: 

         - 브라우저 실행 시 전달할 옵션들을 지정한다.

      - 예시 옵션:

         - headless: 

            - 브라우저를 헤드리스 모드(화면 없이 백그라운드 실행)로 실행할지 여부
         - args: 

            - 추가적인 커맨드라인 인자 (예: "--no-sandbox")

         - 활용: 

            - 성능 최적화나 CI/CD 환경에서의 안정성 확보를 위해 활용된다.

 

   3.3. PLAYWRIGHT_DEFAULT_NAVIGATION_TIMEOUT 및 PAGE_WAIT_FOR_TIMEOUT

      - 설명: 

         - 페이지 로딩 및 요소 대기 시간에 대한 타임아웃을 설정한다.

      - 단위: 

         - 밀리초(ms) 단위로 설정하며, 페이지 로딩이 오래 걸리는 사이트에 대해 유연하게 대처할 수 있다.

 

   3.4. PLAYWRIGHT_CONTEXT_ARGS

      - 설명: 

         - 브라우저 컨텍스트 생성 시 추가 옵션(예: 사용자 에이전트, 뷰포트 크기 등)을 지정할 수 있다.


      - 예시:

PLAYWRIGHT_CONTEXT_ARGS = {
    "user_agent": "Mozilla/5.0 (compatible; CustomBot/1.0)",
    "viewport": {"width": 1280, "height": 800},
}

 

4. 일반적으로 사용되는 Playwright 활용 기법

   4.1. 동적 페이지 처리

      - 대기 메서드: 

         - wait_for_selector, wait_for_load_state 등을 활용해 동적 요소가 로드될 때까지 기다린다.
      -  예시: 

         - AJAX로 로딩되는 콘텐츠를 대기 후 추출

 

   4.2. 페이지 조작 및 상호작용

      - 메서드 사용:

         - page.click, page.fill, page.press 등을 사용하여 버튼 클릭, 입력 필드 채우기, 키 입력 등의 동작을 구현할 수 있다.
      - 활용: 

         - 로그인 폼 제출, 필터 적용 등 사용자 인터랙션이 필요한 상황

 

   4.3. 네트워크 요청 가로채기

      - route 메서드: 

         - 특정 리소스(예: 광고, 추적 스크립트)를 차단하거나 수정하여 페이지 로딩 속도를 개선하거나 데이터 수집에 집중할 수 있다.

 

   4.4. 스크린샷 및 PDF 저장

      - 메서드: 

         - page.screenshot, page.pdf를 이용하여 페이지의 스냅샷이나 PDF 파일로 저장할 수 있다.

      - 활용: 

         - 크롤링 결과의 시각적 증거 확보나 보고서 작성 시 유용

 

5. 일반적인 사용 패턴

   스크린샷 캡쳐

def parse_with_page(self, response):
    page = response.meta["playwright_page"]
    
    # 스크린샷 저장
    screenshot_path = f"screenshots/{response.url.split('/')[-1]}.png"
    await page.screenshot(path=screenshot_path, full_page=True)
    
    await page.close()
    yield {"screenshot": screenshot_path}

 

6. 폼 입력 및 제출

from scrapy_playwright.page import PageMethod

def start_requests(self):
    yield scrapy.Request(
        "https://example.com/login",
        meta={
            "playwright": True,
            "playwright_include_page": True,
            "playwright_page_methods": [
                PageMethod("wait_for_selector", "#login-form"),
                # 폼 채우기
                PageMethod("fill", "#username", "my_username"),
                PageMethod("fill", "#password", "my_password"),
                # 폼 제출
                PageMethod("click", "#submit-button"),
                # 로그인 후 페이지 로드 기다리기
                PageMethod("wait_for_navigation"),
            ]
        },
        callback=self.parse_after_login
    )

 

7. 무한 스크롤 처리

def parse_infinite_scroll(self, response):
    page = response.meta["playwright_page"]
    
    # 초기 데이터 추출
    items = response.css("div.item::text").getall()
    
    # 더 많은 콘텐츠를 로드하기 위해 스크롤
    for _ in range(5):  # 5번 스크롤 다운
        await page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
        # 새 콘텐츠 로드 대기
        await page.wait_for_timeout(1000)  # 1초 대기
    
    # 최종 데이터 추출 (스크롤 후)
    final_items = await page.query_selector_all("div.item")
    for item in final_items:
        text = await item.text_content()
        items.append(text)
    
    await page.close()
    yield {"items": items}

 

8. 동적 콘텐츠 기다리기 간단 예시

def start_requests(self):
    yield scrapy.Request(
        "https://example.com/dynamic-content",
        meta={
            "playwright": True,
            "playwright_include_page": True,
            "playwright_page_methods": [
                # 네트워크 요청이 완료될 때까지 대기
                PageMethod("wait_for_load_state", "networkidle"),
                # JavaScript 실행이 완료될 때까지 대기
                PageMethod("wait_for_load_state", "domcontentloaded"),
                # 특정 AJAX 요청이 완료될 때까지 대기
                PageMethod("wait_for_response", "https://api.example.com/data"),
            ]
        },
        callback=self.parse_dynamic_content
    )

 

9. 쿠키 관리

# settings.py에서 쿠키 관리 설정
COOKIES_ENABLED = True
COOKIES_DEBUG = True

# 스파이더에서 쿠키 설정
def start_requests(self):
    yield scrapy.Request(
        "https://example.com",
        meta={
            "playwright": True,
            "playwright_include_page": True,
            "playwright_page_methods": [
                # 쿠키 설정
                PageMethod("add_cookies", [
                    {"name": "session_id", "value": "abc123", "domain": "example.com"}
                ]),
            ]
        },
        callback=self.parse_with_cookies
    )

def parse_with_cookies(self, response):
    page = response.meta["playwright_page"]
    # 현재 쿠키 가져오기
    cookies = await page.context.cookies()
    yield {"cookies": cookies}
    await page.close()


10. 실무 기반 프로젝트 예제

   10.1. 뉴스 사이트 동적 크롤링

      - 뉴스 사이트는 동적으로 콘텐츠를 로드하는 경우가 많다. 아래 예제는 뉴스 기사 리스트를 추출하는 Spider이다.

import scrapy

class NewsSpider(scrapy.Spider):
    name = "news"
    start_urls = ["https://news.example.com"]

    def start_requests(self):
        for url in self.start_urls:
            yield scrapy.Request(
                url=url,
                meta={
                    "playwright": True,
                    # 뉴스 기사 컨테이너(div.news-container)가 로드될 때까지 대기
                    "playwright_page_methods": [
                        {"method": "wait_for_selector", "selector": "div.news-container"}
                    ],
                },
                callback=self.parse
            )

    def parse(self, response):
        # XPath를 통해 뉴스 기사들을 추출
        articles = response.xpath('//div[@class="article"]')
        for article in articles:
            title = article.xpath('.//h2/text()').get()
            link = article.xpath('.//a/@href').get()
            yield {
                "title": title,
                "link": response.urljoin(link)
            }

 

      - 동작 설명:

         - meta 옵션:

            - "playwright":

               - True로 Playwright를 활성화하며, "playwright_page_methods"를 통해 특정 요소(div.news-container)가 나타날 때까지 대기한다.

 

         - 데이터 추출: 

            - XPath를 사용하여 뉴스 기사의 제목과 링크를 추출한다.

 

         - 링크 처리: 

            - 상대 경로를 절대 경로로 변환하기 위해 response.urljoin을 사용한다.

 

   10.2. 제품 리뷰 웹사이트 스크래핑 (무한 스크롤 처리)

      - 제품 리뷰 사이트는 무한 스크롤 방식으로 데이터를 로드하는 경우가 많다. 아래 예제는 스크롤을 통해 리뷰 데이터를 추출하는 Spider이다.

import scrapy

class ReviewSpider(scrapy.Spider):
    name = "reviews"
    start_urls = ["https://reviews.example.com/products"]

    def start_requests(self):
        for url in self.start_urls:
            yield scrapy.Request(
                url=url,
                meta={
                    "playwright": True,
                    # playwright_page 객체를 meta에 포함하여 직접 제어 (예: 스크롤 후 추가 로딩)
                    "playwright_include_page": True,
                    "playwright_page_methods": [
                        # 페이지의 가장 아래로 스크롤하여 추가 콘텐츠 로드
                        {"method": "evaluate", "script": "window.scrollBy(0, document.body.scrollHeight)"},
                        # 스크롤 후 3초 대기 (네트워크 요청 및 콘텐츠 로딩 시간 확보)
                        {"method": "wait_for_timeout", "timeout": 3000}
                    ],
                },
                callback=self.parse
            )

    def parse(self, response):
        # playwright_include_page 옵션으로 페이지 객체를 meta에 포함함
        page = response.meta["playwright_page"]

        # XPath로 리뷰 섹션 추출
        reviews = response.xpath('//div[@class="review"]')
        for review in reviews:
            reviewer = review.xpath('.//span[@class="name"]/text()').get()
            content = review.xpath('.//p[@class="content"]/text()').get()
            yield {
                "reviewer": reviewer,
                "content": content
            }
        # 메모리 누수를 방지하기 위해 페이지 객체를 닫음
        page.close()

 

      - 동작 및 설명:

         - 무한 스크롤 구현: 

            - "playwright_page_methods"를 통해 페이지 내에서 JavaScript 코드를 실행하여 스크롤 동작을 수행한다.

 

         - evaluate: 

            - JavaScript를 실행해 페이지를 아래로 스크롤한다.

 

         - wait_for_timeout: 

             - 스크롤 후 콘텐츠 로딩을 위해 3초 대기한다.

 

         - 데이터 추출: 

             - 리뷰어 이름과 리뷰 내용을 XPath로 추출한다.

 

         - 리소스 정리: 

             - Playwright 페이지 객체를 사용한 후 반드시 page.close()를 호출해 브라우저 리소스를 해제한다.

 

   10.3. 대기 조건 설정하기

      - 페이지가 완전히 로드될 때까지 기다리기 위한 대기 조건을 설정할 수 있다.

yield scrapy.Request(
    "https://example.com",
    meta={
        "playwright": True,
        "playwright_include_page": True,  # page 객체 포함
        "playwright_page_methods": [
            PageMethod("wait_for_selector", "div.content"),  # 특정 선택자가 나타날 때까지 대기
        ]
    },
    callback=self.parse_with_page
)

def parse_with_page(self, response):
    page = response.meta["playwright_page"]
    # page 객체를 사용하여 추가 작업 수행
    title = page.title()
    
    # 작업 완료 후 페이지 닫기
    await page.close()
    
    yield {"title": title}

 

   10.4. 이커머스 사이트 동적 상품 정보 크롤러

# ecommerce_crawler/spiders/product_spider.py

import scrapy
import json
from scrapy_playwright.page import PageMethod
import logging
from datetime import datetime

class ProductSpider(scrapy.Spider):
    name = "product_spider"
    
    # 크롤링할 카테고리 및 페이지 수 설정
    custom_settings = {
        'FEEDS': {
            f'data/products_{datetime.now().strftime("%Y%m%d_%H%M%S")}.json': {
                'format': 'json',
                'encoding': 'utf8',
                'indent': 4,
            },
        },
        'PLAYWRIGHT_DEFAULT_NAVIGATION_TIMEOUT': 60000,  # 60초로 타임아웃 설정
        'DOWNLOAD_DELAY': 2,  # 요청 간 딜레이 2초
    }
    
    def start_requests(self):
        # 여러 카테고리 페이지 크롤링
        categories = [
            {"id": "electronics", "pages": 3},
            {"id": "clothing", "pages": 2},
        ]
        
        for category in categories:
            for page in range(1, category["pages"] + 1):
                # 카테고리별 URL 생성
                url = f"https://example-ecommerce.com/category/{category['id']}?page={page}"
                
                self.logger.info(f"크롤링 시작: {url}")
                
                yield scrapy.Request(
                    url=url,
                    callback=self.parse_category_page,
                    meta={
                        "playwright": True,  # Playwright 활성화
                        "playwright_include_page": True,  # page 객체 포함
                        "playwright_page_methods": [
                            # 상품 목록이 로드될 때까지 대기
                            PageMethod("wait_for_selector", ".product-grid", timeout=10000),
                            # 페이지가 완전히 로드될 때까지 대기
                            PageMethod("wait_for_load_state", "networkidle"),
                            # 필터 메뉴가 펼쳐져 있다면 닫기 (선택적)
                            PageMethod("evaluate", """
                                () => {
                                    const filterBtn = document.querySelector('.filter-collapse-btn');
                                    if (filterBtn && filterBtn.getAttribute('aria-expanded') === 'true') {
                                        filterBtn.click();
                                    }
                                }
                            """),
                        ],
                        "category_id": category["id"],
                        "page_num": page,
                    }
                )
    
    async def parse_category_page(self, response):
        """
        카테고리 페이지에서 개별 상품 URL을 추출하고 각 상품 페이지로 요청을 생성합니다.
        """
        page = response.meta["playwright_page"]
        category_id = response.meta["category_id"]
        page_num = response.meta["page_num"]
        
        # 스크린샷 저장 (디버깅 용도)
        await page.screenshot(path=f"screenshots/{category_id}_page{page_num}.png", full_page=True)
        
        try:
            # 상품 목록에서 각 상품의 URL 추출
            product_links = response.css(".product-item a.product-link::attr(href)").getall()
            
            self.logger.info(f"카테고리 {category_id}, 페이지 {page_num}에서 {len(product_links)}개 상품 URL 추출됨")
            
            # 페이지 닫기 (메모리 관리를 위해 중요)
            await page.close()
            
            # 각 상품 페이지에 대한 요청 생성
            for product_url in product_links:
                # 상대 URL을 절대 URL로 변환
                if product_url.startswith('/'):
                    product_url = f"https://example-ecommerce.com{product_url}"
                
                yield scrapy.Request(
                    url=product_url,
                    callback=self.parse_product_page,
                    meta={
                        "playwright": True,
                        "playwright_include_page": True,
                        "playwright_page_methods": [
                            # 상품 상세 정보가 로드될 때까지 대기
                            PageMethod("wait_for_selector", ".product-details", timeout=10000),
                            # 동적으로 로드되는 가격 정보 대기
                            PageMethod("wait_for_selector", ".price-info", timeout=10000),
                            # AJAX 로드 완료 대기
                            PageMethod("wait_for_load_state", "networkidle"),
                        ],
                        "category_id": category_id,
                    }
                )
        except Exception as e:
            self.logger.error(f"카테고리 페이지 파싱 중 오류 발생: {str(e)}")
            await page.close()
    
    async def parse_product_page(self, response):
        """
        개별 상품 페이지에서 상세 정보를 추출합니다.
        동적으로 로드되는 재고, 리뷰, 가격 정보 등을 처리합니다.
        """
        page = response.meta["playwright_page"]
        category_id = response.meta["category_id"]
        
        try:
            # 기본 상품 정보 추출
            product_id = response.css("meta[property='product:id']::attr(content)").get()
            product_name = response.css("h1.product-title::text").get().strip()
            
            # 가격 정보 추출 (종종 동적으로 로드됨)
            price_info = await page.evaluate("""
                () => {
                    const priceElement = document.querySelector('.price-info .current-price');
                    return priceElement ? priceElement.innerText.trim() : null;
                }
            """)
            
            # 상품 이미지 URL 추출
            image_urls = response.css(".product-gallery img::attr(src)").getall()
            
            # 설명 정보 추출
            description = response.css(".product-description::text").get()
            if description:
                description = description.strip()
            
            # 재고 정보 (동적으로 로드되는 경우 JavaScript로 추출)
            stock_status = await page.evaluate("""
                () => {
                    const stockElement = document.querySelector('.stock-info');
                    return stockElement ? stockElement.innerText.trim() : "재고 정보 없음";
                }
            """)
            
            # 사양 정보 추출
            specifications = {}
            spec_rows = response.css(".product-specifications tr")
            for row in spec_rows:
                spec_name = row.css("th::text").get()
                spec_value = row.css("td::text").get()
                if spec_name and spec_value:
                    specifications[spec_name.strip()] = spec_value.strip()
            
            # 리뷰 정보 로드 (버튼 클릭을 통해 동적으로 로드되는 경우)
            review_count = 0
            average_rating = 0
            
            # 리뷰 탭 존재 여부 확인
            review_tab_exists = await page.query_selector(".reviews-tab")
            if review_tab_exists:
                # 리뷰 탭 클릭
                await page.click(".reviews-tab")
                await page.wait_for_selector(".review-list", timeout=5000)
                
                # 리뷰 수와 평균 평점 추출
                review_data = await page.evaluate("""
                    () => {
                        const reviewCountElement = document.querySelector('.review-count');
                        const ratingElement = document.querySelector('.average-rating');
                        return {
                            count: reviewCountElement ? parseInt(reviewCountElement.innerText.match(/\\d+/)[0]) : 0,
                            rating: ratingElement ? parseFloat(ratingElement.innerText) : 0
                        };
                    }
                """)
                
                review_count = review_data.get("count", 0)
                average_rating = review_data.get("rating", 0)
            
            # 최근 본 상품에 추가 (사용자 세션 추적에 유용)
            await page.evaluate(f"""
                () => {{
                    const recentlyViewed = JSON.parse(localStorage.getItem('recentlyViewed') || '[]');
                    if (!recentlyViewed.includes("{product_id}")) {{
                        recentlyViewed.unshift("{product_id}");
                        if (recentlyViewed.length > 10) recentlyViewed.pop();
                        localStorage.setItem('recentlyViewed', JSON.stringify(recentlyViewed));
                    }}
                }}
            """)
            
            # 스크린샷 저장
            await page.screenshot(path=f"screenshots/product_{product_id}.png")
            
            # 데이터 수집 완료, 페이지 닫기
            await page.close()
            
            # 결과 반환
            yield {
                "product_id": product_id,
                "product_name": product_name,
                "category_id": category_id,
                "price": price_info,
                "stock_status": stock_status,
                "description": description,
                "specifications": specifications,
                "image_urls": image_urls,
                "review_count": review_count,
                "average_rating": average_rating,
                "url": response.url,
                "crawled_at": datetime.now().isoformat(),
            }
            
        except Exception as e:
            self.logger.error(f"상품 페이지 파싱 중 오류 발생 ({response.url}): {str(e)}")
            # 오류 발생 시에도 페이지 닫기
            await page.close()


# ecommerce_crawler/settings.py

BOT_NAME = 'ecommerce_crawler'

SPIDER_MODULES = ['ecommerce_crawler.spiders']
NEWSPIDER_MODULE = 'ecommerce_crawler.spiders'

# Playwright 설정
DOWNLOAD_HANDLERS = {
    "http": "scrapy_playwright.handler.PlaywrightDownloadHandler",
    "https": "scrapy_playwright.handler.PlaywrightDownloadHandler",
}

DOWNLOADER_MIDDLEWARES = {
    'scrapy_playwright.middleware.PlaywrightMiddleware': 543,
}

# 브라우저 타입 설정
PLAYWRIGHT_BROWSER_TYPE = "chromium"

# 브라우저 시작 옵션
PLAYWRIGHT_LAUNCH_OPTIONS = {
    "headless": True,  # 헤드리스 모드 활성화
    "timeout": 30000,  # 브라우저 시작 타임아웃
    "args": [
        "--disable-gpu",
        "--no-sandbox",
        "--disable-dev-shm-usage",
        "--disable-setuid-sandbox",
    ],
}

# 브라우저 컨텍스트 설정
PLAYWRIGHT_BROWSER_CONTEXT_ARGS = {
    "viewport": {"width": 1366, "height": 768},
    "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
    "locale": "ko-KR",
}

# 동시 요청 제한
CONCURRENT_REQUESTS = 4
PLAYWRIGHT_CONTEXTS_LIMIT = 4

# 재시도 설정
RETRY_ENABLED = True
RETRY_TIMES = 3
RETRY_HTTP_CODES = [500, 502, 503, 504, 522, 524, 408, 429]

# 저장 경로 설정
FEEDS_TEMPDIR = 'tmp/'

# 캐시 설정
HTTPCACHE_ENABLED = True
HTTPCACHE_EXPIRATION_SECS = 86400  # 24시간
HTTPCACHE_DIR = 'httpcache'

# 로깅 설정
LOG_LEVEL = 'INFO'
LOG_FILE = 'logs/ecommerce_crawler.log'

# 아이템 파이프라인 설정
ITEM_PIPELINES = {
    'ecommerce_crawler.pipelines.PriceCleaningPipeline': 300,
    'ecommerce_crawler.pipelines.ImageDownloadPipeline': 400,
}


# ecommerce_crawler/pipelines.py

import re
from scrapy.pipelines.images import ImagesPipeline
from scrapy import Request
import os

class PriceCleaningPipeline:
    """가격 데이터 정제 파이프라인"""
    
    def process_item(self, item, spider):
        # 가격 데이터 정제
        if 'price' in item and item['price']:
            # 쉼표 제거 및 숫자만 추출
            price_str = item['price']
            price_num = re.sub(r'[^\d.]', '', price_str)
            
            try:
                item['price_value'] = float(price_num)
                item['price_currency'] = self._extract_currency(price_str)
            except ValueError:
                spider.logger.warning(f"가격 변환 실패: {price_str}")
                item['price_value'] = None
        
        return item
    
    def _extract_currency(self, price_str):
        """가격 문자열에서 통화 기호 추출"""
        currency_map = {
            '₩': 'KRW',
            '원': 'KRW',
            '$': 'USD',
            '€': 'EUR',
            '£': 'GBP',
        }
        
        for symbol, code in currency_map.items():
            if symbol in price_str:
                return code
        
        return 'UNKNOWN'


class ImageDownloadPipeline(ImagesPipeline):
    """상품 이미지 다운로드 파이프라인"""
    
    def get_media_requests(self, item, info):
        if 'image_urls' in item and item['image_urls']:
            for img_url in item['image_urls']:
                yield Request(
                    url=img_url,
                    meta={'product_id': item.get('product_id', 'unknown')}
                )
    
    def file_path(self, request, response=None, info=None, *, item=None):
        product_id = request.meta.get('product_id', 'unknown')
        image_name = request.url.split('/')[-1]
        return f'product_images/{product_id}/{image_name}'
    
    def item_completed(self, results, item, info):
        if results:
            # 다운로드된 이미지 경로 저장
            image_paths = [x['path'] for ok, x in results if ok]
            if image_paths:
                item['image_paths'] = image_paths
        
        return item


# 실행 명령:
# scrapy crawl product_spider

 

   10.5. 소셜 미디어 모니터링 크롤러

# social_media_monitor/spiders/twitter_spider.py

import scrapy
import json
import re
from scrapy_playwright.page import PageMethod
from datetime import datetime, timedelta
import logging
import csv
import os
from urllib.parse import quote

class TwitterSpider(scrapy.Spider):
    name = "twitter_monitor"
    
    # 크롤링 대상 키워드 및 설정
    custom_settings = {
        'DOWNLOAD_DELAY': 3,  # 요청 간 딜레이 3초
        'FEEDS': {
            f'data/twitter_posts_{datetime.now().strftime("%Y%m%d_%H%M%S")}.json': {
                'format': 'json',
                'encoding': 'utf8',
                'indent': 4,
            },
        },
        'PLAYWRIGHT_DEFAULT_NAVIGATION_TIMEOUT': 90000,  # 90초로 타임아웃 설정
    }
    
    def __init__(self, keywords=None, days_back=3, max_scrolls=10, *args, **kwargs):
        """
        초기화 함수로 크롤링 매개변수를 설정합니다.
        
        Args:
            keywords (str): 쉼표로 구분된 검색 키워드 목록
            days_back (int): 오늘부터 몇 일 전까지의 게시물을 크롤링할지 지정
            max_scrolls (int): 페이지당 최대 스크롤 횟수
        """
        super(TwitterSpider, self).__init__(*args, **kwargs)
        
        # 키워드 설정 (기본값 제공)
        self.keywords = keywords.split(',') if keywords else ["Python", "데이터분석", "인공지능"]
        
        # 날짜 범위 계산
        self.days_back = int(days_back)
        self.end_date = datetime.now()
        self.start_date = self.end_date - timedelta(days=self.days_back)
        
        # 스크롤 제한 설정
        self.max_scrolls = int(max_scrolls)
        
        # 결과 저장 디렉토리 생성
        os.makedirs('data', exist_ok=True)
        os.makedirs('screenshots', exist_ok=True)
        
        # CSV 요약 파일 초기화
        self.csv_path = f'data/twitter_summary_{datetime.now().strftime("%Y%m%d_%H%M%S")}.csv'
        with open(self.csv_path, 'w', newline='', encoding='utf-8') as f:
            writer = csv.writer(f)
            writer.writerow(['키워드', '총 게시물 수', '총 좋아요 수', '총 리트윗 수', '총 댓글 수', '평균 참여율'])
    
    def start_requests(self):
        """
        각 키워드에 대한 검색 URL을 생성하고 요청합니다.
        """
        # 날짜 형식 변환
        start_date_str = self.start_date.strftime("%Y-%m-%d")
        end_date_str = self.end_date.strftime("%Y-%m-%d")
        
        self.logger.info(f"크롤링 시작: 키워드 {self.keywords}, 기간 {start_date_str} ~ {end_date_str}")
        
        for keyword in self.keywords:
            # 검색 URL 인코딩 (고급 검색 쿼리 포함)
            encoded_query = quote(f"{keyword} since:{start_date_str} until:{end_date_str}")
            search_url = f"https://twitter.com/search?q={encoded_query}&src=typed_query&f=live"
            
            self.logger.info(f"키워드 '{keyword}' 검색 시작: {search_url}")
            
            yield scrapy.Request(
                url=search_url

 

- referfence : 

https://playwright.dev/docs/intro

 

Installation | Playwright

Introduction

playwright.dev

https://github.com/scrapy-plugins/scrapy-playwright

 

GitHub - scrapy-plugins/scrapy-playwright: 🎭 Playwright integration for Scrapy

🎭 Playwright integration for Scrapy. Contribute to scrapy-plugins/scrapy-playwright development by creating an account on GitHub.

github.com

https://scrapeops.io/python-scrapy-playbook/scrapy-playwright/

 

The Scrapy Playwright Guide | ScrapeOps

In this guide we show you how to use Scrapy Playwright to render and scrape Javascript heavy websites

scrapeops.io

https://www.zenrows.com/blog/scrapy-playwright#install-scrapy-playwright

https://magae5basement.tistory.com/5

 

[웹 스크래핑] Scrapy + playwright를 이용한 유튜브 스크래핑

Scrapy Scrapy는 대표적인 웹 스크래핑 도구로, 파이썬에서 사용 가능하다. 스크래핑(scraping)은 날카롭거나 예리한 도구를 이용해 표면에 붙은 어떤 것을 떼어내는 행위를 뜻한다. 이와 비슷하게, 웹

magae5basement.tistory.com

https://taejoone.jeju.onl/posts/2023-03-06-scrapy-playwright-day1/

 

Scrapy, Playwright 공부하기 - 1일차

Microsoft 에서 만든 Playwright 를 사용하여 웹 스크래핑 방법을 공부합니다. Scrapy 와 연동하거나 단독으로 사용할 수 있습니다.

taejoone.jeju.onl

https://taejoone.jeju.onl/posts/2023-03-15-scrapy-playwright-day2/

 

Scrapy, Playwright 공부하기 - 2일차

스크래핑은 데이터를 가져오는 행위를 말하고, 크롤링은 페이지 내의 링크를 수집하는 것을 말합니다. 본 글에서는 Scrapy 의 크롤링 모드를 사용해봅니다.

taejoone.jeju.onl

https://taejoone.jeju.onl/posts/2022-10-18-run-scrapy-on-jupyter/

 

Scrapy 사용법과 Jupyter 에서 Scrapy 실행하기

Scrapy 사용법을 소개하고, Jupyter 에서 오류 없이 실행하기 위한 방법을 설명합니다.

taejoone.jeju.onl