Python 웹 스크래핑 | BeautifulSoup, Selenium 완벽 정리
이 글의 핵심
response = requests.get( 웹 스크래핑은 웹사이트에서 자동으로 데이터를 수집하는 기술입니다. --- import requests'https://example.com') 개념과 예제 코드를 단계적으로 다루며, 실무·학습에 참고할 수 있도록 구성했습니다.
들어가며
웹 스크래핑은 웹사이트에서 자동으로 데이터를 수집하는 기술입니다. 브라우저가 주소를 치고 HTML을 받아 오는 과정을, Python 스크립트가 대신 반복해 주는 것으로 이해하시면 됩니다.
이 글에서 다루는 내용
- 웹 스크래핑의 역사와 동작 원리: HTTP 통신부터 HTML 파싱까지의 전체 흐름
- BeautifulSoup vs Selenium: 정적/동적 페이지의 차이와 선택 기준
- 실전 패턴: 재시도, 레이트 리밋, 세션 관리, 분산 스크래핑
- 법적·윤리적 고려사항: robots.txt, ToS, 개인정보 보호
0. 웹 스크래핑의 역사와 설계 철학
웹 스크래핑의 탄생 배경
웹 스크래핑의 역사는 웹 자체의 역사와 함께합니다. 1990년대 초 Tim Berners-Lee가 World Wide Web을 발명한 직후부터, 사람들은 웹에 흩어진 정보를 자동으로 모으고 싶어했습니다.
초기 웹은 정적 HTML 문서의 집합에 불과했습니다. 검색 엔진이 등장하기 전, 웹 크롤러(web crawler)는 링크를 따라가며 페이지를 수집하고 인덱싱하는 봇이었습니다. Google의 PageRank 알고리즘(1998)도 본질적으로는 대규모 웹 크롤링과 링크 분석의 산물입니다.
HTML 파싱의 진화
초기 스크래핑은 정규 표현식으로 HTML을 파싱했습니다. 하지만 HTML은 잘 정의된 XML이 아니라 “느슨한” 마크업이라는 문제가 있었습니다:
# ❌ 안티패턴: 정규식으로 HTML 파싱
import re
html = '<div class="title">Hello</div>'
match = re.search(r'<div class="title">(.*?)</div>', html)
# 중첩 태그, 속성 순서 변경, 공백 차이 등에 취약
이런 문제를 해결하기 위해 HTML 파서 라이브러리가 등장했습니다:
- BeautifulSoup(2004): Python의 대표적인 HTML 파서.
lxml,html.parser등 다양한 백엔드를 지원하며, 잘못된 HTML도 관대하게 처리하는 것이 특징입니다. - lxml(2005): C 라이브러리
libxml2의 Python 바인딩. 속도가 빠르고 XPath를 지원합니다. - html5lib(2006): 브라우저와 동일한 방식으로 HTML을 파싱하여 최대한 관대한 처리를 목표로 합니다.
JavaScript 시대와 Headless 브라우저
2010년대 들어 Single Page Application(SPA) 과 JavaScript 프레임워크(React, Vue, Angular)가 보편화되면서, 서버가 보내는 초기 HTML이 거의 빈 껍데기인 경우가 많아졌습니다. 실제 콘텐츠는 JavaScript가 실행된 후에야 DOM에 삽입됩니다.
<!-- 서버가 보내는 초기 HTML -->
<div id="root"></div>
<script src="app.js"></script>
<!-- JavaScript 실행 후 -->
<div id="root">
<h1>실제 콘텐츠</h1>
<p>데이터가 여기 렌더링됨</p>
</div>
이를 스크래핑하려면 브라우저처럼 JavaScript를 실행할 수 있는 환경이 필요합니다. 그래서 등장한 것이 Headless 브라우저입니다:
- PhantomJS(2011-2018, 지원 종료): WebKit 기반의 headless 브라우저
- Selenium(2004~): 원래는 웹 애플리케이션 테스트 도구였으나, 실제 브라우저를 제어할 수 있어 스크래핑에도 널리 사용
- Puppeteer(2017~): Google이 만든 Node.js용 Chrome/Chromium 제어 라이브러리
- Playwright(2020~): Microsoft가 만든 크로스 브라우저 자동화 도구
Python에서는 Selenium + Chrome/Firefox가 가장 일반적이고, 최근에는 Playwright의 Python 바인딩도 인기입니다.
HTTP 통신의 내부 구조
웹 스크래핑의 핵심은 HTTP 요청/응답입니다. requests.get(url)이 내부적으로 하는 일은 다음과 같습니다:
sequenceDiagram
participant Client as Python Script
participant DNS
participant Server as 웹 서버
Client->>DNS: example.com의 IP는?
DNS-->>Client: 93.184.216.34
Client->>Server: TCP 3-Way Handshake
Server-->>Client: SYN-ACK
Client->>Server: GET /page HTTP/1.1
Host: example.com
User-Agent: ...
Server-->>Client: HTTP/1.1 200 OK
Content-Type: text/html
...
Client->>Client: HTML 파싱, 데이터 추출
중요한 개념:
- DNS 조회: 도메인 이름을 IP 주소로 변환. 캐싱으로 반복 조회를 줄일 수 있음.
- TCP 연결: 3-Way Handshake로 연결 수립. Keep-Alive로 연결 재사용 가능.
- HTTP 요청: Method(GET/POST), Headers(User-Agent, Cookie, …), Body
- HTTP 응답: Status Code(200, 404, 503, …), Headers(Content-Type, Set-Cookie, …), Body(HTML, JSON, …)
BeautifulSoup의 내부 동작
BeautifulSoup은 HTML을 트리 구조로 파싱합니다. 내부적으로는 다음 과정을 거칩니다:
from bs4 import BeautifulSoup
html = """
<html>
<body>
<div class="container">
<h1 id="title">제목</h1>
<p>내용</p>
</div>
</body>
</html>
"""
soup = BeautifulSoup(html, 'html.parser')
# 1. 토큰화(Tokenization): <html>, <body>, <div>, ... 로 분해
# 2. 파싱(Parsing): 트리 구조로 구성
# 3. 탐색(Navigation): find, select 등으로 노드 접근
파서 백엔드 비교:
| 파서 | 속도 | 관대함 | 외부 의존성 |
|---|---|---|---|
html.parser | 중간 | 중간 | 없음 (표준 라이브러리) |
lxml | 빠름 | 중간 | C 라이브러리 필요 |
html5lib | 느림 | 매우 관대 | 순수 Python |
실무 권장: 속도가 중요하면 lxml, 호환성이 중요하면 html.parser, 브라우저와 완전히 동일한 파싱이 필요하면 html5lib.
Selenium의 작동 원리
Selenium은 WebDriver 프로토콜을 통해 브라우저를 원격 제어합니다:
from selenium import webdriver
# 1. WebDriver 시작 (Chrome, Firefox, ...)
driver = webdriver.Chrome() # chromedriver 실행
# 2. WebDriver <-> Chrome 간 JSON-RPC 통신
driver.get('https://example.com')
# → POST /session/:sessionId/url {"url": "..."}
# 3. JavaScript 실행 대기
driver.implicitly_wait(10)
# 4. DOM 요소 찾기
element = driver.find_element(By.CSS_SELECTOR, '.title')
# → POST /session/:sessionId/element {"using": "css selector", "value": ".title"}
# 5. 요소 조작
element.click()
# → POST /session/:sessionId/element/:elementId/click
왜 느릴까?
- 실제 브라우저를 띄우기 때문: 메모리 수백 MB, CPU 사용
- 렌더링 과정 포함: CSS 계산, 레이아웃, 페인팅
- 네트워크 통신 오버헤드: Python ↔ WebDriver ↔ Browser
최적화 방법:
- Headless 모드: GUI 없이 실행 (
options.add_argument('--headless')) - 이미지/CSS 비활성화: 필요 없는 리소스 로딩 차단
- 브라우저 인스턴스 재사용: 매번 새로 띄우지 않기
1. requests 기본
HTML 가져오기
requests.get은 서버에 “이 페이지 주세요”라고 부탁하고, 응답 본문·상태 코드·헤더를 돌려받는 우편 요청과 같습니다. 일부 사이트는 기본 User-Agent만 보면 봇으로 막기 때문에, 실제 브라우저에 가까운 헤더를 넣으면 차단이 줄어드는 경우가 많습니다.
import requests
# GET 요청
response = requests.get('https://example.com')
print(response.status_code) # 200
print(response.text) # HTML 내용
print(response.headers) # 헤더 정보
# User-Agent 설정
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
response = requests.get('https://example.com', headers=headers)
2. BeautifulSoup
HTML 파싱
서버가 준 HTML 문자열은 한 덩어리 텍스트라서, 그대로는 제목·링크만 골라내기 어렵습니다. BeautifulSoup은 이를 태그 단위로 나눈 뒤 find/select로 원하는 조각만 집는 도구입니다. 마치 책에서 목차와 본문만 골라 읽는 것과 비슷합니다.
from bs4 import BeautifulSoup
import requests
# HTML 가져오기
url = 'https://example.com'
response = requests.get(url)
soup = BeautifulSoup(response.text, 'html.parser')
# 태그 찾기
title = soup.find('title')
print(title.text)
# 여러 태그 찾기
links = soup.find_all('a')
for link in links:
print(link.get('href'))
# CSS 선택자
articles = soup.select('.article-title')
for article in articles:
print(article.text)
실전 예제: 뉴스 크롤링
import requests
from bs4 import BeautifulSoup
import pandas as pd
def scrape_news(url):
"""뉴스 제목과 링크 수집"""
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'
}
response = requests.get(url, headers=headers)
soup = BeautifulSoup(response.text, 'html.parser')
articles = []
for item in soup.select('.news-item'):
title = item.select_one('.title').text.strip()
link = item.select_one('a')['href']
date = item.select_one('.date').text.strip()
articles.append({
'title': title,
'link': link,
'date': date
})
return pd.DataFrame(articles)
# 사용
df = scrape_news('https://news.example.com')
df.to_csv('news.csv', index=False, encoding='utf-8-sig')
3. Selenium (동적 페이지)
설치
pip install selenium
기본 사용
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
# 드라이버 설정
driver = webdriver.Chrome()
try:
# 페이지 열기
driver.get('https://example.com')
# 요소 대기
element = WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.CLASS_NAME, 'content'))
)
# 요소 찾기
title = driver.find_element(By.TAG_NAME, 'h1')
print(title.text)
# 클릭
button = driver.find_element(By.ID, 'load-more')
button.click()
# 스크롤
driver.execute_script('window.scrollTo(0, document.body.scrollHeight);')
finally:
driver.quit()
4. 실전 예제
상품 가격 모니터링
import requests
from bs4 import BeautifulSoup
import time
from datetime import datetime
def check_price(url, target_price):
"""상품 가격 확인"""
headers = {
'User-Agent': 'Mozilla/5.0'
}
response = requests.get(url, headers=headers)
soup = BeautifulSoup(response.text, 'html.parser')
# 가격 추출 (사이트마다 다름)
price_text = soup.select_one('.price').text
price = int(price_text.replace(',', ').replace('원', '))
print(f"[{datetime.now()}] 현재 가격: {price:,}원")
if price <= target_price:
print(f"🎉 목표 가격 달성! ({target_price:,}원 이하)")
return True
return False
# 사용 (1시간마다 확인)
url = 'https://shopping.example.com/product/123'
target = 50000
while True:
if check_price(url, target):
break
time.sleep(3600) # 1시간 대기
5. 데이터 저장
CSV 저장
import pandas as pd
def scrape_and_save(url, output_file):
"""스크래핑 후 CSV 저장"""
# 스크래핑
data = scrape_data(url)
# DataFrame 생성
df = pd.DataFrame(data)
# CSV 저장
df.to_csv(output_file, index=False, encoding='utf-8-sig')
print(f"저장 완료: {output_file}")
요청 간격·robots.txt·예외 처리 (에티켓)
같은 서버에 짧은 시간에 수백 번 요청을 보내면 부하와 차단으로 이어질 수 있습니다. robots.txt로 허용 범위를 확인하고, time.sleep으로 간격을 두며, 네트워크 오류는 try/except로 한 번 실패했다고 전체가 죽지 않게 받아 주는 편이 좋습니다.
# ✅ robots.txt 확인
# https://example.com/robots.txt
# ✅ 요청 간격 두기
import time
time.sleep(1) # 1초 대기
# ✅ User-Agent 설정
headers = {'User-Agent': '...'}
# ✅ 에러 처리
try:
response = requests.get(url, timeout=10)
response.raise_for_status()
except requests.exceptions.RequestException as e:
print(f"요청 실패: {e}")
내부 동작과 핵심 메커니즘
이 글의 주제는 「Python 웹 스크래핑 | BeautifulSoup, Selenium 완벽 정리」입니다. 여기서는 앞선 설명을 구현·런타임 관점에서 한 번 더 압축합니다. 데이터 흐름과 실패 모드를 기준으로 생각하면, “입력이 어디서 검증되고, 핵심 연산이 어디서 일어나며, 부작용(I/O·네트워크·디스크)이 어디서 터지는가”가 한눈에 드러납니다.
처리 파이프라인(개념도)
flowchart TD A[입력·요청·이벤트] --> B[파싱·검증·디코딩] B --> C[핵심 연산·상태 전이] C --> D[부작용: I/O·네트워크·동시성] D --> E[결과·관측·저장]
알고리즘·프로토콜 관점에서의 체크포인트
- 불변 조건(Invariant): 각 단계가 만족해야 하는 조건(예: 버퍼 경계, 프로토콜 상태, 트랜잭션 격리)을 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
- 결정성: 동일 입력에 동일 출력이 보장되는 순수한 층과, 시간·네트워크에 의해 달라질 수 있는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
- 경계 비용: 직렬화/역직렬화, 문자 인코딩, syscall 횟수, 락 경합처럼 “한 번의 호출이 아니라 누적되는 비용”을 의심 목록에 넣습니다.
프로덕션 운영 패턴
실서비스에서는 기능 구현과 함께 관측·배포·보안·비용이 동시에 요구됩니다. 아래는 팀에서 자주 쓰는 최소 체크리스트입니다.
| 영역 | 운영 관점에서의 질문 |
|---|---|
| 관측성 | 요청 단위 상관 ID, 에러율/지연 분위수, 주요 의존성 타임아웃이 보이는가 |
| 안전성 | 입력 검증·권한·비밀 관리가 코드 경로마다 일관적인가 |
| 신뢰성 | 재시도는 멱등한 연산에만 적용되는가, 서킷 브레이커·백오프가 있는가 |
| 성능 | 캐시 계층·배치 크기·풀링·백프레셔가 데이터 규모에 맞는가 |
| 배포 | 롤백 룬북, 카나리, 마이그레이션 호환성이 문서화되어 있는가 |
운영 환경에서는 “개발자 PC에서는 재현되지 않던 문제”가 시간·부하·데이터 크기 때문에 드러납니다. 따라서 스테이징의 데이터 양·네트워크 지연을 가능한 한 현실에 가깝게 맞추는 것이 중요합니다.
문제 해결(Troubleshooting)
| 증상 | 가능 원인 | 조치 |
|---|---|---|
| 간헐적 실패 | 레이스 컨디션, 타임아웃, 외부 의존성 불안정 | 최소 재현 스크립트 작성, 분산 트레이스·로그 상관관계 확인 |
| 성능 저하 | N+1 쿼리, 동기 I/O, 잠금 경합, 과도한 직렬화 | 프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거 |
| 메모리 증가 | 캐시 무제한, 클로저/이벤트 구독 누수, 대용량 객체의 불필요한 복사 | 상한·TTL·스냅샷 비교(힙 덤프/트레이스) |
| 빌드·배포만 실패 | 환경 변수·권한·플랫폼 차이 | CI 로그와 로컬 diff, 컨테이너/런타임 버전 핀(pin) |
권장 디버깅 순서: (1) 최소 재현 만들기 (2) 최근 변경 범위 좁히기 (3) 의존성·환경 변수 차이 확인 (4) 관측 데이터로 가설 검증 (5) 수정 후 회귀·부하 테스트.
정리
핵심 요약
- requests: HTTP 요청
- BeautifulSoup: HTML 파싱
- Selenium: 동적 페이지
- 에티켓: robots.txt, 요청 간격
- 저장: CSV, JSON, 데이터베이스
다음 단계
관련 글
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. Python 웹 스크래핑에 대해 정리한 개발 블로그 글입니다. response = requests.get( 웹 스크래핑은 웹사이트에서 자동으로 데이터를 수집하는 기술입니다. --- import requests. St… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. Python 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- Python REST API | Flask/Django로 API 서버 만들기
- Python 예외 처리 | try-except, raise, 커스텀 예외 완벽 정리
- Actix Web 완벽 가이드 — Rust 액터 기반 고성능 웹 프레임워크
이 글에서 다루는 키워드 (관련 검색어)
Python, 웹스크래핑, 크롤링, BeautifulSoup, Selenium, requests 등으로 검색하시면 이 글이 도움이 됩니다.