soup.find_all('h1') → 모든 제목 한 번에! soup.select('.price') → 가격만 쏙! 😊
뉴스 제목 수집, 상품 가격 비교, 부동산 정보 모으기… 웹페이지에서 원하는 정보만 자동으로 추출합니다!
(40-50분 완독 ⭐⭐⭐)
🎯 오늘의 학습 목표
📚 사전 지식
🎯 학습 목표 1: BeautifulSoup 라이브러리 설치하고 사용하기
1.1 BeautifulSoup이란?
BeautifulSoup은 HTML과 XML 문서를 파싱(분석)하고 데이터를 추출하는 Python 라이브러리입니다.
핵심 기능:
- HTML/XML 문서 파싱 및 트리 구조 생성
- 태그, 클래스, ID로 요소 검색
- CSS 선택자 지원으로 강력한 요소 선택
- 트리 탐색 (부모, 자식, 형제 요소)
- 속성 값 추출 및 텍스트 내용 가져오기
1.2 설치하기
1
2
3
4
5
| # BeautifulSoup 설치
pip install beautifulsoup4
# 파서 라이브러리 (선택사항, 더 빠른 파싱)
pip install lxml
|
파서 비교:
| 파서 | 속도 | 유연성 | 설치 |
html.parser | 보통 | 보통 | 기본 내장 |
lxml | 빠름 | 높음 | 별도 설치 필요 |
html5lib | 느림 | 매우 높음 | 별도 설치 필요 |
권장: lxml (빠르고 안정적), 설치가 안 되면 html.parser 사용
1.3 기본 사용법
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| from bs4 import BeautifulSoup
import requests
# 1. 웹페이지 가져오기
url = 'https://example.com'
response = requests.get(url)
# 2. BeautifulSoup 객체 생성
soup = BeautifulSoup(response.text, 'html.parser')
# 또는 lxml 파서 사용
soup = BeautifulSoup(response.text, 'lxml')
# 3. 데이터 추출
title = soup.find('title')
print(title.string) # 페이지 제목
|
1.4 로컬 HTML 파일 파싱
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| from bs4 import BeautifulSoup
# 파일에서 읽기
with open('index.html', 'r', encoding='utf-8') as f:
html_content = f.read()
soup = BeautifulSoup(html_content, 'html.parser')
# 또는 문자열 직접 파싱
html = """
<html>
<head><title>테스트 페이지</title></head>
<body>
<h1>메인 제목</h1>
<p>본문 내용</p>
</body>
</html>
"""
soup = BeautifulSoup(html, 'html.parser')
|
🎯 학습 목표 2: HTML 파싱과 요소 선택하기
2.1 기본 요소 접근
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| from bs4 import BeautifulSoup
html = """
<html>
<head><title>테스트 페이지</title></head>
<body>
<h1 class="main-title">메인 제목</h1>
<p id="intro">소개 문단입니다.</p>
<ul>
<li>항목 1</li>
<li>항목 2</li>
<li>항목 3</li>
</ul>
</body>
</html>
"""
soup = BeautifulSoup(html, 'html.parser')
# 제목 가져오기
print(soup.title) # <title>테스트 페이지</title>
print(soup.title.string) # 테스트 페이지
print(soup.title.name) # title
# h1 태그 (첫 번째만)
print(soup.h1) # <h1 class="main-title">메인 제목</h1>
print(soup.h1.string) # 메인 제목
# body 태그
print(soup.body)
|
출력:
1
2
3
4
5
6
7
8
9
| <title>테스트 페이지</title>
테스트 페이지
title
<h1 class="main-title">메인 제목</h1>
메인 제목
<body>
<h1 class="main-title">메인 제목</h1>
...
</body>
|
2.2 find() - 첫 번째 요소 찾기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| # 태그 이름으로 찾기
h1 = soup.find('h1')
print(h1.string) # 메인 제목
# 클래스로 찾기
title = soup.find(class_='main-title')
print(title.string) # 메인 제목
# ID로 찾기
intro = soup.find(id='intro')
print(intro.string) # 소개 문단입니다.
# 여러 조건 동시에
p_with_id = soup.find('p', id='intro')
print(p_with_id.string)
|
2.3 find_all() - 모든 요소 찾기
1
2
3
4
5
6
| # 모든 li 태그
lis = soup.find_all('li')
print(f"li 태그 개수: {len(lis)}")
for i, li in enumerate(lis, 1):
print(f"{i}. {li.string}")
|
출력:
1
2
3
4
| li 태그 개수: 3
1. 항목 1
2. 항목 2
3. 항목 3
|
2.4 고급 검색 옵션
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| html = """
<div>
<p class="text">첫 번째</p>
<p class="text highlight">두 번째</p>
<p class="text">세 번째</p>
<span class="text">네 번째</span>
</div>
"""
soup = BeautifulSoup(html, 'html.parser')
# 클래스가 정확히 일치하는 요소
exact = soup.find_all('p', class_='text')
print(f"정확히 일치: {len(exact)}개") # 2개 (highlight 포함 안 됨)
# 클래스를 포함하는 요소
contains = soup.find_all(class_='text')
print(f"포함하는 요소: {len(contains)}개") # 4개 (모두)
# 여러 클래스 (리스트로)
multi = soup.find_all(class_=['text', 'highlight'])
for elem in multi:
print(elem)
# 태그 여러 개 동시 검색
mixed = soup.find_all(['p', 'span'])
print(f"p 또는 span: {len(mixed)}개")
|
2.5 속성으로 검색
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| html = """
<a href="https://example.com" class="external">외부 링크</a>
<a href="/about" class="internal">내부 링크</a>
<img src="/photo.jpg" alt="사진" width="300">
"""
soup = BeautifulSoup(html, 'html.parser')
# 속성 존재 여부로 검색
links = soup.find_all('a', href=True)
print(f"링크 개수: {len(links)}")
# 속성 값으로 검색
external = soup.find('a', href='https://example.com')
print(external.string) # 외부 링크
# 속성 값 가져오기
img = soup.find('img')
print(img['src']) # /photo.jpg
print(img.get('alt')) # 사진
print(img.get('width')) # 300
|
🎯 학습 목표 3: CSS 셀렉터로 요소 찾기
3.1 select() 메서드
CSS 셀렉터는 웹 개발에서 사용하는 강력한 선택 문법입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
| html = """
<html>
<body>
<h1 id="main-title">메인 제목</h1>
<div class="container">
<p class="text">첫 번째 문단</p>
<p class="text highlight">두 번째 문단</p>
<ul>
<li>항목 1</li>
<li>항목 2</li>
</ul>
</div>
<footer>
<p>푸터 텍스트</p>
</footer>
</body>
</html>
"""
soup = BeautifulSoup(html, 'html.parser')
# 1. 태그 선택자
h1s = soup.select('h1')
print(f"h1 개수: {len(h1s)}")
# 2. 클래스 선택자 (.)
texts = soup.select('.text')
print(f".text 개수: {len(texts)}")
# 3. ID 선택자 (#)
title = soup.select('#main-title')
print(title[0].string)
# 4. 자손 선택자 (공백)
container_ps = soup.select('.container p')
print(f"container 내부 p: {len(container_ps)}")
# 5. 자식 선택자 (>)
direct_children = soup.select('div > p')
print(f"div의 직접 자식 p: {len(direct_children)}")
# 6. 여러 클래스 동시에
highlight = soup.select('.text.highlight')
print(highlight[0].string)
|
출력:
1
2
3
4
5
6
| h1 개수: 1
.text 개수: 2
메인 제목
container 내부 p: 2
div의 직접 자식 p: 2
두 번째 문단
|
3.2 select_one() - 첫 번째만
1
2
3
4
5
6
7
8
9
10
| # 첫 번째 매칭만 반환 (리스트 아님!)
first_p = soup.select_one('p')
print(first_p.string) # 첫 번째 문단
first_text = soup.select_one('.text')
print(first_text.string)
# 요소가 없으면 None 반환
none = soup.select_one('.non-existent')
print(none) # None
|
3.3 고급 CSS 셀렉터
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| html = """
<div class="products">
<article class="product" data-category="electronics">
<h3>노트북</h3>
<span class="price">1000000</span>
</article>
<article class="product" data-category="books">
<h3>파이썬 책</h3>
<span class="price">30000</span>
</article>
<article class="product" data-category="electronics">
<h3>마우스</h3>
<span class="price">25000</span>
</article>
</div>
"""
soup = BeautifulSoup(html, 'html.parser')
# 속성 선택자 []
electronics = soup.select('[data-category="electronics"]')
print(f"전자제품: {len(electronics)}개")
# 모든 price 추출
prices = soup.select('.product .price')
for price in prices:
print(f"가격: {price.string}원")
# nth-of-type (n번째 요소)
second_product = soup.select('.product:nth-of-type(2)')
print(second_product[0].h3.string) # 파이썬 책
|
3.4 실전 예제: 뉴스 기사 파싱
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| html = """
<div class="article">
<h2 class="title">뉴스 제목 1</h2>
<p class="summary">요약 내용 1</p>
<span class="author">기자 A</span>
<time class="date">2025-04-22</time>
</div>
<div class="article">
<h2 class="title">뉴스 제목 2</h2>
<p class="summary">요약 내용 2</p>
<span class="author">기자 B</span>
<time class="date">2025-04-21</time>
</div>
"""
soup = BeautifulSoup(html, 'html.parser')
# 모든 기사 추출
articles = soup.select('.article')
print(f"총 {len(articles)}개 기사\n")
for i, article in enumerate(articles, 1):
title = article.select_one('.title').string
summary = article.select_one('.summary').string
author = article.select_one('.author').string
date = article.select_one('.date').string
print(f"[기사 {i}]")
print(f"제목: {title}")
print(f"요약: {summary}")
print(f"저자: {author}")
print(f"날짜: {date}")
print("-" * 50)
|
🎯 학습 목표 4: 실전 웹 스크래핑 예제 만들기
4.1 뉴스 헤드라인 스크래핑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
| import requests
from bs4 import BeautifulSoup
def scrape_news_headlines(url):
"""뉴스 사이트에서 헤드라인 추출"""
try:
# User-Agent 설정 (중요!)
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status()
soup = BeautifulSoup(response.text, 'html.parser')
# 예: 제목이 <h2 class="headline">에 있다고 가정
headlines = soup.select('.headline')
articles = []
for headline in headlines:
title = headline.get_text(strip=True)
link = headline.find('a')
if link:
articles.append({
'title': title,
'url': link.get('href', '')
})
return articles
except requests.exceptions.RequestException as e:
print(f"요청 오류: {e}")
return []
except Exception as e:
print(f"파싱 오류: {e}")
return []
# 사용 예제 (실제 URL로 교체 필요)
# headlines = scrape_news_headlines('https://news.example.com')
# for i, article in enumerate(headlines, 1):
# print(f"{i}. {article['title']}")
# print(f" 링크: {article['url']}\n")
|
4.2 HTML 테이블 데이터 추출
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
| def scrape_table(url):
"""HTML 테이블을 딕셔너리 리스트로 변환"""
response = requests.get(url)
soup = BeautifulSoup(response.text, 'html.parser')
# 첫 번째 테이블 찾기
table = soup.find('table')
if not table:
print("테이블을 찾을 수 없습니다.")
return []
data = []
# 헤더 추출
headers = []
header_row = table.find('thead')
if header_row:
ths = header_row.find_all('th')
headers = [th.get_text(strip=True) for th in ths]
# 데이터 행 추출
tbody = table.find('tbody')
rows = tbody.find_all('tr') if tbody else table.find_all('tr')
for row in rows:
cells = row.find_all(['td', 'th'])
row_data = [cell.get_text(strip=True) for cell in cells]
if row_data:
if headers:
# 헤더가 있으면 딕셔너리로
data.append(dict(zip(headers, row_data)))
else:
# 없으면 리스트로
data.append(row_data)
return data
# 사용 예제
# table_data = scrape_table('https://example.com/table')
# for row in table_data:
# print(row)
|
실전 예제:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| # 간단한 테이블 HTML
html = """
<table>
<thead>
<tr>
<th>이름</th>
<th>나이</th>
<th>직업</th>
</tr>
</thead>
<tbody>
<tr>
<td>홍길동</td>
<td>30</td>
<td>개발자</td>
</tr>
<tr>
<td>김영희</td>
<td>25</td>
<td>디자이너</td>
</tr>
</tbody>
</table>
"""
soup = BeautifulSoup(html, 'html.parser')
# 헤더
headers = [th.string for th in soup.select('thead th')]
print("헤더:", headers)
# 데이터
for row in soup.select('tbody tr'):
cells = [td.string for td in row.find_all('td')]
print(dict(zip(headers, cells)))
|
출력:
1
2
3
| 헤더: ['이름', '나이', '직업']
{'이름': '홍길동', '나이': '30', '직업': '개발자'}
{'이름': '김영희', '나이': '25', '직업': '디자이너'}
|
4.3 이미지 다운로드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
| import os
from urllib.parse import urljoin
def download_images(url, save_dir='images'):
"""페이지의 모든 이미지 다운로드"""
# 저장 디렉터리 생성
os.makedirs(save_dir, exist_ok=True)
response = requests.get(url)
soup = BeautifulSoup(response.text, 'html.parser')
# 모든 img 태그
images = soup.find_all('img')
print(f"총 {len(images)}개 이미지 발견")
for i, img in enumerate(images, 1):
img_url = img.get('src')
if not img_url:
continue
# 상대 URL을 절대 URL로 변환
img_url = urljoin(url, img_url)
try:
# 이미지 다운로드
img_response = requests.get(img_url, timeout=10)
img_response.raise_for_status()
# 파일명 (확장자 유지)
ext = img_url.split('.')[-1].split('?')[0] # 쿼리 파라미터 제거
filename = f'image_{i:03d}.{ext}'
filepath = os.path.join(save_dir, filename)
# 바이너리로 저장
with open(filepath, 'wb') as f:
f.write(img_response.content)
print(f"✅ {filename} 다운로드 완료")
except Exception as e:
print(f"❌ {img_url} 다운로드 실패: {e}")
# 사용
# download_images('https://example.com')
|
4.4 트리 탐색 실전 예제
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| html = """
<div id="parent">
<h2>제목</h2>
<p>첫 번째 문단</p>
<p>두 번째 문단</p>
<ul>
<li>항목 1</li>
<li id="current">항목 2</li>
<li>항목 3</li>
</ul>
</div>
"""
soup = BeautifulSoup(html, 'html.parser')
# 부모 요소 찾기
current = soup.find(id='current')
print("부모:", current.parent.name) # ul
# 형제 요소
print("이전 형제:", current.find_previous_sibling().string) # 항목 1
print("다음 형제:", current.find_next_sibling().string) # 항목 3
# 자식 요소
div = soup.find(id='parent')
print("\n자식 요소들:")
for child in div.children:
if child.name: # 태그만 (공백 텍스트 제외)
print(f"- {child.name}: {child.get_text(strip=True)}")
|
💡 실전 팁 & 주의사항
✅ DO: 이렇게 하세요
- 항상 User-Agent 설정하기
1
2
| headers = {'User-Agent': 'Mozilla/5.0 ...'}
requests.get(url, headers=headers)
|
- 요소 존재 확인 후 사용
1
2
3
4
5
| title = soup.find('h1')
if title:
print(title.string)
else:
print("제목 없음")
|
- 타임아웃 설정
1
| requests.get(url, timeout=10)
|
- 예외 처리 필수
1
2
3
4
5
| try:
response = requests.get(url)
soup = BeautifulSoup(response.text, 'html.parser')
except Exception as e:
print(f"오류: {e}")
|
- robots.txt 확인
- 스크래핑 전에 해당 사이트의
robots.txt 확인 - 예:
https://example.com/robots.txt
❌ DON’T: 이러지 마세요
- 과도한 요청 금지
1
2
3
4
5
6
7
8
9
| # 나쁜 예: 연속 요청
for i in range(1000):
requests.get(f'https://example.com/page/{i}')
# 좋은 예: 딜레이 추가
import time
for i in range(100):
requests.get(f'https://example.com/page/{i}')
time.sleep(1) # 1초 대기
|
- None 체크 없이 속성 접근
1
2
3
4
5
6
| # 위험: title이 None일 수 있음
print(soup.find('title').string)
# 안전
title = soup.find('title')
print(title.string if title else "제목 없음")
|
- 인코딩 문제 무시
1
2
3
| # 한글 깨짐 방지
response.encoding = 'utf-8'
soup = BeautifulSoup(response.text, 'html.parser')
|
🧪 연습 문제
문제 1: 블로그 포스트 스크래퍼
다음 HTML에서 모든 블로그 포스트의 제목, 날짜, 태그를 추출하세요.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| html = """
<div class="posts">
<article class="post">
<h2 class="title">Python 기초 강좌</h2>
<time>2025-04-20</time>
<div class="tags">
<span class="tag">python</span>
<span class="tag">tutorial</span>
</div>
</article>
<article class="post">
<h2 class="title">웹 스크래핑 입문</h2>
<time>2025-04-22</time>
<div class="tags">
<span class="tag">scraping</span>
<span class="tag">beautifulsoup</span>
</div>
</article>
</div>
"""
# TODO: 각 포스트의 정보를 딕셔너리로 추출하세요
|
💡 힌트
soup.select('.post')로 모든 포스트 찾기 - 각 포스트에서
select_one()으로 제목, 날짜 추출 select('.tag')로 모든 태그 추출
✅ 정답
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'html.parser')
posts = []
for post in soup.select('.post'):
title = post.select_one('.title').string
date = post.select_one('time').string
tags = [tag.string for tag in post.select('.tag')]
posts.append({
'title': title,
'date': date,
'tags': tags
})
# 출력
for i, post in enumerate(posts, 1):
print(f"[포스트 {i}]")
print(f"제목: {post['title']}")
print(f"날짜: {post['date']}")
print(f"태그: {', '.join(post['tags'])}\n")
|
출력:
1
2
3
4
5
6
7
8
9
| [포스트 1]
제목: Python 기초 강좌
날짜: 2025-04-20
태그: python, tutorial
[포스트 2]
제목: 웹 스크래핑 입문
날짜: 2025-04-22
태그: scraping, beautifulsoup
|
문제 2: 가격 비교 스크래퍼
상품 목록에서 최저가와 최고가를 찾으세요.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| html = """
<div class="products">
<div class="product">
<h3>노트북 A</h3>
<span class="price">1200000</span>
</div>
<div class="product">
<h3>노트북 B</h3>
<span class="price">950000</span>
</div>
<div class="product">
<h3>노트북 C</h3>
<span class="price">1500000</span>
</div>
</div>
"""
# TODO: 최저가/최고가 상품을 찾으세요
|
✅ 정답
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'html.parser')
products = []
for product in soup.select('.product'):
name = product.h3.string
price = int(product.select_one('.price').string)
products.append({'name': name, 'price': price})
# 최저가/최고가
min_product = min(products, key=lambda x: x['price'])
max_product = max(products, key=lambda x: x['price'])
print(f"최저가: {min_product['name']} - {min_product['price']:,}원")
print(f"최고가: {max_product['name']} - {max_product['price']:,}원")
|
출력:
1
2
| 최저가: 노트북 B - 950,000원
최고가: 노트북 C - 1,500,000원
|
📝 오늘 배운 내용 정리
| 기능 | 메서드 | 설명 |
| 파싱 | BeautifulSoup(html, 'html.parser') | HTML을 파싱 가능한 객체로 변환 |
| 단일 검색 | find('tag') | 첫 번째 매칭 요소 반환 |
| 전체 검색 | find_all('tag') | 모든 매칭 요소 리스트 반환 |
| CSS 선택 | select('.class') | CSS 선택자로 전체 검색 |
| CSS 단일 | select_one('#id') | CSS 선택자로 첫 번째만 |
| 텍스트 | element.string 또는 get_text() | 요소의 텍스트 내용 |
| 속성 | element['attr'] 또는 get('attr') | 속성 값 가져오기 |
핵심 코드 패턴:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| # 1. 기본 스크래핑 패턴
from bs4 import BeautifulSoup
import requests
response = requests.get(url, headers=headers, timeout=10)
soup = BeautifulSoup(response.text, 'html.parser')
# 2. 요소 검색
elements = soup.find_all('tag', class_='classname')
# 또는
elements = soup.select('.classname')
# 3. 안전한 데이터 추출
for elem in elements:
text = elem.get_text(strip=True)
attr = elem.get('href', '') # 기본값 제공
|
🔗 관련 자료
📚 이전 학습
📚 다음 학습
“늦었다고 생각할 때가 가장 빠른 때입니다. 오늘도 한 걸음 더 나아갔습니다!” 🚀
내일은 Day 54: 웹 스크래핑 고급에서 만나요!