파이썬(Python) 공부 13편
웹 스크래핑 기초 완전 정복
웹에 있는 데이터를 자동으로 긁어오는 것, 바로 웹 스크래핑입니다. 매일 뉴스 헤드라인을 엑셀에 저장하거나, 쇼핑몰 가격을 자동으로 수집하거나, 날씨 정보를 주기적으로 가져오는 작업 — 파이썬 코드 몇 십 줄이면 가능합니다. requests로 웹 페이지를 가져오고, BeautifulSoup으로 원하는 데이터만 뽑아내는 방법을 실전 예제와 함께 정리합니다.
🌎 웹 스크래핑이란 — 어떤 원리로 동작할까
우리가 브라우저에서 웹 페이지를 열면, 브라우저가 서버에 HTTP 요청을 보내고 서버가 HTML 문서를 돌려줍니다. 브라우저는 이 HTML을 해석해 화면에 보여줍니다. 웹 스크래핑은 이 과정을 파이썬 코드가 대신 수행하는 것입니다.
파이썬은 두 가지 역할을 나눠 처리합니다. requests가 HTTP 요청을 보내 HTML 텍스트를 가져오고, BeautifulSoup이 그 HTML을 파싱 해서 원하는 데이터를 추출합니다.
- HTTP GET / POST 요청 전송
- 응답 HTML 텍스트 수신
- 헤더·쿠키·세션 처리
- 타임아웃·에러 핸들링
- HTML·XML 파싱
- 태그·클래스·id로 검색
- CSS 선택자 지원
- 텍스트·속성 값 추출
pip install requests beautifulsoup4 lxml
lxml은 HTML 파서로, html.parser(기본 내장)보다 속도가 빠르고 불완전한 HTML도 잘 처리합니다. 함께 설치해 두는 것이 좋습니다.
📱 requests 기초 — HTTP 요청 보내기
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)가 만들어집니다. 이 트리를 탐색해 원하는 태그와 텍스트를 꺼내는 것이 핵심입니다.
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)") |
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 저장
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행
📰 실전 ② 뉴스 헤드라인 수집 — 여러 페이지 순회
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")
📷 실전 ③ 이미지 파일 자동 다운로드
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 확인: https://사이트주소/robots.txt에서 크롤링 허용 여부 확인. Disallow: /가 있으면 해당 경로 크롤링 금지
- 서버 부하 최소화: 요청 사이에 반드시 time.sleep(1~3) 삽입. 짧은 시간에 수백 번 요청하면 서버에 부담을 주고 IP 차단될 수 있음
- 개인정보 주의: 개인 식별 정보(이름·연락처·이메일 등)를 수집하면 개인정보보호법 위반 소지
- 저작권 주의: 수집한 데이터를 상업적으로 재배포하면 저작권 문제 발생 가능. 개인 학습·비공개 용도로만 사용 권장
- 이용약관 확인: 사이트 이용약관에 크롤링·자동화 수집 금지 조항이 있을 수 있음
requests + BeautifulSoup은 서버가 직접 HTML을 돌려주는 정적 페이지에서 잘 작동합니다. 하지만 React·Vue로 만든 SPA처럼 JavaScript가 실행된 후 데이터가 채워지는 동적 페이지는 HTML에 데이터가 없어 스크래핑이 어렵습니다.
- Selenium: 실제 브라우저를 코드로 제어. 동적 페이지 완전 지원
- Playwright: Selenium보다 빠르고 현대적인 브라우저 자동화 도구
- API 확인 먼저: 개발자 도구 Network 탭에서 XHR/Fetch 요청을 확인하면 JSON API를 직접 호출할 수 있는 경우가 많음 (훨씬 효율적)
📝 13편 실습 문제
- 아래 HTML 문자열을 BeautifulSoup으로 파싱해 모든 상품명·가격·링크를 추출
- find_all()과 select() 두 가지 방법으로 각각 구현
- 결과를 딕셔너리 리스트로 만들고 CSV로 저장
- Hacker News (news.ycombinator.com) 첫 페이지에서 뉴스 제목·URL·점수 수집
- 요청 사이에 time.sleep(1) 추가
- 결과를 pandas DataFrame으로 변환 후 엑셀로 저장
- 점수 기준 내림차순 정렬 후 상위 10개만 출력
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'} ...
- 웹 스크래핑 흐름: 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 직접 호출
파일 정리 자동화 / 이메일 자동 발송 / 스케줄러 / 실무 자동화 완성