2단계
정적 vs 동적 — BS4 + Playwright
25 분
정적 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