포스트

[Python 100일 챌린지] Day 60 - 미니 프로젝트: 뉴스 스크래퍼

[Python 100일 챌린지] Day 60 - 미니 프로젝트: 뉴스 스크래퍼

Phase 6 완성! 🎉 scraper.scrape_all()analyzer.analyze()storage.save() → 뉴스 자동 수집 시스템 완성!** 😊

9일간 배운 모든 웹 기술로 실전 뉴스 스크래퍼 제작! requests + BeautifulSoup + 크롤링 + 데이터 분석까지!

(50-60분 완독 ⭐⭐⭐⭐)

🎯 오늘의 학습 목표

📚 사전 지식


🎯 오늘의 학습 목표 1: Phase 6 내용 복습하기

Phase 6에서 배운 내용

Day 51-54: 웹 스크래핑 기초

  • requests 라이브러리
  • HTTP 메서드 (GET, POST, PUT, DELETE)
  • BeautifulSoup HTML 파싱
  • 동적 콘텐츠 스크래핑

Day 55-57: API 설계와 인증

  • API 설계 원칙
  • RESTful API 구현
  • API 인증 방식 (API Key, OAuth, JWT)
  • API 문서화

Day 58-59: 고급 크롤링

  • 웹 크롤러 구현
  • URL 관리와 방문 기록
  • Selenium 브라우저 자동화
  • 대기 전략과 예외 처리

오늘 만들 프로젝트

뉴스 스크래퍼 - Phase 6의 모든 웹 스크래핑 기술을 활용합니다!


🎯 오늘의 학습 목표 2: 뉴스 스크래퍼 시스템 설계하기

프로젝트 목표

뉴스 수집 및 분석 시스템:

  • 여러 뉴스 사이트 스크래핑
  • 데이터 정제 및 저장
  • 키워드 분석
  • JSON/CSV 출력
  • 일정 자동화

📁 프로젝트 구조

💡 왜 파일을 나눌까요?

하나의 큰 파일에 모든 코드를 넣으면 찾기도 어렵고 수정도 힘들어요. 기능별로 파일을 나누면 “스크래핑 문제? scraper.py 확인!” 이렇게 바로 찾을 수 있어요!

1
2
3
4
5
news_scraper/
├── scraper.py          # 스크래퍼 - 웹에서 뉴스 가져오기
├── analyzer.py         # 분석 - 키워드 추출, 통계
├── storage.py          # 저장 - JSON, CSV 파일로 저장
└── main.py             # 메인 - 전체 프로그램 실행

🛠️ 사전 준비

⚠️ 패키지 설치 확인: Day 53에서 설치했다면 OK! 없다면 먼저 설치하세요.

1
pip install requests beautifulsoup4

폴더 생성:

1
2
mkdir news_scraper
cd news_scraper

🎯 오늘의 학습 목표 3: 웹 스크래핑과 API를 활용하여 구현하기

1. 뉴스 스크래퍼 (scraper.py)

💡 이 파일의 역할: 웹사이트에 접속해서 뉴스 기사 목록을 가져오는 “수집가” 역할이에요!

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# scraper.py
import requests
from bs4 import BeautifulSoup
from datetime import datetime

class NewsScraper:
    """
    뉴스 스크래퍼 클래스

    💡 클래스를 사용하는 이유:
    - 여러 뉴스 소스를 하나의 객체로 관리
    - 설정(sources)을 한 번만 전달하면 계속 사용 가능
    """

    def __init__(self, sources):
        """
        sources: 뉴스 소스 정보가 담긴 리스트
        예: [{'name': '뉴스A', 'url': '...', 'article_selector': '...'}]
        """
        self.sources = sources

    def scrape_all(self):
        """모든 뉴스 소스에서 기사를 수집"""
        all_articles = []
        for source in self.sources:
            articles = self.scrape_source(source)
            all_articles.extend(articles)  # 리스트에 리스트를 합침
        return all_articles

    def scrape_source(self, source):
        """
        하나의 뉴스 소스에서 기사 수집

        💡 try-except로 감싸는 이유:
        네트워크 오류, 사이트 변경 등 언제든 실패할 수 있으니까요!
        """
        try:
            # 1. 웹페이지 가져오기 (timeout=10: 10초 안에 응답 없으면 포기)
            response = requests.get(source['url'], timeout=10)
            soup = BeautifulSoup(response.text, 'html.parser')

            # 2. 기사 요소들 찾기 (CSS 선택자 사용)
            articles = []
            elements = soup.select(source['article_selector'])

            # 3. 각 요소에서 정보 추출
            for element in elements:
                article = self._parse_article(element, source)
                if article:  # None이 아닌 경우만 추가
                    articles.append(article)

            return articles
        except Exception as e:
            print(f"{source['name']} 실패: {e}")
            return []  # 실패해도 빈 리스트 반환 (프로그램 중단 방지)

    def _parse_article(self, element, source):
        """
        개별 기사 요소에서 제목, 링크 추출

        💡 메서드 이름 앞의 _ (언더스코어):
        "이 메서드는 클래스 내부에서만 쓰는 거예요"라는 관례적 표시
        """
        try:
            title = element.select_one(source['title_selector'])
            link = element.select_one(source['link_selector'])

            if title and link:
                return {
                    'source': source['name'],           # 뉴스 출처
                    'title': title.get_text(strip=True),  # 제목 (공백 제거)
                    'url': link.get('href'),            # 링크 URL
                    'scraped_at': datetime.now().isoformat()  # 수집 시간
                }
        except:
            return None  # 파싱 실패 시 None 반환

2. 데이터 분석기 (analyzer.py)

💡 이 파일의 역할: 수집한 뉴스를 분석해서 “어떤 키워드가 많이 나왔는지” 등을 알려주는 “분석가” 역할이에요!

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
# analyzer.py
from collections import Counter  # 단어 개수 세기용
import re  # 정규표현식 (텍스트에서 단어 추출)

class NewsAnalyzer:
    """
    뉴스 분석 클래스

    💡 분석 기능:
    - 총 기사 수, 출처별 기사 수 통계
    - 제목에서 자주 등장하는 키워드 추출
    """

    def __init__(self, articles):
        """articles: 스크래퍼에서 수집한 기사 리스트"""
        self.articles = articles

    def get_statistics(self):
        """기본 통계 반환"""
        return {
            'total': len(self.articles),  # 총 기사 수
            # Counter: 각 출처별로 기사가 몇 개인지 세기
            # 예: Counter({'뉴스A': 10, '뉴스B': 5})
            'sources': Counter(a['source'] for a in self.articles)
        }

    def extract_keywords(self, top_n=10):
        """
        제목에서 자주 나오는 키워드 추출

        💡 동작 원리:
        1. 모든 제목을 하나의 문자열로 합침
        2. 정규표현식으로 단어만 추출
        3. Counter로 각 단어 개수 세기
        4. 가장 많이 나온 top_n개 반환
        """
        # 모든 제목을 공백으로 연결
        all_text = ' '.join(a['title'] for a in self.articles)
        # \b\w+\b: 단어 경계 사이의 문자들 (단어만 추출)
        words = re.findall(r'\b\w+\b', all_text.lower())
        # most_common(10): 가장 많이 나온 10개
        return Counter(words).most_common(top_n)

3. 저장소 (storage.py)

💡 이 파일의 역할: 수집한 데이터를 파일로 저장하는 “저장소” 역할이에요! JSON과 CSV 두 가지 형식을 지원해요.

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
# storage.py
import json  # JSON 파일 처리
import csv   # CSV 파일 처리

class NewsStorage:
    """
    뉴스 저장 클래스

    💡 왜 두 가지 형식으로 저장할까요?
    - JSON: 프로그램에서 다시 읽어서 처리하기 좋음
    - CSV: 엑셀에서 열어서 보기 좋음
    """

    def save_json(self, articles, filename='news.json'):
        """JSON 형식으로 저장"""
        with open(filename, 'w', encoding='utf-8') as f:
            # ensure_ascii=False: 한글이 깨지지 않게
            # indent=2: 보기 좋게 들여쓰기
            json.dump(articles, f, ensure_ascii=False, indent=2)
        print(f"✅ JSON 저장: {filename}")

    def save_csv(self, articles, filename='news.csv'):
        """CSV 형식으로 저장 (엑셀에서 열기 좋음)"""
        if not articles:
            print("⚠️ 저장할 기사가 없습니다")
            return

        with open(filename, 'w', newline='', encoding='utf-8') as f:
            # DictWriter: 딕셔너리를 CSV로 쉽게 저장
            writer = csv.DictWriter(f, fieldnames=articles[0].keys())
            writer.writeheader()   # 첫 줄에 컬럼명 작성
            writer.writerows(articles)  # 모든 기사 작성
        print(f"✅ CSV 저장: {filename}")

4. 메인 프로그램 (main.py)

💡 이 파일의 역할: 모든 모듈을 연결해서 실행하는 “지휘자” 역할이에요!

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
46
47
48
49
50
51
52
53
# main.py
# 💡 다른 파일에서 클래스를 가져옵니다 (같은 폴더에 있어야 해요!)
from scraper import NewsScraper
from analyzer import NewsAnalyzer
from storage import NewsStorage

# 뉴스 소스 설정
# 💡 실제 사이트 URL과 CSS 선택자로 바꿔서 사용하세요!
NEWS_SOURCES = [
    {
        'name': 'Example News',           # 뉴스 소스 이름
        'url': 'https://news.example.com', # 뉴스 목록 페이지 URL
        'article_selector': '.article',    # 기사 요소의 CSS 선택자
        'title_selector': '.title',        # 제목의 CSS 선택자
        'link_selector': 'a'               # 링크의 CSS 선택자
    }
    # 더 많은 소스 추가 가능:
    # {'name': '또다른 뉴스', 'url': '...', ...}
]

def main():
    """메인 실행 함수"""
    print("📰 뉴스 스크래핑 시작...")

    # 1단계: 뉴스 수집
    scraper = NewsScraper(NEWS_SOURCES)
    articles = scraper.scrape_all()
    print(f"{len(articles)}개 수집")

    # 2단계: 분석 및 저장 (기사가 있을 때만)
    if articles:
        # 분석
        analyzer = NewsAnalyzer(articles)
        stats = analyzer.get_statistics()
        keywords = analyzer.extract_keywords()

        # 결과 출력
        print(f"\n📊 통계: {stats['total']}")
        print(f"🔑 키워드: {keywords[:5]}")

        # 저장
        storage = NewsStorage()
        storage.save_json(articles)
        storage.save_csv(articles)

        print("\n🎉 완료! news.json, news.csv 파일을 확인하세요!")
    else:
        print("⚠️ 수집된 기사가 없습니다. URL과 선택자를 확인해주세요.")

# 💡 이 파일을 직접 실행할 때만 main() 호출
# 다른 파일에서 import할 때는 실행되지 않음
if __name__ == '__main__':
    main()

🏃 실행 방법

1
2
# news_scraper 폴더에서 실행
python main.py

예상 출력:

1
2
3
4
5
6
7
8
9
📰 뉴스 스크래핑 시작...
✅ 15개 수집

📊 통계: 15개
🔑 키워드: [('the', 8), ('news', 5), ('today', 4), ...]
✅ JSON 저장: news.json
✅ CSV 저장: news.csv

🎉 완료! news.json, news.csv 파일을 확인하세요!

🧪 도전 과제

프로젝트를 더 발전시켜보세요!

과제 1: 실제 뉴스 사이트 적용

요구사항:

  1. 실제 뉴스 사이트의 CSS 선택자 찾기
  2. NEWS_SOURCES에 추가
  3. 기사 수집 테스트
💡 힌트

CSS 선택자 찾는 방법:

  1. 브라우저에서 뉴스 사이트 열기
  2. F12 (개발자 도구) 열기
  3. 기사 제목에서 마우스 우클릭 → “검사”
  4. HTML 구조를 보고 클래스명, ID 확인

예시 (가상의 구조):

1
2
3
4
5
6
7
{
    'name': 'Tech News',
    'url': 'https://technews.example.com',
    'article_selector': 'article.news-item',
    'title_selector': 'h2.headline',
    'link_selector': 'a.read-more'
}

과제 2: 기능 확장

아이디어:

  • 중복 기사 제거 기능
  • 날짜 필터링 (오늘 기사만)
  • 특정 키워드 포함 기사만 수집
  • Selenium으로 동적 페이지 지원
💡 중복 제거 힌트
1
2
3
4
5
6
7
8
9
def remove_duplicates(articles):
    """제목 기준 중복 제거"""
    seen = set()
    unique = []
    for article in articles:
        if article['title'] not in seen:
            seen.add(article['title'])
            unique.append(article)
    return unique

🎯 학습 목표 4: 프로젝트 완성하고 Phase 6 마무리

축하합니다! 🎉 Phase 6을 완료했어요!

📝 Phase 6에서 배운 핵심 개념

Day 주제 핵심 코드/개념
51 requests 기초 requests.get(url), response.json()
52 HTTP 메서드 GET, POST, PUT, DELETE, 헤더
53 BeautifulSoup soup.find(), soup.select(), CSS 선택자
54 스크래핑 고급 동적 콘텐츠, 에러 처리, robots.txt
55 API 설계 REST 원칙, 엔드포인트 설계
56 RESTful API CRUD 구현, 상태 코드
57 API 인증 API Key, OAuth, JWT
58 웹 크롤러 BFS/DFS, URL 큐, 중복 방지
59 Selenium WebDriver, 동적 페이지, 대기 전략
60 미니 프로젝트 뉴스 스크래퍼 시스템

🎯 Phase 6 성취도 체크

  • HTTP 요청/응답을 이해한다
  • BeautifulSoup으로 HTML을 파싱할 수 있다
  • CSS 선택자로 원하는 요소를 찾을 수 있다
  • REST API의 기본 원칙을 안다
  • 간단한 크롤러를 만들 수 있다
  • Selenium으로 동적 페이지를 다룰 수 있다

📚 이전 학습

Day 59: Selenium 기초 ⭐⭐⭐ 어제는 Selenium WebDriver, 브라우저 자동화, 대기 전략을 배웠습니다!

📚 다음 학습

Day 61: NumPy 기초 ⭐⭐ 내일부터 Phase 7에서 NumPy, Pandas, Matplotlib 등 데이터 분석 기초를 배웁니다!


“늦었다고 생각할 때가 가장 빠른 때입니다. 오늘도 한 걸음 성장했어요!” 🚀

Day 60/100 Phase 6: 웹 스크래핑과 API #100DaysOfPython
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.