BeautifulSoup과 Requests로 웹 크롤링 완벽 가이드

웹사이트에서 필요한 데이터만 추출하고 싶으신가요? HTML 코드 전체가 아닌 특정 요소의 값만 가져오는 방법이 궁금하신가요? 이 글에서는 BeautifulSoup과 Requests를 활용한 실전 웹 크롤링 기법을 완벽하게 알려드립니다.

웹 크롤링 기본 원리

웹 크롤링은 requests로 HTML을 가져오고, BeautifulSoup으로 파싱한 후 원하는 데이터를 추출하는 3단계 과정입니다. 검색 순위 키워드, 뉴스 제목, 상품 가격 등 모든 웹 데이터를 자동으로 수집할 수 있습니다.

기본 코드 구조

HTML 전체 가져오기

import requests
from bs4 import BeautifulSoup

url = '<https://www.naver.com>'
html = requests.get(url)
soup = BeautifulSoup(html.text, 'html.parser')

print(soup.prettify())  # HTML을 보기 좋게 출력

핵심 요소:

  • requests.get(url): 웹페이지 요청
  • html.text: HTML 문자열 추출
  • BeautifulSoup(html.text, 'html.parser'): HTML 파싱

CSS 선택자로 요소 찾기

select() – 여러 개 찾기

import requests
from bs4 import BeautifulSoup

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

# CSS 선택자로 여러 요소 찾기
elements = soup.select('div.content')

for element in elements:
    print(element.text)

CSS 선택자 기본:

  • div: 모든 div 태그
  • .classname: 특정 class
  • #idname: 특정 id
  • div.content: div 태그 중 content 클래스
  • div > p: div의 직계 자식 p 태그
  • div p: div 안의 모든 p 태그

select_one() – 하나만 찾기

# 첫 번째 매칭되는 요소만 가져오기
element = soup.select_one('h1.title')

if element:
    print(element.text)
else:
    print("요소를 찾을 수 없습니다")

차이점: select()는 리스트 반환, select_one()은 단일 요소 또는 None 반환

태그 검색 메서드

find() vs find_all()

import requests
from bs4 import BeautifulSoup

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

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

# 모든 h1 태그 찾기
all_h1 = soup.find_all('h1')
for h1 in all_h1:
    print(h1.text)

# class로 찾기
content_div = soup.find('div', class_='content')
print(content_div.text)

# id로 찾기
header = soup.find('div', id='header')
print(header.text)

# 여러 조건
result = soup.find('a', class_='link', href=True)

find vs select 비교:

  • find(): 파이썬 문법, 간단한 검색
  • select(): CSS 선택자, 복잡한 검색

값 추출 방법

텍스트 추출

import requests
from bs4 import BeautifulSoup

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

# 텍스트만 추출
element = soup.select_one('h1.title')
text = element.text
print(text)

# 또는
text = element.get_text()
print(text)

# 공백 제거
text = element.text.strip()
print(text)

# 줄바꿈 제거 및 공백 정리
text = ' '.join(element.text.split())
print(text)

text vs get_text():

  • .text: 속성으로 접근
  • .get_text(): 메서드, 옵션 설정 가능

속성값 추출

# href 속성 가져오기
link = soup.select_one('a.main-link')
href = link['href']
print(href)

# 또는
href = link.get('href')
print(href)

# 이미지 src
img = soup.select_one('img.thumbnail')
src = img['src']
print(src)

# 여러 속성
attrs = link.attrs
print(attrs)  # {'href': '...', 'class': ['main-link'], ...}

# 속성 존재 확인
if link.has_attr('href'):
    print(link['href'])

안전한 접근: get() 메서드를 사용하면 속성이 없어도 None을 반환합니다.

data 속성 추출

# data-* 속성
element = soup.select_one('div.item')
data_id = element['data-id']
data_price = element['data-price']

print(f"ID: {data_id}, 가격: {data_price}")

실전 크롤링 예제

네이버 실시간 검색어

import requests
from bs4 import BeautifulSoup

def crawl_naver_realtime():
    url = '<https://www.naver.com>'

    # User-Agent 추가 (차단 방지)
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
    }

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

    # CSS 선택자로 검색어 추출
    keywords = soup.select('.ah_k')

    print("🔥 네이버 실시간 검색어")
    for i, keyword in enumerate(keywords[:10], 1):
        print(f"{i}. {keyword.text.strip()}")

crawl_naver_realtime()

핵심: User-Agent를 추가해 봇 차단을 우회합니다.

뉴스 헤드라인 수집

import requests
from bs4 import BeautifulSoup

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

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

    # 뉴스 제목 찾기 (사이트마다 다름)
    headlines = soup.select('a.news_tit')

    news_list = []
    for headline in headlines:
        title = headline.text.strip()
        link = headline['href']

        news_list.append({
            'title': title,
            'link': link
        })

    return news_list

# 사용 예시
url = '<https://news.naver.com/>'
news = crawl_news_headlines(url)

for i, item in enumerate(news[:5], 1):
    print(f"{i}. {item['title']}")
    print(f"   {item['link']}\\\\n")

구조화: 딕셔너리 리스트로 데이터를 정리합니다.

쇼핑몰 상품 정보

import requests
from bs4 import BeautifulSoup

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

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

    # 상품명
    product_name = soup.select_one('h1.product-title')

    # 가격
    price = soup.select_one('span.price')

    # 이미지
    image = soup.select_one('img.product-image')

    product_info = {
        'name': product_name.text.strip() if product_name else 'N/A',
        'price': price.text.strip() if price else 'N/A',
        'image_url': image['src'] if image else 'N/A'
    }

    return product_info

# 사용 예시
product = crawl_product_info('<https://example-shop.com/product/123>')
print(f"상품명: {product['name']}")
print(f"가격: {product['price']}")
print(f"이미지: {product['image_url']}")

None 체크: 요소가 없을 때를 대비한 안전한 코드입니다.

테이블 데이터 추출

import requests
from bs4 import BeautifulSoup
import pandas as pd

def crawl_table_data(url):
    html = requests.get(url)
    soup = BeautifulSoup(html.text, 'html.parser')

    # 테이블 찾기
    table = soup.select_one('table.data-table')

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

    # 데이터 추출
    rows = []
    for tr in table.select('tbody tr'):
        row = []
        for td in tr.select('td'):
            row.append(td.text.strip())
        rows.append(row)

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

# 사용 예시
df = crawl_table_data('<https://example.com/data-table>')
print(df.head())
df.to_csv('table_data.csv', index=False)

판다스 연동: 테이블 데이터를 DataFrame으로 쉽게 변환합니다.

고급 크롤링 기법

페이지네이션 처리

import requests
from bs4 import BeautifulSoup
import time

def crawl_multiple_pages(base_url, max_pages=5):
    all_items = []

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

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

        items = soup.select('div.item')
        for item in items:
            title = item.select_one('h3.title').text.strip()
            all_items.append(title)

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

    return all_items

# 사용 예시
items = crawl_multiple_pages('<https://example.com/list>', max_pages=3)
print(f"총 {len(items)}개 항목 수집")

예의: 페이지 간 1초 딜레이로 서버 부담을 줄입니다.

동적 검색어 처리

import requests
from bs4 import BeautifulSoup

def search_and_crawl(keyword):
    # 검색 URL 생성
    search_url = f"<https://example.com/search?q={keyword}>"

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

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

    results = soup.select('div.search-result')

    search_results = []
    for result in results:
        title = result.select_one('h2.title').text.strip()
        link = result.select_one('a')['href']

        search_results.append({
            'title': title,
            'link': link
        })

    return search_results

# 사용 예시
keywords = ['파이썬', '크롤링', '데이터분석']
for keyword in keywords:
    print(f"\\\\n🔍 '{keyword}' 검색 결과:")
    results = search_and_crawl(keyword)
    for result in results[:3]:
        print(f"- {result['title']}")

자동화: 여러 키워드를 반복 검색합니다.

중첩 요소 탐색

import requests
from bs4 import BeautifulSoup

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

# 부모에서 자식 탐색
parent = soup.select_one('div.parent')
children = parent.select('div.child')

for child in children:
    print(child.text)

# 형제 요소 찾기
element = soup.select_one('h2.title')
next_sibling = element.find_next_sibling('p')
print(next_sibling.text)

# 부모 요소 찾기
child = soup.select_one('span.price')
parent_div = child.find_parent('div')
print(parent_div['class'])

# 특정 부모까지 올라가기
section = child.find_parent('section')

계층 탐색: 복잡한 HTML 구조에서 원하는 요소를 정확히 찾습니다.

에러 처리 및 예외 상황

안전한 크롤링 코드

import requests
from bs4 import BeautifulSoup

def safe_crawl(url):
    try:
        # 타임아웃 설정
        html = requests.get(url, timeout=10)

        # 상태 코드 확인
        html.raise_for_status()  # 4xx, 5xx 에러 시 예외 발생

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

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

        return soup

    except requests.exceptions.Timeout:
        print("❌ 타임아웃: 서버 응답 시간 초과")
        return None

    except requests.exceptions.ConnectionError:
        print("❌ 연결 오류: 네트워크 확인 필요")
        return None

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

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

# 사용 예시
result = safe_crawl('<https://example.com>')
if result:
    print("✅ 크롤링 성공")

견고성: 모든 예외 상황을 처리해 프로그램이 중단되지 않습니다.

인코딩 문제 해결

import requests
from bs4 import BeautifulSoup

url = '<https://example.com>'
html = requests.get(url)

# 자동 인코딩 감지
html.encoding = html.apparent_encoding

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

# 또는 명시적 인코딩
html.encoding = 'utf-8'
soup = BeautifulSoup(html.text, 'html.parser')

# 한글 깨짐 방지
html = requests.get(url)
soup = BeautifulSoup(html.content, 'html.parser', from_encoding='utf-8')

한글 처리: 인코딩을 올바르게 설정하면 한글이 깨지지 않습니다.

데이터 저장하기

CSV 파일로 저장

import requests
from bs4 import BeautifulSoup
import csv

def crawl_and_save_csv(url, filename='data.csv'):
    html = requests.get(url)
    soup = BeautifulSoup(html.text, 'html.parser')

    items = soup.select('div.item')

    with open(filename, 'w', encoding='utf-8-sig', newline='') as f:
        writer = csv.writer(f)
        writer.writerow(['순위', '제목', '링크'])  # 헤더

        for i, item in enumerate(items, 1):
            title = item.select_one('h3').text.strip()
            link = item.select_one('a')['href']
            writer.writerow([i, title, link])

    print(f"✅ {filename}에 저장 완료")

crawl_and_save_csv('<https://example.com/list>')

엑셀 호환: utf-8-sig 인코딩을 사용하면 엑셀에서 한글이 정상 표시됩니다.

JSON 파일로 저장

import requests
from bs4 import BeautifulSoup
import json
from datetime import datetime

def crawl_and_save_json(url, filename='data.json'):
    html = requests.get(url)
    soup = BeautifulSoup(html.text, 'html.parser')

    items = soup.select('div.item')

    data = {
        'collected_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
        'total_count': len(items),
        'items': []
    }

    for item in items:
        title = item.select_one('h3').text.strip()
        link = item.select_one('a')['href']

        data['items'].append({
            'title': title,
            'link': link
        })

    with open(filename, 'w', encoding='utf-8') as f:
        json.dump(data, f, ensure_ascii=False, indent=2)

    print(f"✅ {filename}에 저장 완료")

crawl_and_save_json('<https://example.com/list>')

구조화 데이터: JSON은 복잡한 데이터 구조를 저장하기 좋습니다.

데이터베이스 저장

import requests
from bs4 import BeautifulSoup
import pymysql
from datetime import datetime

def crawl_and_save_db(url):
    # 크롤링
    html = requests.get(url)
    soup = BeautifulSoup(html.text, 'html.parser')
    items = soup.select('div.item')

    # DB 연결
    conn = pymysql.connect(
        host='localhost',
        user='root',
        password='password',
        database='crawling_db'
    )
    cursor = conn.cursor()

    # 테이블 생성
    cursor.execute("""
        CREATE TABLE IF NOT EXISTS crawled_data (
            id INT AUTO_INCREMENT PRIMARY KEY,
            title VARCHAR(255),
            link VARCHAR(500),
            collected_at DATETIME
        )
    """)

    # 데이터 삽입
    sql = "INSERT INTO crawled_data (title, link, collected_at) VALUES (%s, %s, %s)"

    for item in items:
        title = item.select_one('h3').text.strip()
        link = item.select_one('a')['href']
        collected_at = datetime.now()

        cursor.execute(sql, (title, link, collected_at))

    conn.commit()
    print(f"✅ {len(items)}개 데이터 DB 저장 완료")

    cursor.close()
    conn.close()

crawl_and_save_db('<https://example.com/list>')

대용량 처리: 데이터베이스는 대량의 크롤링 데이터를 효율적으로 관리합니다.

성능 최적화

Session 재사용

import requests
from bs4 import BeautifulSoup

# Session 객체 생성 (연결 재사용)
session = requests.Session()
session.headers.update({
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
})

urls = [
    '<https://example.com/page1>',
    '<https://example.com/page2>',
    '<https://example.com/page3>'
]

for url in urls:
    html = session.get(url)
    soup = BeautifulSoup(html.text, 'html.parser')
    # 크롤링 로직
    print(f"✅ {url} 완료")

session.close()

성능 향상: Session을 재사용하면 연결 오버헤드가 줄어듭니다.

병렬 처리

import requests
from bs4 import BeautifulSoup
from concurrent.futures import ThreadPoolExecutor

def crawl_single_page(url):
    html = requests.get(url)
    soup = BeautifulSoup(html.text, 'html.parser')
    title = soup.select_one('h1').text.strip()
    return {'url': url, 'title': title}

urls = [f'<https://example.com/page{i}>' for i in range(1, 11)]

# 병렬로 크롤링 (최대 5개 동시)
with ThreadPoolExecutor(max_workers=5) as executor:
    results = list(executor.map(crawl_single_page, urls))

for result in results:
    print(f"{result['title']}: {result['url']}")

속도 향상: 여러 페이지를 동시에 크롤링해 시간을 단축합니다.

마치며

BeautifulSoup과 Requests로 웹 크롤링하는 핵심은 requests.get()으로 HTML을 가져오고, BeautifulSoup으로 파싱한 후, select() 또는 find()로 원하는 요소를 추출하는 것입니다.

CSS 선택자를 마스터하고, 안전한 에러 처리를 추가하며, 적절한 형식으로 데이터를 저장하면 실무급 크롤러를 만들 수 있습니다. User-Agent 설정과 딜레이 추가로 서버에 부담을 주지 않는 것도 잊지 마세요.

이 가이드의 예제를 활용해 원하는 웹 데이터를 자동으로 수집하는 프로젝트를 시작해보세요!


참고 자료

댓글 남기기