파이썬 데이터 수집 애플리케이션 만들기 [시리즈 1탄] - 웹 스크래핑 기초부터 실전까지

파이썬 데이터 수집 애플리케이션 만들기 [시리즈 1탄] - 웹 스크래핑 기초부터 실전까지

🚀 왜 데이터 수집 애플리케이션을 만들어야 할까요?

혹시 이런 경험 있으시나요?

"매일 같은 웹사이트에서 정보를 확인하는 것이 번거로워서 자동화하고 싶다" 또는 "경쟁사 가격 정보를 매일 체크해야 하는데 손으로 하기엔 너무 비효율적이다"

실제로 2024년 네이버 검색 데이터를 보면, 파이썬 데이터 수집 관련 키워드가 3-5월에 최고치를 기록했습니다. 웹 스크래핑과 BeautifulSoup 관련 검색량도 꾸준히 증가하고 있어, 많은 분들이 데이터 수집 자동화에 관심을 갖고 계신 것 같습니다.

제가 지난 3년간 여러 데이터 수집 프로젝트를 진행하면서 깨달은 것은, 올바른 순서와 방법으로 접근하면 초보자도 충분히 실용적인 애플리케이션을 만들 수 있다는 점입니다.

이 시리즈에서는 파이썬을 이용해 실제 업무에서 사용할 수 있는 데이터 수집 애플리케이션을 차근차근 만들어보겠습니다. 첫 번째 시간에는 웹 스크래핑의 기초부터 시작해서 간단한 뉴스 수집기를 만들어보겠습니다.

📋 시리즈 전체 로드맵

이 시리즈는 총 6편으로 구성됩니다:

  • 1탄 (오늘): 웹 스크래핑 기초 - 뉴스 헤드라인 수집기
  • 2탄: API 활용 데이터 수집 - 소셜미디어 데이터 수집
  • 3탄: 데이터베이스 연동 - 수집 데이터 체계적 저장
  • 4탄: 스케줄링과 자동화 - 주기적 데이터 수집
  • 5탄: 데이터 정제와 분석 - 수집 데이터 활용
  • 6탄: 대시보드 구축 - 시각화와 모니터링

🎯 1탄 목표: 뉴스 헤드라인 수집기 만들기

오늘 우리가 만들 프로그램의 기능:

  • ✅ 네이버 뉴스에서 최신 헤드라인 수집
  • ✅ 제목, 링크, 발행시간 추출
  • ✅ CSV 파일로 저장
  • ✅ 중복 제거 기능
  • ✅ 에러 처리 및 예외 상황 대응

🛠️ 필요한 도구와 환경 설정

1) 파이썬 설치 확인

먼저 파이썬이 설치되어 있는지 확인해보세요:

python --version
# 또는
python3 --version

파이썬 3.7 이상이 설치되어 있으면 됩니다. 만약 설치되어 있지 않다면 공식 웹사이트에서 다운로드하세요.

2) 필요한 라이브러리 설치

터미널에서 다음 명령어를 실행하세요:

pip install requests beautifulsoup4 pandas lxml
💡 라이브러리 설명:
- requests: 웹 페이지 요청을 위한 라이브러리
- beautifulsoup4: HTML 파싱을 위한 라이브러리
- pandas: 데이터 처리 및 CSV 저장용
- lxml: 빠른 XML/HTML 파서

3) 개발 환경 준비

추천하는 개발 환경:

  • VS Code (무료, 확장성 좋음)
  • PyCharm Community (무료, 파이썬 특화)
  • Jupyter Notebook (실험적 코딩에 최적)

🔍 웹 스크래핑 기초 이론

웹 스크래핑이란?

웹 스크래핑은 웹 페이지에서 필요한 데이터를 자동으로 추출하는 기술입니다. 마치 사람이 웹 브라우저로 페이지를 보고 정보를 복사하는 것을 프로그램이 대신 해주는 것입니다.

기본 작동 원리

  1. HTTP 요청: 웹 페이지에 데이터를 요청
  2. HTML 수신: 서버로부터 HTML 코드를 받음
  3. 파싱: HTML을 분석해서 필요한 부분 찾기
  4. 데이터 추출: 원하는 정보만 골라내기
  5. 저장/처리: 추출한 데이터를 파일이나 DB에 저장

주의사항 (중요!)

⚠️ 윤리적 스크래핑을 위한 가이드라인:
1. robots.txt 파일 확인 (예: https://news.naver.com/robots.txt)
2. 요청 간격 두기 (서버 부하 방지)
3. 사이트 이용약관 준수
4. 개인정보 수집 금지
5. 상업적 이용 시 사전 허가 받기

💻 실습 1: 기본 웹 페이지 요청하기

먼저 간단한 예제로 웹 페이지를 요청하는 방법을 알아보겠습니다:

import requests
from bs4 import BeautifulSoup
import time

# 1. 웹 페이지 요청
def fetch_webpage(url):
    """웹 페이지를 요청하고 HTML을 반환하는 함수"""
    try:
        # User-Agent 설정 (봇이 아닌 브라우저인 것처럼 보이게 함)
        headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
        }
        
        # 웹 페이지 요청
        response = requests.get(url, headers=headers)
        
        # 요청 성공 확인
        if response.status_code == 200:
            print(f"✅ 성공적으로 페이지를 가져왔습니다: {url}")
            return response.text
        else:
            print(f"❌ 요청 실패: HTTP {response.status_code}")
            return None
            
    except requests.exceptions.RequestException as e:
        print(f"❌ 연결 오류: {e}")
        return None

# 2. 테스트
if __name__ == "__main__":
    # 테스트용 URL
    test_url = "https://httpbin.org/html"
    
    html_content = fetch_webpage(test_url)
    
    if html_content:
        print("📄 받아온 HTML의 일부:")
        print(html_content[:200] + "...")

💻 실습 2: HTML 파싱 기초

이제 받아온 HTML에서 원하는 정보를 추출해보겠습니다:

from bs4 import BeautifulSoup

def parse_html_basic():
    """BeautifulSoup을 이용한 기본 HTML 파싱 예제"""
    
    # 예제 HTML
    sample_html = """
    <html>
        <head>
            <title>뉴스 테스트 페이지</title>
        </head>
        <body>
            <div class="news-container">
                <article class="news-item">
                    <h2 class="title">파이썬으로 데이터 수집하기</h2>
                    <span class="date">2025-01-20</span>
                    <a href="/news/1" class="link">전체 기사 보기</a>
                </article>
                <article class="news-item">
                    <h2 class="title">웹 스크래핑 기초 가이드</h2>
                    <span class="date">2025-01-19</span>
                    <a href="/news/2" class="link">전체 기사 보기</a>
                </article>
            </div>
        </body>
    </html>
    """
    
    # BeautifulSoup 객체 생성
    soup = BeautifulSoup(sample_html, 'html.parser')
    
    print("🔍 HTML 파싱 결과:")
    print("-" * 50)
    
    # 1. 제목 추출
    title = soup.find('title').get_text()
    print(f"페이지 제목: {title}")
    
    # 2. 모든 뉴스 제목 추출
    news_titles = soup.find_all('h2', class_='title')
    print(f"\n📰 뉴스 제목들:")
    for i, title in enumerate(news_titles, 1):
        print(f"{i}. {title.get_text()}")
    
    # 3. 날짜 정보 추출
    dates = soup.find_all('span', class_='date')
    print(f"\n📅 발행 날짜들:")
    for i, date in enumerate(dates, 1):
        print(f"{i}. {date.get_text()}")
    
    # 4. 링크 추출
    links = soup.find_all('a', class_='link')
    print(f"\n🔗 링크들:")
    for i, link in enumerate(links, 1):
        print(f"{i}. {link.get('href')}")

# 실행
parse_html_basic()

💻 실습 3: 뉴스 헤드라인 수집기 만들기

이제 본격적으로 네이버 뉴스에서 헤드라인을 수집하는 프로그램을 만들어보겠습니다:

import requests
from bs4 import BeautifulSoup
import pandas as pd
import time
from datetime import datetime
from urllib.parse import urljoin
import os

class NewsCollector:
    """뉴스 헤드라인 수집기 클래스"""
    
    def __init__(self):
        self.headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
        }
        self.news_data = []
    
    def collect_naver_news(self, category='정치', max_pages=3):
        """네이버 뉴스에서 헤드라인 수집"""
        
        print(f"🔄 {category} 뉴스 수집을 시작합니다...")
        
        # 카테고리별 URL 매핑
        category_codes = {
            '정치': '100', '경제': '101', '사회': '102', 
            '생활문화': '103', '세계': '104', 'IT과학': '105'
        }
        
        if category not in category_codes:
            print(f"❌ 지원하지 않는 카테고리입니다: {category}")
            return
        
        category_code = category_codes[category]
        
        for page in range(1, max_pages + 1):
            print(f"📖 {page}페이지 수집 중...")
            
            # 네이버 뉴스 URL 구성
            url = f"https://news.naver.com/main/list.naver?mode=LS2D&mid=shm&sid1={category_code}&page={page}"
            
            try:
                # 페이지 요청
                response = requests.get(url, headers=self.headers)
                response.raise_for_status()
                
                # HTML 파싱
                soup = BeautifulSoup(response.text, 'html.parser')
                
                # 뉴스 기사 목록 추출
                news_items = soup.find_all('li', {'data-comment-count-group': True})
                
                for item in news_items:
                    news_info = self.extract_news_info(item, category)
                    if news_info:
                        self.news_data.append(news_info)
                
                # 요청 간격 두기 (예의를 지키는 스크래핑)
                time.sleep(1)
                
            except requests.RequestException as e:
                print(f"❌ 페이지 요청 실패: {e}")
                continue
        
        print(f"✅ 총 {len(self.news_data)}개의 뉴스를 수집했습니다.")
    
    def extract_news_info(self, item, category):
        """개별 뉴스 항목에서 정보 추출"""
        try:
            # 제목과 링크 추출
            title_elem = item.find('a')
            if not title_elem:
                return None
            
            title = title_elem.get_text().strip()
            link = title_elem.get('href', '')
            
            # 절대 URL로 변환
            if link.startswith('/'):
                link = urljoin('https://news.naver.com', link)
            
            # 언론사 추출
            press_elem = item.find('span', class_='writing')
            press = press_elem.get_text().strip() if press_elem else 'Unknown'
            
            # 현재 시간 기록
            collect_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
            
            return {
                'title': title,
                'link': link,
                'press': press,
                'category': category,
                'collect_time': collect_time
            }
            
        except Exception as e:
            print(f"⚠️ 뉴스 정보 추출 중 오류: {e}")
            return None
    
    def remove_duplicates(self):
        """중복 뉴스 제거"""
        original_count = len(self.news_data)
        
        # 제목을 기준으로 중복 제거
        unique_news = []
        seen_titles = set()
        
        for news in self.news_data:
            if news['title'] not in seen_titles:
                unique_news.append(news)
                seen_titles.add(news['title'])
        
        self.news_data = unique_news
        removed_count = original_count - len(self.news_data)
        
        if removed_count > 0:
            print(f"🧹 {removed_count}개의 중복 뉴스를 제거했습니다.")
    
    def save_to_csv(self, filename=None):
        """수집한 뉴스를 CSV 파일로 저장"""
        if not self.news_data:
            print("❌ 저장할 데이터가 없습니다.")
            return
        
        # 파일명 생성
        if not filename:
            timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
            filename = f'news_data_{timestamp}.csv'
        
        try:
            # DataFrame 생성 및 저장
            df = pd.DataFrame(self.news_data)
            df.to_csv(filename, index=False, encoding='utf-8-sig')
            
            print(f"💾 데이터가 성공적으로 저장되었습니다: {filename}")
            print(f"📊 저장된 데이터: {len(self.news_data)}개 뉴스")
            
            # 파일 경로 표시
            abs_path = os.path.abspath(filename)
            print(f"📁 파일 위치: {abs_path}")
            
        except Exception as e:
            print(f"❌ 파일 저장 중 오류: {e}")
    
    def show_summary(self):
        """수집 결과 요약 표시"""
        if not self.news_data:
            print("📊 수집된 데이터가 없습니다.")
            return
        
        print(f"\n📊 수집 결과 요약")
        print("=" * 50)
        print(f"총 뉴스 개수: {len(self.news_data)}개")
        
        # 카테고리별 개수
        categories = {}
        for news in self.news_data:
            cat = news['category']
            categories[cat] = categories.get(cat, 0) + 1
        
        print("\n📂 카테고리별 개수:")
        for cat, count in categories.items():
            print(f"  {cat}: {count}개")
        
        # 언론사별 개수 (상위 5개)
        press_count = {}
        for news in self.news_data:
            press = news['press']
            press_count[press] = press_count.get(press, 0) + 1
        
        print("\n📰 주요 언론사 (상위 5개):")
        sorted_press = sorted(press_count.items(), key=lambda x: x[1], reverse=True)[:5]
        for press, count in sorted_press:
            print(f"  {press}: {count}개")
        
        # 최신 뉴스 3개 미리보기
        print(f"\n🔍 최신 뉴스 미리보기:")
        for i, news in enumerate(self.news_data[:3], 1):
            print(f"{i}. [{news['category']}] {news['title'][:50]}...")

# 사용 예제
def main():
    """메인 실행 함수"""
    print("🚀 뉴스 헤드라인 수집기를 시작합니다!")
    print("=" * 60)
    
    # 수집기 생성
    collector = NewsCollector()
    
    # 여러 카테고리에서 뉴스 수집
    categories = ['정치', '경제', 'IT과학']
    
    for category in categories:
        collector.collect_naver_news(category=category, max_pages=2)
        time.sleep(2)  # 카테고리 간 대기
    
    # 중복 제거
    collector.remove_duplicates()
    
    # 결과 요약 표시
    collector.show_summary()
    
    # CSV 파일로 저장
    collector.save_to_csv()
    
    print("\n🎉 뉴스 수집이 완료되었습니다!")

if __name__ == "__main__":
    main()

🔧 실행 및 결과 확인

위 코드를 news_collector.py 파일로 저장한 후 실행해보세요:

python news_collector.py

예상 출력 결과:

🚀 뉴스 헤드라인 수집기를 시작합니다!
============================================================
🔄 정치 뉴스 수집을 시작합니다...
📖 1페이지 수집 중...
📖 2페이지 수집 중...
✅ 총 34개의 뉴스를 수집했습니다.

🔄 경제 뉴스 수집을 시작합니다...
📖 1페이지 수집 중...
📖 2페이지 수집 중...
✅ 총 68개의 뉴스를 수집했습니다.

📊 수집 결과 요약
==================================================
총 뉴스 개수: 68개

📂 카테고리별 개수:
  정치: 34개
  경제: 34개

💾 데이터가 성공적으로 저장되었습니다: news_data_20250120_143022.csv
📊 저장된 데이터: 68개 뉴스

📝 수집된 데이터 활용하기

CSV 파일을 열어보면 다음과 같은 구조로 데이터가 저장됩니다:

title link press category collect_time
정부, 내년 최저임금 인상률... https://news.naver.com/... 연합뉴스 정치 2025-01-20 14:30:22
코스피, 장 초반 상승세... https://news.naver.com/... 매일경제 경제 2025-01-20 14:31:15

추가 분석 예제

수집한 데이터로 간단한 분석을 해보겠습니다:

import pandas as pd
import matplotlib.pyplot as plt
from collections import Counter

def analyze_news_data(csv_file):
    """수집한 뉴스 데이터 분석"""
    try:
        # CSV 파일 읽기
        df = pd.read_csv(csv_file)
        
        print("📈 뉴스 데이터 분석 결과")
        print("=" * 40)
        
        # 1. 기본 통계
        print(f"총 뉴스 개수: {len(df)}개")
        print(f"카테고리 수: {df['category'].nunique()}개")
        print(f"언론사 수: {df['press'].nunique()}개")
        
        # 2. 카테고리별 뉴스 개수
        print(f"\n📊 카테고리별 뉴스 개수:")
        category_counts = df['category'].value_counts()
        for category, count in category_counts.items():
            print(f"  {category}: {count}개")
        
        # 3. 가장 많이 나온 키워드 분석
        print(f"\n🏷️ 제목에서 자주 나오는 키워드 (Top 10):")
        all_titles = ' '.join(df['title'].astype(str))
        
        # 간단한 키워드 추출 (공백 기준)
        words = all_titles.split()
        # 한글만 포함된 단어 필터링 (길이 2 이상)
        korean_words = [word for word in words if len(word) >= 2 and any('가' <= c <= '힣' for c in word)]
        
        word_freq = Counter(korean_words)
        for word, freq in word_freq.most_common(10):
            print(f"  {word}: {freq}회")
        
        # 4. 언론사별 기사 수 (상위 10개)
        print(f"\n📰 언론사별 기사 수 (Top 10):")
        press_counts = df['press'].value_counts().head(10)
        for press, count in press_counts.items():
            print(f"  {press}: {count}개")
        
    except Exception as e:
        print(f"❌ 분석 중 오류 발생: {e}")

# 사용 예제
# analyze_news_data('news_data_20250120_143022.csv')

⚠️ 문제 해결 가이드

자주 발생하는 오류와 해결책

1. HTTP 403 Forbidden 오류

# 해결책: User-Agent 헤더 추가
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
response = requests.get(url, headers=headers)

2. 연결 시간 초과

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

3. 인코딩 문제

# 해결책: 인코딩 명시적 설정
response.encoding = 'utf-8'
soup = BeautifulSoup(response.text, 'html.parser')

4. 선택자를 찾을 수 없음

# 해결책: 요소 존재 확인
title_elem = soup.find('h2', class_='title')
if title_elem:
    title = title_elem.get_text()
else:
    print("제목을 찾을 수 없습니다.")

🎯 실습 과제

오늘 배운 내용을 바탕으로 다음 과제를 해보세요:

  1. 기본 과제: 다른 카테고리 (생활문화, 세계) 뉴스도 수집해보기
  2. 중급 과제: 수집한 뉴스의 본문 내용도 추출해보기
  3. 고급 과제: 다른 뉴스 사이트 (다음 뉴스 등)에서도 헤드라인 수집해보기
💡 과제 힌트:
본문 내용을 추출할 때는 각 뉴스 기사의 개별 페이지에 접속해야 합니다. 하지만 요청 횟수가 많아지므로 반드시 적절한 지연시간(time.sleep())을 두세요.

🔮 다음 시간 예고: API 활용 데이터 수집

2탄에서는 웹 스크래핑의 한계를 극복하고 더 안정적으로 데이터를 수집할 수 있는 API 활용 방법을 배워보겠습니다:

  • 🌐 REST API 기초 이해
  • 🔑 API 키 관리와 인증
  • 📱 소셜미디어 API 활용 (Twitter, Instagram)
  • 📊 공공데이터 API 활용
  • ⚡ 비동기 처리로 수집 속도 향상

📚 추가 학습 자료

추천 도서:

  • 「파이썬으로 웹 크롤러 만들기」 - 라이언 미첼
  • 「실전 파이썬 웹 스크래핑」 - 정재희

유용한 도구들:

  • requests-html: JavaScript 렌더링 지원
  • Scrapy: 대규모 스크래핑 프레임워크
  • Selenium: 브라우저 자동화

연습할 수 있는 사이트:

💬 마무리

오늘은 파이썬을 이용한 웹 스크래핑의 기초부터 실전 뉴스 수집기까지 만들어봤습니다. 처음에는 복잡해 보일 수 있지만, 차근차근 따라 하시면 누구나 할 수 있습니다.

핵심 요약:

  1. 웹 스크래핑은 HTTP 요청 → HTML 파싱 → 데이터 추출 → 저장 순서
  2. BeautifulSoup을 사용하면 HTML 파싱이 쉬워짐
  3. 윤리적 스크래핑을 위해 적절한 지연시간과 User-Agent 설정 필수
  4. 수집한 데이터는 CSV, 데이터베이스 등 다양한 형태로 저장 가능

다음 시간에는 더 안정적이고 효율적인 API 기반 데이터 수집 방법을 배워보겠습니다. 궁금한 점이 있으시면 댓글로 언제든 질문해주세요! 🚀


Featured image by Christina Morillo on Unsplash