개인 프로젝트/DayCar 프로젝트

[DayCar] 리팩토링 일지 - 3편: 스크래퍼 개발

안기용 2025. 4. 17. 19:31

아키텍처가 나왔으니 개발을 진행합니다.

1. 크롤러 설계 방향

  • 수집 대상: 보배드림, K카 등 주요 중고차 거래 사이트
  • 설계 목표
    • 비동기 기반의 효율적인 수집
    • 관심사 분리(fetch / parse / load)
    • 사이트 추가 시 최소 변경만으로 확장 가능

2. 디렉토리 구조

scraper/
├── core/                # 공통 fetch, parse, load 인터페이스 정의
│   ├── fetcher.py
│   ├── parser.py
│   ├── loader.py
│   └── __init__.py
├── fetcher/             # 실제 페이지 요청 구현 (Playwright 등)
│   └── playwright_fetcher.py
├── parser/              # 사이트별 파서 정의
│   ├── a_parser.py
│   └── b_parser.py
├── loader/              # 추후 GCS, MinIO 등 저장소 확장 고려
├── pipeline/            # 전체 흐름을 조율하는 파이프라인 정의
│   └── pipelines.py
├── redis_client.py      # Redis 연결 및 큐 작업
├── main.py              # 실행 엔트리포인트
└── spark-jobs/          # 후처리 Spark Job
    └── etl/preprocessing.py

3. 컴포넌트 설명

✅ core/

  • 추상 클래스 또는 공통 인터페이스
  • 모든 fetcher/parser/loader의 베이스가 되는 모듈

✅ Fetcher

  • Playwright 기반으로 페이지를 비동기적으로 로드하고 HTML을 반환하는 모듈입니다.
# fetcher/playwright_fetcher.py

from playwright.async_api import async_playwright
import asyncio
import random

class PlaywrightFetcher:
    async def __aenter__(self):
        self.playwright = await async_playwright().start()
        self.browser = await self.playwright.chromium.launch(headless=True)
        self.page = await self.browser.new_page()
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        await self.browser.close()
        await self.playwright.stop()

    async def fetch(self, url: str) -> str:
        await self.page.goto(url, timeout=30000)
        await asyncio.sleep(random.uniform(1, 2))  # anti-ban
        return await self.page.content()

✅ parser/

  • BeautifulSoup을 사용해 사이트별 HTML 구조를 파싱하고, 필요한 필드를 추출합니다.
# parser/bobae_dream.py



from bs4 import BeautifulSoup

class BobaeParser(BaseParser):  
      async def parse(self, data: str):  
        soup = bs(data, "html.parser")
        records = []
        for product in soup.select("ul.clearfix li.product-item"):
            inner = product.select_one("div.list-inner")
            if not inner:
                continue
            record = ()
            for cell in inner.select("div.mode-cell"):
                classes = [c for c in cell.get("class", []) if c != "mode-cell"]
                if "title" in classes:
                    title = cell.select_one("p.tit a").get_text(strip=True)
                    subtitle_tag = cell.select_one("p.stxt a")
                    subtitle = subtitle_tag.get_text(strip=True) if subtitle_tag else ""
                    record += (title, subtitle)
                    continue
                if "seller" in classes:
                    record += self._parse_seller(cell)
                    continue
                if classes:
                    record += (cell.get_text(strip=True),)
            records.append(record)
        return records

✅ main

  • 전체 수집 흐름 예시입니다.
# main.py

import asyncio  
from fetcher.playwright\_fetcher import PlaywrightFetcher  
from parser.bobae\_dream import BobaeDreamParser  
from loader.redis\_loader import RedisLoader

async def main():  
url = "[https://example.com/cars?page=1"](https://example.com/cars?page=1")
async with PlaywrightFetcher() as fetcher:
    html = await fetcher.fetch(url)

records = BobaeDreamParser().parse(html)
RedisLoader().save(records)

if name == "__main__":  
    asyncio.run(main())

4. 개발 시 중점 사항

  • 모듈화: 공통 로직과 사이트별 로직을 철저히 분리
  • 비동기 처리: Playwright + asyncio 조합으로 성능 확보
  • 확장성: 새로운 사이트 추가가 매우 간단함 (parser/에 파일 추가만)