본문으로 바로가기

5단계

5단계 — 외부 API · 크롤러 윤리

4회 조회

목차

외부 사이트의 데이터를 가져올 때 지켜야 할 예의기술 이 있어요. 안 지키면 차단되거나, 법적 문제가 생겨요.

크롤러 5계명

  1. robots.txt 를 먼저 본다 — 사이트 루트의 /robots.txt크롤 가능 영역 을 알려줍니다
  2. 요청 간격을 둔다 — 1초당 1회 이하 (rate limit)
  3. User-Agent 를 명시한다 — 어떤 봇인지 밝힘
  4. 캐시한다 — 같은 데이터를 두 번 요청하지 않음
  5. 공식 API 를 우선 시도 — RSS/Atom/Open API 가 있으면 그쪽

http_client 패턴

# utils/http_client.py
import time
import httpx

class RateLimitedClient:
    def __init__(self, base_url: str, requests_per_second: float = 1.0):
        self.client = httpx.Client(
            base_url=base_url,
            headers={"User-Agent": "example-crawler/1.0 (https://example.org)"},
            timeout=10.0,
        )
        self.min_interval = 1.0 / requests_per_second
        self.last_request_at = 0.0

    def get(self, path: str, **kwargs):
        elapsed = time.time() - self.last_request_at
        if elapsed < self.min_interval:
            time.sleep(self.min_interval - elapsed)
        self.last_request_at = time.time()
        return self.client.get(path, **kwargs)

이 한 클래스를 거치면 자동으로 rate limit + User-Agent 가 적용돼요.

robots.txt 파싱

from urllib.robotparser import RobotFileParser

rp = RobotFileParser()
rp.set_url("https://example.com/robots.txt")
rp.read()

if not rp.can_fetch("example-crawler/1.0", "https://example.com/some-page"):
    raise PermissionError("robots.txt 에서 차단된 경로")

기본은 허락받지 않은 곳은 가지 않음.

캐싱은 DB 또는 Redis

def get_with_cache(url: str, ttl_seconds: int = 3600):
    with get_conn() as conn, conn.cursor() as cur:
        cur.execute(
            "SELECT body, fetched_at FROM http_cache WHERE url = %s",
            (url,),
        )
        row = cur.fetchone()
        if row and (datetime.now(tz=UTC) - row[1]).total_seconds() < ttl_seconds:
            return row[0]

    body = client.get(url).text
    with get_conn() as conn, conn.cursor() as cur:
        cur.execute("""
            INSERT INTO http_cache (url, body, fetched_at)
            VALUES (%s, %s, NOW())
            ON CONFLICT (url) DO UPDATE SET body = EXCLUDED.body, fetched_at = NOW()
        """, (url, body))
    return body

Playwright — JavaScript 가 필요한 페이지

정적 HTML 만으론 안 되는 사이트는 Playwright 같은 진짜 브라우저 가 필요. 단, 비용이 100배 비싸므로 최후의 수단.

직접 해 보기

공개 API (예: https://jsonplaceholder.typicode.com) 의 게시물 목록을 1초 간격으로 5번 가져오는 스크립트를 만들어 보세요. RateLimitedClient 가 정확히 1초 간격을 지키는지 확인.

더 깊이

다음 단계

6단계에서는 가져온 데이터를 변환·저장 하는 ETL 파이프라인을 만들어요.