Python 웹 스크래핑: Requests, BeautifulSoup, Selenium을 사용한 완전 가이드
Updated on
API로 사용할 수 없는 데이터가 필요합니다 -- 경쟁사 웹사이트의 제품 가격, 학술 포털의 연구 논문, 채용 플랫폼의 구인 목록, 또는 여러 소스의 뉴스 기사. 수동으로 데이터를 복사하는 것은 느리고, 오류가 발생하기 쉬우며, 대규모로는 불가능합니다. 웹 스크래핑은 이 과정을 자동화하지만, 잘못된 도구나 접근 방식을 선택하면 차단된 요청, 깨진 파서, 법적 문제가 발생합니다.
이 가이드는 완전한 Python 웹 스크래핑 스택을 다룹니다: 정적 페이지용 requests + BeautifulSoup, JavaScript 렌더링 콘텐츠용 Selenium, 대규모 크롤링용 Scrapy. 실제 과제를 처리하는 실용적인 기술을 배울 수 있습니다.
빠른 시작: requests + BeautifulSoup
정적 HTML 페이지 스크래핑에 가장 일반적인 조합:
import requests
from bs4 import BeautifulSoup
# 페이지 가져오기
url = 'https://books.toscrape.com/'
response = requests.get(url)
response.raise_for_status() # 잘못된 상태 코드에 대해 오류 발생
# HTML 파싱
soup = BeautifulSoup(response.text, 'html.parser')
# 책 제목 추출
books = soup.select('article.product_pod h3 a')
for book in books[:5]:
print(book['title'])설치
# 필요한 패키지 설치
# pip install requests beautifulsoup4 lxmlBeautifulSoup으로 HTML 파싱
요소 찾기
from bs4 import BeautifulSoup
html = """
<div class="products">
<div class="product" id="p1">
<h2 class="name">Widget A</h2>
<span class="price">$9.99</span>
<p class="desc">A useful widget</p>
</div>
<div class="product" id="p2">
<h2 class="name">Widget B</h2>
<span class="price">$14.99</span>
<p class="desc">A better widget</p>
</div>
</div>
"""
soup = BeautifulSoup(html, 'html.parser')
# 태그로 찾기
print(soup.find('h2').text) # "Widget A"
# 클래스로 찾기
prices = soup.find_all('span', class_='price')
for p in prices:
print(p.text) # "$9.99", "$14.99"
# ID로 찾기
product = soup.find(id='p2')
print(product.find('h2').text) # "Widget B"
# CSS 선택자
names = soup.select('.product .name')
for n in names:
print(n.text)자주 사용하는 선택자
| 메서드 | 예시 | 찾는 대상 |
|---|---|---|
find('tag') | soup.find('h2') | 첫 번째 h2 요소 |
find_all('tag') | soup.find_all('a') | 모든 앵커 요소 |
find(class_='x') | soup.find(class_='price') | 해당 클래스를 가진 첫 번째 요소 |
find(id='x') | soup.find(id='main') | 해당 ID를 가진 요소 |
select('css') | soup.select('div.product > h2') | CSS 선택자 일치 |
select_one('css') | soup.select_one('#header') | 첫 번째 CSS 일치 |
데이터 추출
from bs4 import BeautifulSoup
html = '<a href="/page/2" class="next" data-page="2">Next Page</a>'
soup = BeautifulSoup(html, 'html.parser')
link = soup.find('a')
# 텍스트 콘텐츠
print(link.text) # "Next Page"
print(link.get_text(strip=True)) # "Next Page" (공백 제거)
# 속성
print(link['href']) # "/page/2"
print(link.get('class')) # ['next']
print(link['data-page']) # "2"헤더와 세션 처리
많은 웹사이트가 적절한 헤더 없는 요청을 차단합니다:
import requests
from bs4 import BeautifulSoup
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Accept-Language': 'en-US,en;q=0.9',
}
# 세션을 사용하여 쿠키와 영구 헤더 관리
session = requests.Session()
session.headers.update(headers)
response = session.get('https://books.toscrape.com/')
soup = BeautifulSoup(response.text, 'html.parser')
print(f"{len(soup.select('article.product_pod'))}권의 책 발견")페이지네이션
다음 페이지 링크 따라가기
import requests
from bs4 import BeautifulSoup
base_url = 'https://books.toscrape.com/catalogue/'
all_books = []
url = 'https://books.toscrape.com/catalogue/page-1.html'
while url:
response = requests.get(url)
soup = BeautifulSoup(response.text, 'html.parser')
# 현재 페이지에서 책 추출
for book in soup.select('article.product_pod'):
title = book.select_one('h3 a')['title']
price = book.select_one('.price_color').text
all_books.append({'title': title, 'price': price})
# 다음 페이지 찾기
next_btn = soup.select_one('li.next a')
if next_btn:
url = base_url + next_btn['href']
else:
url = None
print(f"지금까지 {len(all_books)}권 스크래핑...")
print(f"총: {len(all_books)}권")Selenium으로 JavaScript 페이지 스크래핑
페이지가 JavaScript로 콘텐츠를 렌더링할 때, requests는 렌더링 전의 원시 HTML만 가져옵니다. 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
# 헤드리스 Chrome 설정
options = webdriver.ChromeOptions()
options.add_argument('--headless')
driver = webdriver.Chrome(options=options)
try:
driver.get('https://quotes.toscrape.com/js/')
# 콘텐츠 로딩 대기
WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.CLASS_NAME, 'quote'))
)
quotes = driver.find_elements(By.CLASS_NAME, 'quote')
for quote in quotes:
text = quote.find_element(By.CLASS_NAME, 'text').text
author = quote.find_element(By.CLASS_NAME, 'author').text
print(f'"{text}" -- {author}')
finally:
driver.quit()도구 비교
| 도구 | 최적 용도 | 속도 | JavaScript | 학습 곡선 |
|---|---|---|---|---|
requests + BeautifulSoup | 정적 HTML 페이지 | 빠름 | 미지원 | 쉬움 |
Selenium | JavaScript 렌더링 페이지 | 느림 | 지원 | 중간 |
Scrapy | 대규모 크롤링 | 빠름 | 플러그인 필요 | 가파름 |
Playwright | 모던 JS 앱 | 중간 | 지원 | 중간 |
httpx + selectolax | 고성능 파싱 | 매우 빠름 | 미지원 | 쉬움 |
Scrapy로 대규모 스크래핑
수천 페이지를 크롤링할 때, Scrapy는 내장된 동시성, 재시도 로직, 데이터 파이프라인을 제공합니다:
# spider.py
import scrapy
class BookSpider(scrapy.Spider):
name = 'books'
start_urls = ['https://books.toscrape.com/']
def parse(self, response):
for book in response.css('article.product_pod'):
yield {
'title': book.css('h3 a::attr(title)').get(),
'price': book.css('.price_color::text').get(),
}
# 페이지네이션 따라가기
next_page = response.css('li.next a::attr(href)').get()
if next_page:
yield response.follow(next_page, self.parse)
# 실행 명령: scrapy runspider spider.py -o books.json데이터 정리 및 저장
import requests
from bs4 import BeautifulSoup
import pandas as pd
# 데이터 스크래핑
url = 'https://books.toscrape.com/'
response = requests.get(url)
soup = BeautifulSoup(response.text, 'html.parser')
books = []
for item in soup.select('article.product_pod'):
title = item.select_one('h3 a')['title']
price_text = item.select_one('.price_color').text
price = float(price_text.replace('£', ''))
rating_class = item.select_one('p.star-rating')['class'][1]
books.append({
'title': title,
'price': price,
'rating': rating_class,
})
# DataFrame으로 변환
df = pd.DataFrame(books)
print(df.head())
# CSV로 저장
df.to_csv('books.csv', index=False)윤리적 스크래핑 실천
| 실천 | 설명 |
|---|---|
| robots.txt 확인 | 사이트의 크롤링 규칙 준수 |
| 지연 추가 | 요청 간 time.sleep() 사용 (1-3초) |
| 신원 표시 | 설명적인 User-Agent 문자열 설정 |
| 응답 캐시 | 이미 가져온 페이지를 다시 스크래핑하지 않기 |
| API 확인 | 많은 사이트에 더 빠르고 허용된 공개 API가 있음 |
| 이용약관 읽기 | 일부 사이트는 스크래핑을 명시적으로 금지 |
| 속도 제한 | 동시 요청으로 서버에 과부하를 주지 않기 |
import time
import requests
def polite_scrape(urls, delay=2):
"""지연을 두고 예의 바르게 스크래핑합니다."""
results = []
for url in urls:
response = requests.get(url)
results.append(response.text)
time.sleep(delay) # 예의 바르게
return results스크래핑한 데이터 분석
웹 스크래핑으로 데이터를 수집한 후, PyGWalker (opens in a new tab)를 사용하면 Jupyter에서 스크래핑한 데이터셋을 인터랙티브하게 탐색하고 시각화할 수 있습니다:
import pandas as pd
import pygwalker as pyg
df = pd.read_csv('scraped_data.csv')
walker = pyg.walk(df)Jupyter에서 AI 지원을 받으며 스크래핑 스크립트를 반복적으로 실행하려면, RunCell (opens in a new tab)이 AI 기반 환경을 제공하여 선택자 디버깅과 파싱 로직 테스트를 인터랙티브하게 수행할 수 있습니다.
자주 묻는 질문
Python 웹 스크래핑에 가장 좋은 라이브러리는?
정적 페이지에는 requests + BeautifulSoup이 가장 간단하고 인기 있는 선택입니다. JavaScript 렌더링 페이지에는 Selenium 또는 Playwright를 사용합니다. 수천 페이지의 대규모 크롤링에는 Scrapy가 내장된 동시성과 파이프라인 관리를 제공합니다.
JavaScript를 사용하는 웹사이트를 스크래핑하려면?
Selenium 또는 Playwright를 사용하여 JavaScript를 실행하는 헤드리스 브라우저를 제어합니다. 또는 브라우저의 Network 탭에서 JSON 데이터를 반환하는 API 엔드포인트를 확인하세요 -- API를 직접 스크래핑하는 것이 브라우저 자동화보다 빠르고 안정적입니다.
웹 스크래핑은 합법인가요?
웹 스크래핑의 합법성은 관할 구역, 웹사이트의 이용약관, 데이터 사용 방법에 따라 다릅니다. 공개적으로 이용 가능한 데이터의 스크래핑은 많은 관할 구역에서 일반적으로 합법이지만, 항상 사이트의 robots.txt와 이용약관을 확인하세요. 개인 데이터나 저작권이 있는 콘텐츠의 스크래핑은 피하세요.
스크래핑 중 차단되는 것을 방지하려면?
요청 간 지연(1-3초)을 사용하고, User-Agent 문자열을 순환하고, robots.txt를 준수하고, 쿠키에 세션을 사용하고, 너무 많은 동시 요청을 피하세요. 지속적으로 차단된다면 해당 사이트에 공개 API가 있는지 확인하세요.
웹 스크래핑에서 페이지네이션을 처리하려면?
HTML에서 "다음 페이지" 링크나 버튼을 찾고, URL을 추출하여 더 이상 페이지가 없을 때까지 루프에서 따라갑니다. 또는 페이지가 쿼리 매개변수(예: ?page=2)를 사용하는 경우 페이지 번호를 직접 반복합니다.
결론
Python의 웹 스크래핑 생태계는 모든 시나리오를 다룹니다: 빠른 정적 페이지 스크래핑에는 requests + BeautifulSoup, JavaScript 집약적 사이트에는 Selenium, 프로덕션 규모의 크롤링에는 Scrapy. 작동하는 가장 간단한 도구부터 시작하고, 필요한 경우에만 복잡성을 추가하며, 항상 윤리적으로 스크래핑하세요 -- robots.txt를 준수하고, 지연을 추가하고, 먼저 API를 확인하세요. 스크래핑한 데이터는 CSV나 DataFrame과 같은 구조화된 형식으로 저장하여 즉시 분석할 수 있도록 하세요.