Python 웹 스크래핑 실전 가이드 — requests + BeautifulSoup

웹 스크래핑이란?

웹 스크래핑은 웹 페이지에서 원하는 데이터를 자동으로 추출하는 기술입니다. 뉴스 모니터링, 가격 비교, 데이터 분석 등에 활용됩니다. 파이썬에서는 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 lxmlC 기반, 권장
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")
#idIDsoup.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를 사용합니다

이 글이 도움이 되었나요?