웹 스크래핑이란?
웹 스크래핑은 웹 페이지에서 원하는 데이터를 자동으로 추출하는 기술입니다. 뉴스 모니터링, 가격 비교, 데이터 분석 등에 활용됩니다. 파이썬에서는 requests로 웹 페이지를 가져오고, BeautifulSoup으로 HTML을 파싱하는 조합이 가장 널리 사용됩니다.
이 글에서는 기본적인 HTML 파싱부터 에러 핸들링, 페이지네이션 처리, 그리고 예의 바른 스크래핑 패턴까지 정리합니다.
설치
pip install requests beautifulsoup4 lxml
# requests: HTTP 요청 라이브러리
# beautifulsoup4: HTML/XML 파서
# lxml: 고성능 파서 엔진 (선택사항이지만 권장)
기본 사용법 — 페이지 가져오기와 파싱
import requests
from bs4 import BeautifulSoup
# 1. 웹 페이지 가져오기
url = "https://example.com"
response = requests.get(url, timeout=10)
response.raise_for_status() # HTTP 에러 시 예외 발생
# 2. HTML 파싱
soup = BeautifulSoup(response.text, "lxml")
# 3. 데이터 추출
title = soup.find("title").get_text()
print(f"페이지 제목: {title}")
# 페이지 제목: Example Domain
# 모든 링크 추출
links = soup.find_all("a")
for link in links:
href = link.get("href", "없음")
text = link.get_text(strip=True)
print(f" {text} → {href}")
| 파서 | 속도 | 설치 | 특징 |
|---|---|---|---|
html.parser | 보통 | 내장 | 추가 설치 불필요 |
lxml | 빠름 | pip install lxml | C 기반, 권장 |
html5lib | 느림 | pip install html5lib | 브라우저와 동일한 파싱 |
CSS 선택자로 요소 찾기
select()와 select_one()은 CSS 선택자 문법을 사용하여 직관적으로 요소를 탐색합니다.
from bs4 import BeautifulSoup
html = """
<div class="product-list">
<div class="product" data-id="1">
<h3 class="name">파이썬 입문서</h3>
<span class="price">25,000원</span>
<span class="rating">★ 4.8</span>
</div>
<div class="product" data-id="2">
<h3 class="name">자바스크립트 핵심 가이드</h3>
<span class="price">32,000원</span>
<span class="rating">★ 4.5</span>
</div>
<div class="product" data-id="3">
<h3 class="name">Go 동시성 프로그래밍</h3>
<span class="price">28,000원</span>
<span class="rating">★ 4.9</span>
</div>
</div>
"""
soup = BeautifulSoup(html, "lxml")
# CSS 선택자로 모든 상품 추출
products = soup.select("div.product")
for product in products:
name = product.select_one("h3.name").get_text()
price = product.select_one("span.price").get_text()
rating = product.select_one("span.rating").get_text()
data_id = product.get("data-id")
print(f"[{data_id}] {name} | {price} | {rating}")
# [1] 파이썬 입문서 | 25,000원 | ★ 4.8
# [2] 자바스크립트 핵심 가이드 | 32,000원 | ★ 4.5
# [3] Go 동시성 프로그래밍 | 28,000원 | ★ 4.9
| 선택자 | 의미 | 예시 |
|---|---|---|
tag | 태그 이름 | soup.select("h3") |
.class | 클래스 | soup.select(".price") |
#id | ID | soup.select("#header") |
parent child | 하위 요소 | soup.select("div.product h3") |
[attr=val] | 속성 | soup.select("[data-id='1']") |
실전 패턴 — 안전한 스크래퍼 클래스
실제 스크래핑에서는 에러 핸들링, 요청 간 딜레이, User-Agent 설정 등이 필수입니다.
import requests
from bs4 import BeautifulSoup
import time
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class WebScraper:
"""예의 바른 웹 스크래퍼"""
def __init__(self, delay: float = 1.0):
self.session = requests.Session()
self.session.headers.update({
# User-Agent를 설정하여 봇 차단 방지
"User-Agent": (
"Mozilla/5.0 (compatible; "
"MyBot/1.0; +https://example.com/bot)"
),
"Accept-Language": "ko-KR,ko;q=0.9",
})
self.delay = delay # 요청 간 딜레이 (초)
def fetch_page(self, url: str) -> BeautifulSoup | None:
"""페이지를 가져와 파싱합니다."""
try:
response = self.session.get(url, timeout=15)
response.raise_for_status()
response.encoding = response.apparent_encoding
time.sleep(self.delay) # 서버 부하 방지
return BeautifulSoup(response.text, "lxml")
except requests.RequestException as e:
logger.error("요청 실패 [%s]: %s", url, e)
return None
def extract_text(self, soup: BeautifulSoup,
selector: str) -> str:
"""CSS 선택자로 텍스트를 안전하게 추출합니다."""
element = soup.select_one(selector)
return element.get_text(strip=True) if element else ""
# 사용 예시
scraper = WebScraper(delay=1.5)
soup = scraper.fetch_page("https://example.com")
if soup:
title = scraper.extract_text(soup, "title")
logger.info("페이지 제목: %s", title)
requests.Session을 사용하면 쿠키와 헤더가 자동으로 유지되어 로그인 상태를 유지하는 스크래핑에도 유용합니다.
페이지네이션 처리
여러 페이지에 걸친 데이터를 수집하는 패턴입니다.
import requests
from bs4 import BeautifulSoup
import time
def scrape_paginated(base_url: str,
max_pages: int = 10) -> list[dict]:
"""페이지네이션이 있는 사이트에서 데이터를 수집합니다."""
all_items = []
for page in range(1, max_pages + 1):
url = f"{base_url}?page={page}"
print(f"페이지 {page} 수집 중: {url}")
response = requests.get(url, timeout=15)
if response.status_code != 200:
print(f"페이지 {page} 요청 실패: {response.status_code}")
break
soup = BeautifulSoup(response.text, "lxml")
items = soup.select("div.item")
# 더 이상 항목이 없으면 중단
if not items:
print(f"페이지 {page}: 항목 없음 — 수집 종료")
break
for item in items:
all_items.append({
"title": item.select_one(".title").get_text(strip=True),
"link": item.select_one("a")["href"],
})
print(f" → {len(items)}개 항목 수집")
time.sleep(1.5) # 서버 부하 방지
print(f"총 {len(all_items)}개 수집 완료")
return all_items
데이터 저장 — CSV와 JSON
수집한 데이터를 파일로 저장하는 방법입니다.
import csv
import json
# CSV로 저장
def save_to_csv(data: list[dict], filename: str) -> None:
"""딕셔너리 리스트를 CSV 파일로 저장합니다."""
if not data:
return
with open(filename, "w", newline="", encoding="utf-8-sig") as f:
writer = csv.DictWriter(f, fieldnames=data[0].keys())
writer.writeheader()
writer.writerows(data)
print(f"CSV 저장 완료: {filename} ({len(data)}건)")
# JSON으로 저장
def save_to_json(data: list[dict], filename: str) -> None:
"""딕셔너리 리스트를 JSON 파일로 저장합니다."""
with open(filename, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
print(f"JSON 저장 완료: {filename} ({len(data)}건)")
# 사용 예시
products = [
{"name": "파이썬 입문서", "price": 25000},
{"name": "자바스크립트 핵심", "price": 32000},
]
save_to_csv(products, "products.csv")
save_to_json(products, "products.json")
# CSV 저장 완료: products.csv (2건)
# JSON 저장 완료: products.json (2건)
CSV 저장 시 encoding="utf-8-sig"를 사용하면 Excel에서 한글이 깨지지 않고 정상적으로 표시됩니다.
robots.txt 확인
스크래핑 전에 해당 사이트의 robots.txt를 확인하여 수집 허용 여부를 판단해야 합니다.
from urllib.robotparser import RobotFileParser
def can_scrape(url: str, user_agent: str = "*") -> bool:
"""robots.txt를 확인하여 스크래핑 허용 여부를 반환합니다."""
from urllib.parse import urlparse
parsed = urlparse(url)
robots_url = f"{parsed.scheme}://{parsed.netloc}/robots.txt"
rp = RobotFileParser()
rp.set_url(robots_url)
try:
rp.read()
return rp.can_fetch(user_agent, url)
except Exception:
return True # robots.txt를 읽을 수 없으면 허용으로 간주
# 사용 예시
target = "https://example.com/products"
if can_scrape(target):
print("스크래핑 허용됨")
else:
print("스크래핑 차단됨 — robots.txt에서 금지")
실전 팁
- robots.txt 준수: 스크래핑 전에 반드시 robots.txt를 확인합니다
- 요청 간 딜레이: 최소 1초 이상의 딜레이를 설정하여 서버에 부담을 주지 않습니다
- User-Agent 설정: 봇 정보를 포함한 User-Agent를 설정합니다
- 에러 핸들링: 네트워크 오류, 404, 파싱 실패 등에 대비합니다
- 세션 활용:
requests.Session으로 쿠키와 연결을 재사용합니다 - 인코딩 처리:
response.apparent_encoding으로 올바른 인코딩을 감지합니다 - 법적 유의사항: 저작권이 있는 콘텐츠, 개인정보 수집은 법적 문제가 될 수 있습니다
- 동적 페이지: JavaScript로 렌더링되는 페이지는
selenium또는playwright를 사용합니다