Web crawling(웹 크롤링)과 web scraping(웹 스크레이핑)을 구분하고, 웹페이지의 데이터 제공 방식에 맞춰 가장 효율적인 수집 방법을 제시한다. 정적 페이지는 SSR(Server-Side Rendering, 서버사이드 렌더링)로 HTML에 데이터가 내장되어 있고, 동적 페이지는 CSR(Client-Side Rendering, 클라이언트사이드 렌더링) 또는 하이브리드로 JSON format의 API (Application Programming Interface) 응답을 받아 렌더링한다. DevTools(Network/Elements)를 통해 구조를 판별한 뒤, 정적은 requests + BeautifulSoup로, 동적은 Playwright·Puppeteer·Selenium 또는 직접 API 호출로 처리한다. 대규모 수집은 Scrapy로 파이프라인화하고, 배포는 Docker 컨테이너로 FaaS(Function as a Service, 함수형 서비스)나 PaaS(Platform as a Service, 플랫폼형 서비스)에 올리는 것이 효율적이다.
서론
핵심 질문은 다음과 같다.
1) 웹페이지가 데이터를 어디서 어떻게 공급하는가?
2) 그 구조에 가장 맞는 최소 비용·최대 안정성 도구 조합은 무엇인가?
답은 DevTools로 구조를 먼저 식별한 뒤, 구조에 맞춘 최소한의 스택으로 구현·확장하는 것이다.
본론 1: 분류 체계와 판별 절차
- 기준 용어 정의
- DevTools 판별 3단계
- 보조 시그널
본론 2: 구조별 추천 도구·방법 매핑
- 정적(SSR/SSG)
- 동적(CSR/하이브리드)
- 대체 경로
- 상용/클라우드
본론 3: 의사결정 트리
- Elements에서 데이터가 보이는가? 보이면 requests + BeautifulSoup.
- Network에 JSON/XHR가 보이는가? 보이면 API 재현을 시도.
- 1·2가 막히면 Playwright로 렌더 후 DOM 또는 response interception으로 추출.
- 전역 크롤링이면 Scrapy로 링크 추출·큐·파이프라인 구현. 동적은 scrapy-playwright 병행.
- 반봇·레이트리밋 대응이 필요하면 세션 유지, 지수 백오프, 캐시, 존중 가능한 속도 설정. 합법성·robots.txt·약관 준수.
본론 4: 최소 구현 레시피 예시
- 정적 페이지 파싱
import requests
from bs4 import BeautifulSoup
url = "https://example.com/page"
res = requests.get(url, timeout=20)
res.raise_for_status()
soup = BeautifulSoup(res.text, "lxml")
data = [e.get_text(strip=True) for e in soup.select("table#prices tr td:nth-child(2)")]
print(data)
- Network에서 본 JSON API 재현
import httpx
api = "https://example.com/api/items"
headers = {
"User-Agent": "Mozilla/5.0",
"Accept": "application/json",
"X-Requested-With": "XMLHttpRequest",
}
params = {"page": 1, "q": "keyword"}
with httpx.Client(timeout=20, headers=headers) as client:
items = []
while True:
r = client.get(api, params=params)
r.raise_for_status()
payload = r.json()
items.extend(payload["data"])
if not payload.get("next_page"):
break
params["page"] += 1
print(len(items))
- 스크립트 태그에 직렬화된 JSON 추출
import re, json
from bs4 import BeautifulSoup
import requests
html = requests.get("https://example.com").text
soup = BeautifulSoup(html, "lxml")
script = soup.find("script", string=re.compile("__NEXT_DATA__|__INITIAL_STATE__"))
obj = json.loads(script.string)
print(obj.keys())
- Playwright로 동적 렌더링 및 XHR 가로채기
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
ctx = browser.new_context()
page = ctx.new_page()
captured = []
page.on("response", lambda resp: captured.append(resp) if "api" in resp.url else None)
page.goto("https://example.com/list", wait_until="networkidle")
html = page.content()
data = [r.json() for r in captured if r.request.resource_type == "xhr"]
browser.close()
print(len(html), len(data))
- Scrapy 스켈레톤
import scrapy
class ItemsSpider(scrapy.Spider):
name = "items"
start_urls = ["https://example.com/list"]
def parse(self, response):
for href in response.css("a.item::attr(href)").getall():
yield response.follow(href, self.parse_item)
def parse_item(self, response):
yield {
"title": response.css("h1::text").get(),
"price": response.css(".price::text").get(),
}
- scrapy-playwright 설정 요지
# settings.py
DOWNLOADER_MIDDLEWARES = {"scrapy_playwright.middleware.ScrapyPlaywrightDownloaderMiddleware": 543}
PLAYWRIGHT_BROWSER_TYPE = "chromium"
PLAYWRIGHT_DEFAULT_NAVIGATION_TIMEOUT = 30000
본론 5: 신뢰성·확장성 체크리스트
- 정확성
- 성능
- 유지보수
- 합법·윤리
본론 6: 상용 서비스와 자체 호스팅 비교 요약
- Apify: 빠른 시작, 4,000+ Actor. 비용 증가와 커스터마이징 제약이 단점.
- ThunderBit: 유사 FaaS. 비용·기능 비교 필요.
- 자체 호스팅 권장 패턴
결론
데이터 전송 방식 판별이 먼저이고 그에 따른 도구 선택은 그 다음에 결정한다. SSR/SSG는 requests + BeautifulSoup, CSR/하이브리드는 API 재현 우선, 필요 시 Playwright. 전역·대량은 Scrapy로 파이프라인화하고, 배포는 컨테이너 기반 FaaS/PaaS로 표준화한다. DevTools 중심의 구조 판별과 최소 스택 원칙이 비용·안정성을 동시에 달성하는 최적 경로다.