codingstairs
노트에듀라이프연락
⌕검색⌘K
koen

Navigation

  • Intro
  • Blog
  • Life

연락하기

로그인 없이도 보낼 수 있어요. 답변이 필요하면 이메일을 함께 적어 주세요.

  • 익명 폼으로 의견 남기기 →
  • ✉ warragon112@gmail.com
  • 카카오톡 오픈채팅 ↗

© 2026 codingstairs

  • 노트
  • 에듀
  • 검색
  • 라이프
  • 연락
  • 약관
  • RSS
  • GitHub
에듀›공공데이터 크롤러 만들기›2단계

2단계

정적 vs 동적 — BS4 + Playwright

0회 조회

정적 vs 동적 — BS4 + Playwright

잘못된 도구는 10 배 느리고 10 배 차단되기 쉬움. 먼저 "이 페이지가 정적이냐 동적이냐" 를 판단.

1. 정적 (서버 렌더)

curl https://example.com/page 결과에 원하는 데이터가 이미 포함.

  • 속도: 100ms ~ 300ms
  • 리소스: 적음
  • 도구: requests + BeautifulSoup · httpx

2. 동적 (JS 렌더)

페이지 소스에 빈 <div id="app"> 만 있고 JS 실행 후 렌더.

  • 속도: 2 ~ 10초
  • 리소스: 브라우저 메모리 수백 MB
  • 도구: Playwright · Selenium

3. 판단 방법

curl https://target.com/page | grep "원하는 데이터"
  • 매치 → 정적. BS4
  • 없음 → DevTools Network 탭 확인. XHR/fetch 요청 있으면 그 API 직접 호출 가능

4. 숨은 API 발견

많은 "동적" 사이트가 실제로는 REST API 호출. DevTools 에서 XHR 응답 확인:

GET /api/products?page=1
→ JSON { "items": [...] }

이 API 를 직접 호출하면 Playwright 없이 JSON 받음. 속도 · 안정성 압도적.

5. requests + BS4 기본

import httpx
from bs4 import BeautifulSoup

async with httpx.AsyncClient(headers={"User-Agent": "MyBot/1.0"}) as client:
    resp = await client.get("https://example.com/page")
    soup = BeautifulSoup(resp.text, "html.parser")

    items = soup.select("div.item")
    for item in items:
        title = item.select_one(".title").text.strip()
        price = item.select_one(".price").text.strip()
        yield {"title": title, "price": price}
  • CSS 선택자 (.select) 또는 태그 (.find)
  • 실패 시 None 방어 (.select_one(...) or default)

6. Playwright 기본

from playwright.async_api import async_playwright

async with async_playwright() as p:
    browser = await p.chromium.launch(headless=True)
    page = await browser.new_page()
    await page.goto("https://spa.example.com", wait_until="networkidle")
    # JS 로드 기다림
    await page.wait_for_selector(".item")
    html = await page.content()
    # BS4 로 파싱하거나 page.locator 로 직접
    titles = await page.locator(".item .title").all_inner_texts()
    await browser.close()
  • headless=True — UI 없이 (CI 에서 기본)
  • wait_until="networkidle" — 네트워크 2초 조용할 때까지 대기
  • wait_for_selector — 특정 요소 나타날 때까지

7. Playwright 최적화

대량 수집 시 기본 설정은 느림.

# 이미지 · 폰트 · 스타일 차단
await page.route("**/*.{png,jpg,jpeg,gif,svg,woff,woff2,css}", lambda r: r.abort())
await page.goto(url)

HTML + JS 만 로드. 속도 3 ~ 5 배.

8. 재사용 가능한 context

context = await browser.new_context(user_agent="MyBot/1.0")
page1 = await context.new_page()
page2 = await context.new_page()

쿠키 · 로컬스토리지 공유. 같은 도메인 다수 페이지 효율.

9. 하이브리드

목록 페이지는 Playwright 로 한 번 (로그인 · JS 렌더), 상세 페이지 URL 확보 후 BS4 로 병렬 수집.

urls = await extract_urls_with_playwright(list_page)
async with httpx.AsyncClient() as client:
    details = await asyncio.gather(*[fetch_bs4(client, u) for u in urls])

속도 · 안정성 균형.

10. 자주 걸리는 자리

  • 정적 페이지를 Playwright 로 — 10배 느리고 리소스 낭비
  • SPA 를 BS4 로 — 빈 HTML · 데이터 없음
  • 숨은 API 놓침 — DevTools Network 확인 습관
  • Playwright timeout — 기본 30초. 느린 사이트는 더 길게

하고픈 말

"먼저 curl, 그 다음 숨은 API, 마지막이 Playwright" 순서가 속도 · 안정성 · 예의 3 가지를 모두 지킵니다.

Next

  • 03-rate-limit-backoff

← 1단계

크롤러 윤리 · 법적 경계

3단계 →

rate limit · 재시도 · backoff