BeautifulSoup 완벽 가이드 – 웹 크롤링 마스터

웹사이트에서 원하는 데이터만 정확히 추출하고 싶으신가요? HTML 구조를 파악하고 특정 요소를 찾는 방법이 어려우신가요? 이 글에서는 BeautifulSoup의 find, select 메서드부터 헤더 설정, 정규식 활용까지 실전 크롤링 기법을 완벽하게 알려드립니다.

BeautifulSoup이란?

BeautifulSoup(bs4)는 HTML과 XML을 파싱해서 원하는 데이터를 쉽게 추출하는 파이썬 라이브러리입니다. 웹 페이지의 복잡한 구조에서 제목, 링크, 이미지, 텍스트 등을 정확하게 가져올 수 있습니다.

기본 설치 및 설정

# 필수 라이브러리 설치
pip install beautifulsoup4
pip install requests
pip install lxml  # 빠른 파서 (선택사항)

헤더(Headers) 설정 – 필수!

헤더가 필요한 이유

많은 웹사이트는 봇 차단을 위해 User-Agent를 확인합니다. 헤더를 설정하지 않으면 403 Forbidden 오류나 다른 내용이 반환될 수 있습니다.

import requests
from bs4 import BeautifulSoup

# ❌ 나쁜 예 (헤더 없음)
html = requests.get('<https://example.com>').text
# 403 오류 또는 빈 페이지 가능성

# ✅ 좋은 예 (헤더 포함)
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
}

html = requests.get('<https://example.com>', headers=headers).text
soup = BeautifulSoup(html, 'html.parser')

다양한 헤더 설정

import requests
from bs4 import BeautifulSoup

# 기본 헤더
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
    'Accept-Language': 'ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7',
    'Accept-Encoding': 'gzip, deflate, br',
    'Referer': '<https://www.google.com/>',
    'Connection': 'keep-alive'
}

url = '<https://sports.news.naver.com/index.nhn>'
response = requests.get(url, headers=headers)
soup = BeautifulSoup(response.text, 'html.parser')

Referer: 어디서 왔는지 알려줌 (일부 사이트는 필수)

Accept-Language: 한국어 페이지 요청

find()와 find_all() 메서드

find() – 첫 번째 요소 찾기

import requests
from bs4 import BeautifulSoup

headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}

html = requests.get('<https://example.com>', headers=headers).text
soup = BeautifulSoup(html, 'html.parser')

# 첫 번째 div 태그 찾기
div = soup.find('div')
print(div.text)

# class 속성으로 찾기
text_area = soup.find('div', class_='text_area')
print(text_area.text)

# id 속성으로 찾기
content = soup.find('div', id='content')
print(content.text)

# 여러 속성 조합
article = soup.find('div', class_='article', id='main')

# attrs 딕셔너리 사용
link = soup.find('a', attrs={'href': '/news', 'target': '_blank'})

# 없으면 None 반환
result = soup.find('span', class_='non-existent')
if result:
    print(result.text)
else:
    print("요소를 찾을 수 없습니다")

find_all() – 모든 요소 찾기

import requests
from bs4 import BeautifulSoup

url = '<https://example.com>'
headers = {'User-Agent': 'Mozilla/5.0'}
html = requests.get(url, headers=headers).text
soup = BeautifulSoup(html, 'html.parser')

# 모든 a 태그 찾기
links = soup.find_all('a')
for link in links:
    print(link.get('href'))

# 특정 class의 모든 요소
articles = soup.find_all('div', class_='article')
for article in articles:
    print(article.text.strip())

# 개수 제한
first_3_links = soup.find_all('a', limit=3)

# 여러 태그 동시에 찾기
elements = soup.find_all(['h1', 'h2', 'h3'])
for element in elements:
    print(element.text)

# 정규식으로 class 찾기
import re
divs = soup.find_all('div', class_=re.compile('article'))
# class 이름에 'article'이 포함된 모든 div

select()와 select_one() – CSS 선택자

select_one() – 첫 번째 매칭 요소

import requests
from bs4 import BeautifulSoup

url = '<https://sports.news.naver.com/index.nhn>'
headers = {'User-Agent': 'Mozilla/5.0'}
html = requests.get(url, headers=headers).text
soup = BeautifulSoup(html, 'html.parser')

# CSS 선택자로 찾기
tag = soup.select_one('#content > div > div.today_section > ul > li:nth-child(1) > a > div.text_area')
if tag:
    print(tag.text.strip())

# 태그 이름으로
p = soup.select_one('p')

# class로
article = soup.select_one('.article')

# id로
header = soup.select_one('#header')

# 태그 + class
div_article = soup.select_one('div.article')

# 태그 + id
main_div = soup.select_one('div#main')

# 여러 속성 조합
specific = soup.select_one('p.intro#first')

select() – 모든 매칭 요소

import requests
from bs4 import BeautifulSoup

url = '<https://example.com>'
headers = {'User-Agent': 'Mozilla/5.0'}
html = requests.get(url, headers=headers).text
soup = BeautifulSoup(html, 'html.parser')

# 모든 p 태그
paragraphs = soup.select('p')

# 모든 .article class
articles = soup.select('.article')

# 모든 #menu id (보통 하나만 있음)
menu = soup.select('#menu')

# 자식 선택자 (>)
direct_children = soup.select('div.container > p')
# container의 직계 자식 p만

# 자손 선택자 (공백)
all_descendants = soup.select('div.container p')
# container 아래 모든 p (손자, 증손자 포함)

# 복잡한 선택자
items = soup.select('div.list > ul > li.item')

# 속성 선택자
external_links = soup.select('a[target="_blank"]')
news_links = soup.select('a[href*="news"]')  # href에 'news' 포함

CSS 선택자 완전 정리

기본 선택자

# 태그 선택
soup.select('p')           # 모든 p 태그
soup.select('div')         # 모든 div 태그

# class 선택
soup.select('.article')    # class="article"
soup.select('.news-item')  # class="news-item"

# id 선택
soup.select('#header')     # id="header"
soup.select('#content')    # id="content"

# 태그 + class
soup.select('div.article') # <div class="article">

# 태그 + id
soup.select('div#main')    # <div id="main">

# 여러 class
soup.select('.item.active') # class="item active" 모두 포함

관계 선택자

# 자식 선택자 (>) - 직계 자식만
soup.select('div > p')
soup.select('ul.menu > li')
soup.select('#content > div.article')

# 자손 선택자 (공백) - 모든 하위 요소
soup.select('div p')
soup.select('body div')
soup.select('#content div')

# 인접 형제 선택자 (+)
soup.select('h1 + p')  # h1 바로 다음 p

# 일반 형제 선택자 (~)
soup.select('h1 ~ p')  # h1 이후 모든 p

속성 선택자

# 속성 존재
soup.select('a[href]')      # href 속성이 있는 모든 a

# 속성 값 일치
soup.select('a[href="/home"]')
soup.select('input[type="text"]')

# 속성 값 포함
soup.select('a[href*="news"]')     # href에 'news' 포함
soup.select('img[src*=".jpg"]')    # src에 '.jpg' 포함

# 속성 값 시작
soup.select('a[href^="https"]')    # https로 시작
soup.select('img[src^="/images"]')

# 속성 값 끝
soup.select('a[href$=".pdf"]')     # .pdf로 끝남
soup.select('img[src$=".png"]')

# 여러 속성
soup.select('a[href][target="_blank"]')

가상 선택자

# nth-child
soup.select('li:nth-child(1)')     # 첫 번째 li
soup.select('li:nth-child(2)')     # 두 번째 li
soup.select('li:nth-child(odd)')   # 홀수 번째
soup.select('li:nth-child(even)')  # 짝수 번째

# first-child, last-child
soup.select('li:first-child')
soup.select('li:last-child')

# not (제외)
soup.select('div:not(.exclude)')   # exclude 클래스 제외

정규식 활용

정규식으로 찾기

import requests
from bs4 import BeautifulSoup
import re

headers = {'User-Agent': 'Mozilla/5.0'}
html = requests.get('<https://example.com>', headers=headers).text
soup = BeautifulSoup(html, 'html.parser')

# class 이름에 'article' 포함
articles = soup.find_all('div', class_=re.compile('article'))
# <div class="article-main">
# <div class="news-article">
# <div class="article">

# id에 'section' 포함
sections = soup.find_all('div', id=re.compile('section'))
# <div id="section1">
# <div id="main-section">

# href에 특정 패턴
links = soup.find_all('a', href=re.compile(r'/news/\\\\d+'))
# /news/123, /news/456 등

# 텍스트 내용으로 찾기
elements = soup.find_all(text=re.compile('파이썬'))
for elem in elements:
    print(elem)

# 태그 이름으로 찾기
headings = soup.find_all(re.compile('^h[1-6]$'))
# h1, h2, h3, h4, h5, h6

정규식 패턴 예제

import re

# 이메일 찾기
emails = soup.find_all(text=re.compile(r'\\\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\\\.[A-Z|a-z]{2,}\\\\b'))

# 전화번호 찾기
phones = soup.find_all(text=re.compile(r'\\\\d{2,3}-\\\\d{3,4}-\\\\d{4}'))

# URL 찾기
urls = soup.find_all('a', href=re.compile(r'https?://'))

# 숫자만 포함된 class
numeric_classes = soup.find_all('div', class_=re.compile(r'\\\\d+'))

# 날짜 패턴 (YYYY-MM-DD)
dates = soup.find_all(text=re.compile(r'\\\\d{4}-\\\\d{2}-\\\\d{2}'))

데이터 추출 방법

텍스트 추출

# .text - 모든 텍스트 (태그 제거)
element = soup.find('div', class_='content')
text = element.text
print(text)

# .get_text() - 옵션 지정 가능
text = element.get_text()
text_no_space = element.get_text(strip=True)  # 공백 제거
text_newline = element.get_text(separator='\\\\n')  # 구분자 지정

# .string - 직접 자식 텍스트만
text = element.string

# .stripped_strings - 공백 제거된 텍스트 리스트
for string in element.stripped_strings:
    print(string)

속성 추출

# 링크 href
link = soup.find('a')
href = link.get('href')
# 또는
href = link['href']

# 이미지 src
img = soup.find('img')
src = img.get('src')
alt = img.get('alt')

# 여러 속성
element = soup.find('div')
attrs = element.attrs
print(attrs)  # {'class': ['content'], 'id': 'main'}

# class 추출 (리스트로 반환)
classes = element.get('class')
print(classes)  # ['content', 'active']

# data-* 속성
data_id = element.get('data-id')
data_value = element['data-value']

실전 크롤링 예제

예제 1: 네이버 뉴스 제목 수집

import requests
from bs4 import BeautifulSoup

def crawl_naver_news():
    url = '<https://news.naver.com/>'
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
    }

    response = requests.get(url, headers=headers)
    soup = BeautifulSoup(response.text, 'html.parser')

    # CSS 선택자로 뉴스 제목 찾기
    titles = soup.select('.cjs_news_ui .cjs_t')

    print("📰 네이버 주요 뉴스")
    print("=" * 50)

    for i, title in enumerate(titles[:10], 1):
        print(f"{i}. {title.text.strip()}")

crawl_naver_news()

예제 2: 링크와 텍스트 함께 추출

import requests
from bs4 import BeautifulSoup

def extract_links_and_text(url):
    headers = {'User-Agent': 'Mozilla/5.0'}
    response = requests.get(url, headers=headers)
    soup = BeautifulSoup(response.text, 'html.parser')

    # 모든 링크 추출
    links = soup.find_all('a', href=True)

    results = []
    for link in links:
        text = link.text.strip()
        href = link['href']

        # 절대 URL로 변환
        if href.startswith('/'):
            href = url + href

        if text and href:
            results.append({
                'text': text,
                'url': href
            })

    return results

# 사용
data = extract_links_and_text('<https://example.com>')
for item in data[:5]:
    print(f"{item['text']}: {item['url']}")

예제 3: 테이블 데이터 추출

import requests
from bs4 import BeautifulSoup
import pandas as pd

def scrape_table(url):
    headers = {'User-Agent': 'Mozilla/5.0'}
    response = requests.get(url, headers=headers)
    soup = BeautifulSoup(response.text, 'html.parser')

    # 테이블 찾기
    table = soup.find('table')

    # 헤더 추출
    headers = []
    for th in table.find_all('th'):
        headers.append(th.text.strip())

    # 데이터 추출
    rows = []
    for tr in table.find_all('tr')[1:]:  # 헤더 제외
        row = []
        for td in tr.find_all('td'):
            row.append(td.text.strip())
        if row:
            rows.append(row)

    # DataFrame 생성
    df = pd.DataFrame(rows, columns=headers)
    return df

# 사용
df = scrape_table('<https://example.com/table>')
print(df.head())
df.to_csv('table_data.csv', index=False, encoding='utf-8-sig')

예제 4: 이미지 다운로드

import requests
from bs4 import BeautifulSoup
import os

def download_images(url, save_dir='images'):
    headers = {'User-Agent': 'Mozilla/5.0'}
    response = requests.get(url, headers=headers)
    soup = BeautifulSoup(response.text, 'html.parser')

    # 저장 디렉토리 생성
    os.makedirs(save_dir, exist_ok=True)

    # 모든 이미지 찾기
    images = soup.find_all('img', src=True)

    for i, img in enumerate(images, 1):
        img_url = img['src']

        # 상대 URL 처리
        if img_url.startswith('//'):
            img_url = 'https:' + img_url
        elif img_url.startswith('/'):
            img_url = url + img_url

        try:
            # 이미지 다운로드
            img_data = requests.get(img_url, headers=headers).content

            # 파일명 생성
            filename = f"image_{i}.jpg"
            filepath = os.path.join(save_dir, filename)

            # 저장
            with open(filepath, 'wb') as f:
                f.write(img_data)

            print(f"✅ {filename} 다운로드 완료")

        except Exception as e:
            print(f"❌ {img_url} 다운로드 실패: {e}")

# 사용
download_images('<https://example.com>')

예제 5: 페이지네이션 처리

import requests
from bs4 import BeautifulSoup
import time

def crawl_multiple_pages(base_url, max_pages=5):
    all_data = []
    headers = {'User-Agent': 'Mozilla/5.0'}

    for page in range(1, max_pages + 1):
        url = f"{base_url}?page={page}"
        print(f"📄 페이지 {page} 크롤링 중...")

        try:
            response = requests.get(url, headers=headers, timeout=10)
            soup = BeautifulSoup(response.text, 'html.parser')

            # 데이터 추출
            items = soup.select('.item')

            for item in items:
                title = item.select_one('.title')
                link = item.select_one('a')

                if title and link:
                    all_data.append({
                        'title': title.text.strip(),
                        'url': link.get('href'),
                        'page': page
                    })

            # 서버 부담 방지
            time.sleep(1)

        except Exception as e:
            print(f"❌ 페이지 {page} 오류: {e}")
            continue

    print(f"\\\\n✅ 총 {len(all_data)}개 데이터 수집 완료")
    return all_data

# 사용
data = crawl_multiple_pages('<https://example.com/list>', max_pages=3)

자바스크립트 렌더링 사이트 처리

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
from bs4 import BeautifulSoup
import time

def crawl_js_site(url):
    # Chrome 드라이버 설정
    options = webdriver.ChromeOptions()
    options.add_argument('--headless')  # 백그라운드 실행
    options.add_argument('--no-sandbox')
    options.add_argument('user-agent=Mozilla/5.0')

    driver = webdriver.Chrome(options=options)

    try:
        driver.get(url)

        # 페이지 로딩 대기
        time.sleep(3)

        # 또는 특정 요소가 나타날 때까지 대기
        wait = WebDriverWait(driver, 10)
        wait.until(EC.presence_of_element_located((By.CLASS_NAME, 'content')))

        # HTML 가져오기
        html = driver.page_source
        soup = BeautifulSoup(html, 'html.parser')

        # 데이터 추출
        data = soup.select('.item')
        for item in data:
            print(item.text.strip())

    finally:
        driver.quit()

# 사용
crawl_js_site('<https://js-rendered-site.com>')

필요한 경우: “이 사이트의 기능을 모두 이용하기 위해서는 자바스크립트를 활성화시켜야 합니다” 메시지가 나올 때

에러 처리 및 안정성

import requests
from bs4 import BeautifulSoup
import time

def safe_crawl(url):
    headers = {'User-Agent': 'Mozilla/5.0'}

    try:
        # 타임아웃 설정
        response = requests.get(url, headers=headers, timeout=10)

        # 상태 코드 확인
        response.raise_for_status()

        # 인코딩 설정
        response.encoding = response.apparent_encoding

        soup = BeautifulSoup(response.text, 'html.parser')

        # 요소 안전하게 찾기
        title = soup.find('h1', class_='title')
        if title:
            print(f"제목: {title.text.strip()}")
        else:
            print("제목을 찾을 수 없습니다")

        return soup

    except requests.exceptions.Timeout:
        print(f"❌ 타임아웃: {url}")
        return None

    except requests.exceptions.HTTPError as e:
        print(f"❌ HTTP 오류: {e}")
        return None

    except requests.exceptions.ConnectionError:
        print(f"❌ 연결 오류: {url}")
        return None

    except Exception as e:
        print(f"❌ 예상치 못한 오류: {e}")
        return None

# 사용
result = safe_crawl('<https://example.com>')

마치며

BeautifulSoup은 find()로 단일 요소를, select()로 CSS 선택자 기반 검색을, 정규식으로 패턴 매칭을 수행하는 3가지 핵심 방법으로 HTML 데이터를 추출합니다. 반드시 헤더를 설정하고, 에러 처리를 추가하며, 서버에 부담을 주지 않도록 딜레이를 넣어야 합니다.

자바스크립트 렌더링 사이트는 Selenium을 사용하고, robots.txt와 이용약관을 준수하며, 수집한 데이터의 법적 사용 범위를 확인하세요. 이 가이드의 예제로 실전 크롤링 프로젝트를 시작해보세요!


참고 자료

댓글 남기기