포스트

[Python 100일 챌린지] Day 53 - BeautifulSoup 기초

[Python 100일 챌린지] Day 53 - BeautifulSoup 기초

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: 이렇게 하세요

  1. 항상 User-Agent 설정하기
    1
    2
    
    headers = {'User-Agent': 'Mozilla/5.0 ...'}
    requests.get(url, headers=headers)
    
  2. 요소 존재 확인 후 사용
    1
    2
    3
    4
    5
    
    title = soup.find('h1')
    if title:
        print(title.string)
    else:
        print("제목 없음")
    
  3. 타임아웃 설정
    1
    
    requests.get(url, timeout=10)
    
  4. 예외 처리 필수
    1
    2
    3
    4
    5
    
    try:
        response = requests.get(url)
        soup = BeautifulSoup(response.text, 'html.parser')
    except Exception as e:
        print(f"오류: {e}")
    
  5. robots.txt 확인
    • 스크래핑 전에 해당 사이트의 robots.txt 확인
    • 예: https://example.com/robots.txt

❌ DON’T: 이러지 마세요

  1. 과도한 요청 금지
    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초 대기
    
  2. None 체크 없이 속성 접근
    1
    2
    3
    4
    5
    6
    
    # 위험: title이 None일 수 있음
    print(soup.find('title').string)
    
    # 안전
    title = soup.find('title')
    print(title.string if title else "제목 없음")
    
  3. 인코딩 문제 무시
    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: 각 포스트의 정보를 딕셔너리로 추출하세요
💡 힌트
  1. soup.select('.post')로 모든 포스트 찾기
  2. 각 포스트에서 select_one()으로 제목, 날짜 추출
  3. 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: 웹 스크래핑 고급에서 만나요!

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.