웹 크롤링 기초 — BeautifulSoup
이 토픽을 마치면
requests로 웹 페이지를 가져오고, BeautifulSoup으로 원하는 데이터를 추출하는 기본 흐름을 구현할 수 있습니다.
크롤링이란
웹 크롤링(web crawling)은 프로그램이 웹 페이지를 방문해서 데이터를 자동으로 수집하는 것입니다. 스크래핑(scraping)이라고도 합니다.
브라우저가 하는 일을 코드로 하는 것입니다:
- 웹 서버에 요청(request)을 보냄
- HTML을 받아옴
- HTML에서 원하는 데이터를 추출
1단계 — HTML 가져오기 (requests)
import requests
url = "https://example.com"response = requests.get(url)
print(response.status_code) # 200 = 정상print(response.text[:200]) # HTML 텍스트의 처음 200자requests.get(url)이 브라우저처럼 서버에 요청을 보내고, 서버가 보낸 HTML을 response.text로 받습니다.
# 상태 코드 확인if response.status_code == 200: html = response.textelif response.status_code == 404: print("페이지를 찾을 수 없습니다")elif response.status_code == 403: print("접근이 거부되었습니다")2단계 — HTML 파싱 (BeautifulSoup)
from bs4 import BeautifulSoup
html = """<html><body> <h1 class="title">뉴스 제목</h1> <div class="content"> <p>첫 번째 문단입니다.</p> <p>두 번째 문단입니다.</p> </div> <ul id="tags"> <li>Python</li> <li>데이터</li> <li>크롤링</li> </ul></body></html>"""
soup = BeautifulSoup(html, "html.parser")BeautifulSoup은 HTML 문자열을 파싱해서 트리 구조의 객체로 만들어줍니다. 이 객체에서 태그, 클래스, id 등으로 원하는 부분을 찾을 수 있습니다.
3단계 — 데이터 추출
태그로 찾기
# 첫 번째 h1 태그title = soup.find("h1")print(title.text) # "뉴스 제목"print(title["class"]) # ["title"]
# 모든 p 태그paragraphs = soup.find_all("p")for p in paragraphs: print(p.text)# "첫 번째 문단입니다."# "두 번째 문단입니다."CSS 선택자로 찾기
# select — CSS 선택자 사용 (querySelectorAll과 동일)items = soup.select("ul#tags li")for item in items: print(item.text)# "Python"# "데이터"# "크롤링"
# 클래스로 선택content = soup.select_one("div.content")print(content.text.strip())find/find_all보다 select/select_one이 CSS에 익숙한 사람에게는 직관적입니다.
실전 예시 — 테이블 데이터 추출
table_html = """<table> <tr><th>이름</th><th>점수</th></tr> <tr><td>철수</td><td>85</td></tr> <tr><td>영희</td><td>92</td></tr> <tr><td>민수</td><td>78</td></tr></table>"""
soup = BeautifulSoup(table_html, "html.parser")rows = soup.select("tr")
data = []for row in rows[1:]: # 헤더 제외 cols = row.find_all("td") data.append({ "이름": cols[0].text, "점수": int(cols[1].text) })
print(data)# [{'이름': '철수', '점수': 85}, {'이름': '영희', '점수': 92}, {'이름': '민수', '점수': 78}]# pandas DataFrame으로 바로 변환import pandas as pddf = pd.DataFrame(data)print(df)주의할 점
요청 간격
import time
urls = ["https://example.com/page/1", "https://example.com/page/2"]for url in urls: response = requests.get(url) time.sleep(1) # 1초 대기 — 서버에 부담을 주지 않기 위해연속으로 빠르게 요청하면 서버에 부담을 주거나 IP가 차단될 수 있습니다.
User-Agent 설정
headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/91.0"}response = requests.get(url, headers=headers)일부 서버는 봇을 차단하기 위해 User-Agent를 확인합니다.
robots.txt
https://example.com/robots.txt이 파일은 크롤링이 허용된 경로와 금지된 경로를 명시합니다. 사이트의 크롤링 정책을 먼저 확인하는 것이 예의이자 법적 안전장치입니다.
크롤링의 한계
BeautifulSoup은 정적 HTML만 처리할 수 있습니다. JavaScript로 동적으로 로딩되는 콘텐츠(SPA, 무한 스크롤 등)는 HTML에 포함되지 않으므로 추출할 수 없습니다.
| 상황 | 도구 |
|---|---|
| 정적 HTML | requests + BeautifulSoup |
| JavaScript 동적 로딩 | Selenium, Playwright |
| API 제공 | requests로 API 직접 호출 (가장 효율적) |
API가 있다면 크롤링보다 API를 우선 사용합니다. 구조화된 데이터를 정확하게 받을 수 있고, 서버에도 부담이 적습니다.
핵심 흐름 요약
1. requests.get(url) → HTML 텍스트
2. BeautifulSoup(html) → 파싱된 트리
3. soup.select("CSS 선택자") → 원하는 요소
4. element.text / element["attr"] → 데이터 추출에러 처리
실제 크롤링에서는 다양한 에러가 발생합니다:
import requestsfrom bs4 import BeautifulSoup
def safe_fetch(url, retries=3): for attempt in range(retries): try: response = requests.get(url, timeout=10) response.raise_for_status() return response.text except requests.exceptions.Timeout: print(f"타임아웃 ({attempt + 1}/{retries})") except requests.exceptions.HTTPError as e: print(f"HTTP 에러: {e}") return None except requests.exceptions.ConnectionError: print(f"연결 실패 ({attempt + 1}/{retries})") return Nonetimeout을 설정하지 않으면 응답 없는 서버에서 프로그램이 무한 대기합니다. raise_for_status()는 4xx/5xx 응답에서 예외를 발생시킵니다.
여러 페이지 크롤링
import time
base_url = "https://example.com/articles?page="all_titles = []
for page in range(1, 11): html = safe_fetch(f"{base_url}{page}") if html is None: continue soup = BeautifulSoup(html, "html.parser") titles = soup.select("h2.article-title") all_titles.extend([t.text.strip() for t in titles]) print(f"페이지 {page}: {len(titles)}개 수집") time.sleep(1)
print(f"총 {len(all_titles)}개 수집 완료")크롤링 결과 저장
import jsonimport csv
# JSON으로 저장with open("articles.json", "w", encoding="utf-8") as f: json.dump(all_titles, f, ensure_ascii=False, indent=2)
# CSV로 저장with open("articles.csv", "w", encoding="utf-8", newline="") as f: writer = csv.writer(f) writer.writerow(["번호", "제목"]) for i, title in enumerate(all_titles, 1): writer.writerow([i, title])수집한 데이터는 JSON이나 CSV로 저장해서 pandas로 바로 분석할 수 있게 합니다.
링크 추출 — href 속성
# 페이지 내 모든 링크 추출links = soup.select("a[href]")for link in links: url = link["href"] text = link.text.strip() print(f"{text}: {url}")
# 특정 패턴의 링크만article_links = [ a["href"] for a in soup.select("a[href]") if "/article/" in a.get("href", "")]크롤링은 "데이터를 구하는 기술"의 첫 걸음입니다. 데이터 분석, 머신러닝, 모니터링 등 모든 곳에서 데이터 수집이 출발점이 됩니다.