파이썬 교재 · 23편 / 27편

파이썬 웹 크롤링

정적 페이지는 BeautifulSoup, JS 동적 페이지는 Playwright.

실전읽는 시간 7분2026-05-13
requests 와 BeautifulSoup 로 HTML 에서 제목을 추출하는 코드 화면

22편의 자동화에 한 도구를 더하면 진짜 강력해집니다 — 웹에서 데이터 가져오기. 가격 비교·뉴스 모니터링·환율 추적·경쟁사 신제품 감시. 공식 API 가 없는 정보의 95% 는 웹 페이지에 HTML 로 노출돼 있어요. 그걸 자동 추출하는 게 크롤링(스크레이핑).

23편을 마치면 ① 정적/동적 페이지의 차이 ② requests + BeautifulSoup 기본 ③ CSS 선택자 ④ Playwright 로 JS 페이지 ⑤ 매너 — robots.txt·rate-limit·법적 경계 — 5가지를 손에 익힙니다.

정적 vs 동적 페이지 — 첫 분기

같은 페이지 URL 이라도 두 종류로 나뉩니다.

  • 정적 — 서버가 완성된 HTML 을 한 번에 보냄. 페이지 소스에 내가 보는 글자가 다 있음. requests + BeautifulSoup 으로 충분
  • 동적 — JS 가 브라우저에서 데이터를 추가로 받아 화면을 그림. 페이지 소스에는 빈 껍데기만. 실제 브라우저가 필요 → Playwright

판별: requests.get(URL).text 안에 찾는 내용이 있으면 정적, 없으면 동적. 동적이라도 보통은 view-source: 또는 개발자 도구 Network 탭에서 진짜 API URL 을 찾아 직접 호출하는 게 가장 빠릅니다.

requests + BeautifulSoup — 정적 정공법

(.venv) $ pip install requests beautifulsoup4 lxml

import requests
from bs4 import BeautifulSoup

UA = {"User-Agent": "Mozilla/5.0 (study-bot/1.0)"}

r = requests.get("https://example.com/blog", headers=UA, timeout=10)
r.raise_for_status()
soup = BeautifulSoup(r.text, "lxml")     # lxml 이 가장 빠른 파서

# 한 요소
title = soup.select_one("h1.post-title")
print(title.get_text(strip=True))

# 여러 요소
for art in soup.select("article.post"):
    t = art.select_one("h2").get_text(strip=True)
    link = art.select_one("a")["href"]
    print(t, "→", link)

select / select_one 이 핵심 — CSS 선택자로 찾습니다. 브라우저 개발자 도구에서 요소 우클릭 → "Copy → Copy selector" 가 자주 쓰는 트릭. 다음 표가 자주 쓰는 패턴.

📌 CSS 선택자 7가지

· h1 — 태그명

· .title — 클래스

· #main — id

· div.post — 태그 + 클래스

· article a — 자손

· li > a — 직계 자식

· a[href^="https"] — 속성 조건 (시작/끝/포함)

Playwright — JS 동적 페이지

(.venv) $ pip install playwright
(.venv) $ playwright install chromium     # 브라우저 다운로드

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=True)
    page = browser.new_page()
    page.goto("https://example-spa.com/products", timeout=30000)
    page.wait_for_selector("div.product-card")

    titles = page.eval_on_selector_all(
        "div.product-card h3",
        "els => els.map(e => e.textContent.trim())"
    )
    print(titles)
    browser.close()

실제 크롬을 띄워 JS 가 그린 결과를 받습니다. 무겁지만 React·Vue·Angular SPA 가 그린 화면도 그대로 봅니다.

대안: 브라우저 Network 탭 트릭

# JS 가 호출하는 API 를 찾아 직접 부른다 (가능하면 항상 이게 정공법)
r = requests.get(
    "https://api.example.com/v2/products",
    headers={"User-Agent": "...", "Accept": "application/json"},
    params={"page": 1},
)
data = r.json()
# Playwright 보다 100배 빠르고 안정적

Playwright 는 모든 다른 방법이 실패했을 때의 마지막 카드. JS 가 그린 페이지라도 그 데이터를 받아오는 API 가 있고, 개발자 도구에서 찾을 수 있습니다.

매너와 함정 + 마무리

⚠️ 크롤링 매너 5가지.robots.txt 확인 (example.com/robots.txt) — Disallow 경로는 안 가져간다. ② rate-limit — 한 사이트당 초당 1회 이하 권장, time.sleep(1). ③ User-Agent 와 연락처 — 봇이라고 솔직하게. ④ API 가 있으면 API 우선. ⑤ 저작권·약관 — 데이터 재배포는 별도 문제.
# 안전한 크롤러 뼈대
import time, requests
from bs4 import BeautifulSoup

UA = {"User-Agent": "research-bot/0.1 (+mailto:[email protected])"}

def fetch(url: str) -> BeautifulSoup:
    r = requests.get(url, headers=UA, timeout=10)
    r.raise_for_status()
    return BeautifulSoup(r.text, "lxml")

for url in urls:
    try:
        soup = fetch(url)
        # ... 추출 ...
    except requests.RequestException as e:
        log.error("실패 %s: %s", url, e)
    time.sleep(1.0)   # rate-limit 매너

다음 미션: ① 본인 블로그(또는 공개 뉴스 RSS) 의 글 제목 10개 추출 ② requests + BeautifulSoup 로 환율 한 페이지 크롤링 후 CSV 저장 ③ 개발자 도구로 어떤 사이트의 숨은 API URL 찾아보기.

다음 편 미리보기

24편 — "파이썬 async·await": 동시성 처리. 100개 URL 호출이 100배 빨라지는 비밀.

📚 27편 파이썬 교재 시리즈 — 23/27편
← 22편 "자동화" · 다음: 24편 "async·await"

© 2026 주나이테크(주) @JUNAITECH