본문 바로가기
자기 개발/Python

파이썬(Python) 공부 13편 — 웹 스크래핑 기초 완전 정복 | requests·BeautifulSoup·CSS선택자·실전 크롤링까지

by conrad 2026. 4. 3.
13 / 15 Python 공부 시리즈 — 웹 스크래핑 기초
← 12편: 엑셀·CSV 자동화 완전 정복 보러 가기
Python 공부 시리즈 · 13편 | requests · BeautifulSoup · 실전 크롤링

파이썬(Python) 공부 13편
웹 스크래핑 기초 완전 정복

웹에 있는 데이터를 자동으로 긁어오는 것, 바로 웹 스크래핑입니다. 매일 뉴스 헤드라인을 엑셀에 저장하거나, 쇼핑몰 가격을 자동으로 수집하거나, 날씨 정보를 주기적으로 가져오는 작업 — 파이썬 코드 몇 십 줄이면 가능합니다. requests로 웹 페이지를 가져오고, BeautifulSoup으로 원하는 데이터만 뽑아내는 방법을 실전 예제와 함께 정리합니다.

HTTP 요청 — requests HTML 파싱 — BeautifulSoup CSS 선택자 활용 실전 뉴스·날씨 크롤링 robots.txt·크롤링 예절

🌎 웹 스크래핑이란 — 어떤 원리로 동작할까

우리가 브라우저에서 웹 페이지를 열면, 브라우저가 서버에 HTTP 요청을 보내고 서버가 HTML 문서를 돌려줍니다. 브라우저는 이 HTML을 해석해 화면에 보여줍니다. 웹 스크래핑은 이 과정을 파이썬 코드가 대신 수행하는 것입니다.

파이썬은 두 가지 역할을 나눠 처리합니다. requests가 HTTP 요청을 보내 HTML 텍스트를 가져오고, BeautifulSoup이 그 HTML을 파싱 해서 원하는 데이터를 추출합니다.

requests 웹 페이지 가져오기
  • HTTP GET / POST 요청 전송
  • 응답 HTML 텍스트 수신
  • 헤더·쿠키·세션 처리
  • 타임아웃·에러 핸들링
BeautifulSoup4 HTML에서 데이터 추출
  • HTML·XML 파싱
  • 태그·클래스·id로 검색
  • CSS 선택자 지원
  • 텍스트·속성 값 추출
설치

pip install requests beautifulsoup4 lxml

lxml은 HTML 파서로, html.parser(기본 내장)보다 속도가 빠르고 불완전한 HTML도 잘 처리합니다. 함께 설치해 두는 것이 좋습니다.


📱 requests 기초 — HTTP 요청 보내기

 
requests_basic.py — GET 요청과 응답 처리
import requests

# ─── 기본 GET 요청 ────────────────────────────
url = "https://httpbin.org/get"   # 테스트용 공개 API
res = requests.get(url)

print(res.status_code)    # 200 = 성공 / 404 = 없음 / 403 = 차단
print(res.text)           # HTML 문자열 전체
print(res.encoding)       # 인코딩 (utf-8 등)

# ─── 헤더 설정 (User-Agent 위장) ─────────────
# 일부 사이트는 봇 요청을 막음 → 브라우저처럼 보이게 설정
headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                   "AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36"
}
res = requests.get(url, headers=headers)

# ─── 쿼리 파라미터 전달 ──────────────────────
params = {"q": "파이썬", "page": 1}
res = requests.get(url, params=params)
# → url?q=파이썬&page=1 로 자동 변환

# ─── 타임아웃 + 에러 처리 ────────────────────
try:
    res = requests.get(url, timeout=5)   # 5초 안에 응답 없으면 에러
    res.raise_for_status()              # 4xx, 5xx 에러 시 예외 발생
    print("성공:", res.status_code)
except requests.exceptions.Timeout:
    print("타임아웃 — 서버 응답 없음")
except requests.exceptions.HTTPError as e:
    print(f"HTTP 에러: {e}")
except requests.exceptions.RequestException as e:
    print(f"요청 에러: {e}")

# ─── 세션 사용 (쿠키·로그인 유지) ───────────
session = requests.Session()
session.headers.update(headers)   # 세션 전체에 헤더 적용
res1 = session.get("https://example.com/login")
res2 = session.get("https://example.com/mypage")  # 쿠키 자동 유지

🍰 BeautifulSoup 기초 — HTML 파싱과 데이터 추출

requests로 가져온 HTML 텍스트를 BeautifulSoup에 넘기면 파싱 트리(Parsing Tree)가 만들어집니다. 이 트리를 탐색해 원하는 태그와 텍스트를 꺼내는 것이 핵심입니다.

 
bs4_basic.py — BeautifulSoup 기초 파싱
from bs4 import BeautifulSoup
import requests

html = """
<html>
  <head><title>파이썬 뉴스</title></head>
  <body>
    <div class="news-list">
      <article id="a1" class="news-item hot">
        <h2><a href="/news/1">파이썬 3.13 릴리즈</a></h2>
        <p class="summary">새로운 기능과 성능 개선</p>
        <span class="date">2026-04-01</span>
      </article>
      <article id="a2" class="news-item">
        <h2><a href="/news/2">AI와 파이썬의 미래</a></h2>
        <p class="summary">머신러닝 생태계 전망</p>
        <span class="date">2026-03-30</span>
      </article>
    </div>
  </body>
</html>
"""

# ─── 파서 생성 ────────────────────────────────
soup = BeautifulSoup(html, "lxml")   # lxml 권장 / "html.parser" 가능

# ─── 단일 요소 찾기 — find() ─────────────────
print(soup.find("title").text)               # 파이썬 뉴스
print(soup.find("h2").text)                  # 파이썬 3.13 릴리즈
print(soup.find("span", class_="date").text)  # 2026-04-01
print(soup.find(id="a2").find("p").text)     # 머신러닝 생태계 전망

# ─── 여러 요소 찾기 — find_all() ─────────────
articles = soup.find_all("article")
for art in articles:
    title   = art.find("h2").text
    summary = art.find("p").text
    date    = art.find("span").text
    print(f"[{date}] {title} — {summary}")

# ─── 속성 값 가져오기 ────────────────────────
links = soup.find_all("a")
for link in links:
    print(link["href"], link.text)   # href 속성값
    # 또는: link.get("href")  ← 속성 없어도 None 반환 (안전)

# ─── class 여러 개 조건 ──────────────────────
hot = soup.find_all("article", class_="hot")   # hot 클래스 포함된 것만
print(len(hot))   # 1

🌟 CSS 선택자로 정밀하게 추출하기 — select()

select()select_one()은 CSS 선택자 문법을 그대로 사용할 수 있어 복잡한 구조에서 특히 강력합니다. 브라우저 개발자 도구에서 "Copy selector"로 바로 가져다 쓸 수 있어 실무에서 많이 사용합니다.

선택자 의미 예시
태그 태그 이름으로 선택 soup.select("h2")
.클래스 클래스명으로 선택 soup.select(".news-item")
#아이디 id로 선택 soup.select_one("#a1")
부모 자식 공백: 하위 모든 자손 soup.select("div p")
부모 > 자식 > : 직계 자식만 soup.select("div > p")
태그.클래스 태그+클래스 동시 soup.select("article.hot")
[속성] 속성 보유 요소 soup.select("a[href]")
[속성=값] 속성값 일치 soup.select('a[target="_blank"]')
:nth-child(n) n번째 자식 soup.select("li:nth-child(2)")
 
css_selector.py — select() 활용
from bs4 import BeautifulSoup

# 위 html 변수 재사용
soup = BeautifulSoup(html, "lxml")

# ─── select() — 여러 개 반환 ─────────────────
titles = soup.select("article h2 a")        # article 안의 h2 안의 a
for t in titles:
    print(t.text, t.get("href"))

# ─── select_one() — 첫 번째 하나만 ──────────
first = soup.select_one("article.hot .summary")
print(first.text)          # 새로운 기능과 성능 개선

# ─── 복합 선택자 ─────────────────────────────
dates = soup.select("article span.date")
for d in dates:
    print(d.text)

# ─── 결과를 딕셔너리 리스트로 정리 ──────────
news_data = []
for art in soup.select("article"):
    news_data.append({
        "title"  : art.select_one("h2 a").text.strip(),
        "url"    : art.select_one("h2 a").get("href"),
        "summary": art.select_one(".summary").text.strip(),
        "date"   : art.select_one(".date").text.strip(),
    })
print(news_data)

📊 실전 ① HTML 테이블 데이터 수집 → pandas 저장

파이썬 웹 스크래핑 크롤링 코드 ▲ 웹 스크래핑은 반복 작업을 자동화하는 강력한 도구다. requests로 페이지를 가져오고 BeautifulSoup으로 원하는 데이터를 추출한 후 CSV나 Excel로 저장하는 패턴이 가장 기본적인 형태다. (출처: Unsplash / 참고 이미지)
 
scrape_table.py — HTML 테이블 추출 + pandas 저장
import requests
import pandas as pd
from bs4 import BeautifulSoup

# ─── Wikipedia 표 예시 (공개 데이터) ─────────
url  = "https://en.wikipedia.org/wiki/List_of_countries_by_population_(United_Nations)"
headers = {"User-Agent": "Mozilla/5.0"}
res  = requests.get(url, headers=headers, timeout=10)
soup = BeautifulSoup(res.text, "lxml")

# ─── 첫 번째 wikitable 찾기 ──────────────────
table = soup.find("table", class_="wikitable")

rows    = []
headers_ = [th.text.strip() for th in table.find("tr").find_all("th")]

for tr in table.find_all("tr")[1:]:   # 헤더 행 제외
    cells = [td.text.strip() for td in tr.find_all(["td", "th"])]
    if cells:
        rows.append(cells)

df = pd.DataFrame(rows)
df.to_csv("population.csv", index=False, encoding="utf-8-sig")
print(f"{len(df)}행 저장 완료")

# ─── pandas read_html() 단축 버전 ────────────
# pandas는 URL의 모든 table 태그를 자동으로 파싱해 DataFrame 리스트로 반환
tables = pd.read_html(url, attrs={"class": "wikitable"})
print(tables[0].head())   # 첫 번째 테이블 상위 5행

📰 실전 ② 뉴스 헤드라인 수집 — 여러 페이지 순회

 
scrape_news.py — 페이지네이션 크롤링 패턴
import requests, time, csv
from bs4 import BeautifulSoup

# ─── 크롤링 공통 설정 ────────────────────────
BASE_URL  = "https://news.ycombinator.com"  # Hacker News (공개 허용)
HEADERS   = {"User-Agent": "Mozilla/5.0"}
MAX_PAGES = 3   # 수집할 페이지 수 (서버 부하 최소화)

all_news = []

for page in range(1, MAX_PAGES + 1):
    url = f"{BASE_URL}/?p={page}"
    try:
        res = requests.get(url, headers=HEADERS, timeout=10)
        res.raise_for_status()
        soup = BeautifulSoup(res.text, "lxml")

        for item in soup.select("tr.athing"):
            title_tag = item.select_one("span.titleline > a")
            if title_tag:
                all_news.append({
                    "title": title_tag.text.strip(),
                    "url"  : title_tag.get("href", ""),
                    "page" : page,
                })

        print(f"  {page}페이지 수집 완료 ({len(all_news)}개)")
        time.sleep(1)   # 서버 부하 방지 — 반드시 넣을 것!

    except requests.exceptions.RequestException as e:
        print(f"  {page}페이지 에러: {e}")
        continue

# ─── CSV 저장 ─────────────────────────────────
with open("hn_news.csv", "w", newline="", encoding="utf-8-sig") as f:
    writer = csv.DictWriter(f, fieldnames=["title", "url", "page"])
    writer.writeheader()
    writer.writerows(all_news)

print(f"\n총 {len(all_news)}개 뉴스 저장 완료 → hn_news.csv")

📷 실전 ③ 이미지 파일 자동 다운로드

 
download_images.py — 페이지 내 이미지 자동 저장
import requests, time
from bs4 import BeautifulSoup
from pathlib import Path
from urllib.parse import urljoin, urlparse

BASE_URL = "https://example.com"    # 실습 시 실제 URL로 변경
SAVE_DIR = Path("images")
SAVE_DIR.mkdir(exist_ok=True)

res  = requests.get(BASE_URL, headers={"User-Agent": "Mozilla/5.0"})
soup = BeautifulSoup(res.text, "lxml")

for i, img in enumerate(soup.find_all("img", src=True)):
    src = urljoin(BASE_URL, img["src"])   # 상대 URL → 절대 URL 변환
    ext = Path(urlparse(src).path).suffix or ".jpg"
    filename = SAVE_DIR / f"img_{i:03d}{ext}"

    try:
        img_res = requests.get(src, timeout=5)
        filename.write_bytes(img_res.content)
        print(f"저장: {filename}")
        time.sleep(0.5)
    except Exception as e:
        print(f"실패: {src} — {e}")

크롤링 전에 반드시 확인해야 할 것들

📜
크롤링 예절과 법적 고려사항 robots.txt 먼저 확인하기
  • robots.txt 확인: https://사이트주소/robots.txt에서 크롤링 허용 여부 확인. Disallow: /가 있으면 해당 경로 크롤링 금지
  • 서버 부하 최소화: 요청 사이에 반드시 time.sleep(1~3) 삽입. 짧은 시간에 수백 번 요청하면 서버에 부담을 주고 IP 차단될 수 있음
  • 개인정보 주의: 개인 식별 정보(이름·연락처·이메일 등)를 수집하면 개인정보보호법 위반 소지
  • 저작권 주의: 수집한 데이터를 상업적으로 재배포하면 저작권 문제 발생 가능. 개인 학습·비공개 용도로만 사용 권장
  • 이용약관 확인: 사이트 이용약관에 크롤링·자동화 수집 금지 조항이 있을 수 있음
파이썬 robotparser 표준 모듈로 robots.txt를 자동으로 파싱하고 허용 여부를 확인할 수 있습니다.
💡
동적 페이지 대응 JavaScript로 렌더링되는 페이지는 requests만으론 안 된다

requests + BeautifulSoup은 서버가 직접 HTML을 돌려주는 정적 페이지에서 잘 작동합니다. 하지만 React·Vue로 만든 SPA처럼 JavaScript가 실행된 후 데이터가 채워지는 동적 페이지는 HTML에 데이터가 없어 스크래핑이 어렵습니다.

  • Selenium: 실제 브라우저를 코드로 제어. 동적 페이지 완전 지원
  • Playwright: Selenium보다 빠르고 현대적인 브라우저 자동화 도구
  • API 확인 먼저: 개발자 도구 Network 탭에서 XHR/Fetch 요청을 확인하면 JSON API를 직접 호출할 수 있는 경우가 많음 (훨씬 효율적)

📝 13편 실습 문제

실습 1 — 기초 HTML 파싱
  • 아래 HTML 문자열을 BeautifulSoup으로 파싱해 모든 상품명·가격·링크를 추출
  • find_all()select() 두 가지 방법으로 각각 구현
  • 결과를 딕셔너리 리스트로 만들고 CSV로 저장
실습 2 — 실전 크롤링
  • Hacker News (news.ycombinator.com) 첫 페이지에서 뉴스 제목·URL·점수 수집
  • 요청 사이에 time.sleep(1) 추가
  • 결과를 pandas DataFrame으로 변환 후 엑셀로 저장
  • 점수 기준 내림차순 정렬 후 상위 10개만 출력
 
practice_13.py — 실습 1 예시 답안
import csv
from bs4 import BeautifulSoup

html = """
<ul class="product-list">
  <li class="product">
    <a href="/p/1">무선 마우스</a>
    <span class="price">29,900원</span>
  </li>
  <li class="product">
    <a href="/p/2">기계식 키보드</a>
    <span class="price">89,000원</span>
  </li>
  <li class="product">
    <a href="/p/3">모니터 암</a>
    <span class="price">45,000원</span>
  </li>
</ul>
"""

soup = BeautifulSoup(html, "lxml")
products = []

# 방법 1: find_all()
for li in soup.find_all("li", class_="product"):
    products.append({
        "name" : li.find("a").text.strip(),
        "price": li.find("span", class_="price").text.strip(),
        "url"  : li.find("a").get("href"),
    })

# 방법 2: select() — 동일 결과
products_v2 = [
    {
        "name" : li.select_one("a").text.strip(),
        "price": li.select_one(".price").text.strip(),
        "url"  : li.select_one("a").get("href"),
    }
    for li in soup.select(".product")
]

# CSV 저장
with open("products.csv", "w", newline="", encoding="utf-8-sig") as f:
    writer = csv.DictWriter(f, fieldnames=["name", "price", "url"])
    writer.writeheader()
    writer.writerows(products)

for p in products:
    print(p)
# {'name': '무선 마우스', 'price': '29,900원', 'url': '/p/1'}  ...
13편 핵심 요약
  • 웹 스크래핑 흐름: requests로 HTML 수신 → BeautifulSoup으로 파싱 → 데이터 추출 → CSV·Excel 저장
  • requests 핵심: get(url, headers=, timeout=) / raise_for_status() / Session으로 쿠키 유지
  • BeautifulSoup: find(tag, class_=) — 첫 번째 / find_all() — 전체 / .text / .get("속성")
  • CSS 선택자: select() / select_one() — 태그·.클래스·#id·부모>자식·[속성] 모두 지원
  • 테이블 추출: find("table") → find_all("tr") → find_all("td") / pandas read_html()로 단축 가능
  • 다중 페이지: 반복문 + URL 패턴 + time.sleep(1) 필수
  • 크롤링 예절: robots.txt 확인 / time.sleep 삽입 / 개인정보·저작권·이용약관 주의
  • 동적 페이지: requests+BS4는 정적 HTML만 / JS 렌더링 페이지는 Selenium·Playwright 또는 API 직접 호출
다음 편 예고 14편 — 파이썬 자동화 실전 프로젝트

파일 정리 자동화 / 이메일 자동 발송 / 스케줄러 / 실무 자동화 완성

🐍

※ 본 포스팅은 requests 2.x 및 BeautifulSoup4 공식 문서를 기반으로 작성된 학습용 콘텐츠입니다. 코드 예시는 Python 3.10 이상 환경에서 테스트되었습니다. 웹 스크래핑 시 각 사이트의 이용약관과 robots.txt를 반드시 확인하고 준수하시기 바랍니다.