PythonPython 기초강의 10개 중 10번째난이도 입문

최종 프로젝트 — AI 기반 뉴스 요약 도구 만들기

발행 읽는 시간 약 2 분

  • Python
  • 프로젝트
  • OpenAI
  • API
  • 실전

강의 소개

변수, 함수, 클래스, 파일 I/O, 예외 처리, HTTP 요청, OpenAI API까지 Python 기초 시리즈에서 배운 모든 개념을 활용해 실용적인 뉴스 요약 도구를 만듭니다.

최종 프로젝트: AI 뉴스 요약 도구#

뉴스 URL을 입력하면 기사를 가져와 AI가 한국어로 요약해주는 CLI 도구를 만듭니다.

Diagram · Mermaid

사용하는 개념#

편개념사용 위치
1편변수·자료형기사 데이터 저장
2편조건문·반복문여러 URL 처리
3편함수·모듈기능별 함수 분리
4편리스트·딕셔너리기사 목록·메타데이터
5편파일 I/O결과 JSON 저장
6편예외 처리네트워크·API 오류
7편클래스NewsSummarizer 클래스
8편패키지·venv의존성 관리
9편HTTP 요청기사 내용 수집

프로젝트 구조#

Text
news_summarizer/
├── venv/
├── .env
├── .gitignore
├── requirements.txt
├── main.py           # 진입점 (CLI)
├── summarizer.py     # 핵심 로직
└── output/           # 저장된 요약본
Text
# requirements.txt
openai>=1.10.0
requests>=2.31.0
beautifulsoup4>=4.12.0
python-dotenv>=1.0.1

summarizer.py: 핵심 모듈#

Python
import requests
import json
import os
from datetime import datetime
from pathlib import Path
from openai import OpenAI
from bs4 import BeautifulSoup

class ArticleFetcher:
    """웹 기사 내용을 가져옵니다."""
    
    HEADERS = {
        "User-Agent": "Mozilla/5.0 (compatible; NewsSummarizer/1.0)"
    }
    
    def fetch(self, url: str) -> dict:
        """URL에서 기사 제목과 본문을 추출합니다."""
        try:
            response = requests.get(url, headers=self.HEADERS, timeout=10)
            response.raise_for_status()
            
            soup = BeautifulSoup(response.text, "html.parser")
            
            # 제목 추출
            title = (
                soup.find("h1") or 
                soup.find("meta", property="og:title")
            )
            title_text = title.get_text().strip() if title else "제목 없음"
            
            # 본문 추출 (p 태그 모음)
            paragraphs = soup.find_all("p")
            body = " ".join([
                p.get_text().strip() 
                for p in paragraphs 
                if len(p.get_text().strip()) > 50
            ])
            
            return {
                "url": url,
                "title": title_text,
                "body": body[:3000],   # 최대 3000자
                "fetched_at": datetime.now().isoformat()
            }
        
        except requests.RequestException as e:
            raise ConnectionError(f"기사 수집 실패: {e}")


class NewsSummarizer:
    """AI로 뉴스를 요약합니다."""
    
    def __init__(self):
        self.client = OpenAI()
        self.fetcher = ArticleFetcher()
        self.output_dir = Path("output")
        self.output_dir.mkdir(exist_ok=True)
    
    def summarize(self, url: str, lang: str = "ko") -> dict:
        """URL을 받아 요약 결과를 반환합니다."""
        
        # 1. 기사 수집
        print(f"📰 기사 수집 중...")
        article = self.fetcher.fetch(url)
        
        if len(article["body"]) < 100:
            raise ValueError("기사 내용이 너무 짧습니다.")
        
        # 2. AI 요약
        print(f"🤖 AI 요약 중...")
        response = self.client.chat.completions.create(
            model="gpt-4o-mini",
            temperature=0.3,
            messages=[
                {
                    "role": "system",
                    "content": f"""뉴스 기사를 {lang}로 요약하는 전문가입니다.
다음 형식으로 요약하세요:
- **핵심 요약**: 2~3문장으로 핵심 내용
- **주요 사실**: 중요한 사실 3가지 (불릿 포인트)
- **의의**: 이 뉴스의 중요성 1문장"""
                },
                {
                    "role": "user",
                    "content": f"제목: {article['title']}\n\n본문:\n{article['body']}"
                }
            ]
        )
        
        summary = response.choices[0].message.content
        tokens_used = response.usage.total_tokens
        
        # 3. 결과 구성
        result = {
            **article,
            "summary": summary,
            "lang": lang,
            "tokens_used": tokens_used,
            "summarized_at": datetime.now().isoformat()
        }
        
        # 4. 파일 저장
        self._save(result)
        
        return result
    
    def _save(self, result: dict):
        """결과를 JSON 파일로 저장합니다."""
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        filename = self.output_dir / f"summary_{timestamp}.json"
        
        with open(filename, "w", encoding="utf-8") as f:
            json.dump(result, f, ensure_ascii=False, indent=2)
        
        print(f"💾 저장됨: {filename}")
    
    def batch_summarize(self, urls: list[str]) -> list[dict]:
        """여러 URL을 순서대로 요약합니다."""
        results = []
        
        for i, url in enumerate(urls, 1):
            print(f"\n[{i}/{len(urls)}] {url}")
            
            try:
                result = self.summarize(url)
                results.append(result)
                print(f"✅ 완료")
            except Exception as e:
                print(f"❌ 실패: {e}")
                results.append({"url": url, "error": str(e)})
        
        return results

main.py: CLI 진입점#

Python
import sys
import os
from dotenv import load_dotenv
from summarizer import NewsSummarizer

load_dotenv()

def print_result(result: dict):
    """결과를 보기 좋게 출력합니다."""
    if "error" in result:
        print(f"❌ 오류: {result['error']}")
        return
    
    print(f"\n{'='*60}")
    print(f"📌 {result['title']}")
    print(f"🔗 {result['url']}")
    print(f"{'='*60}")
    print(result['summary'])
    print(f"\n💡 사용된 토큰: {result['tokens_used']}")

def main():
    if not os.getenv("OPENAI_API_KEY"):
        print("❌ OPENAI_API_KEY가 설정되지 않았습니다.")
        print("   .env 파일에 OPENAI_API_KEY=sk-... 를 추가하세요.")
        sys.exit(1)
    
    summarizer = NewsSummarizer()
    
    # 커맨드라인 인자로 URL 전달
    if len(sys.argv) > 1:
        urls = sys.argv[1:]
        
        if len(urls) == 1:
            result = summarizer.summarize(urls[0])
            print_result(result)
        else:
            print(f"\n📋 {len(urls)}개 기사 배치 요약 시작\n")
            results = summarizer.batch_summarize(urls)
            for r in results:
                print_result(r)
    
    # 대화형 모드
    else:
        print("🗞️  AI 뉴스 요약 도구")
        print("URL을 입력하거나 'quit'으로 종료하세요.\n")
        
        while True:
            url = input("URL: ").strip()
            
            if url.lower() in ("quit", "exit", "q"):
                print("종료합니다.")
                break
            
            if not url.startswith("http"):
                print("유효한 URL을 입력하세요.")
                continue
            
            try:
                result = summarizer.summarize(url)
                print_result(result)
            except Exception as e:
                print(f"❌ 오류: {e}")

if __name__ == "__main__":
    main()

실행#

Bash
# 가상환경 활성화
source venv/bin/activate

# 단일 URL
python main.py https://www.bbc.com/news/article-123

# 여러 URL
python main.py https://url1.com https://url2.com https://url3.com

# 대화형 모드
python main.py

파이썬 기초 시리즈 전체 복습#

Diagram · Mermaid
편핵심 개념
1int, str, float, bool, None
2if/elif/else, for, while, break/continue
3def, return, *args, **kwargs, import
4list, dict, tuple, set, 컴프리헨션
5open(), json.dump/load, pathlib
6try/except, raise, logging
7class, init, 상속, Pydantic
8pip, venv, .env, requirements.txt
9requests.get/post, 헤더, 에러 처리
10전체 통합 프로젝트

이것으로 Python 기초 시리즈를 마칩니다. 이제 LLM API 개발 시리즈로 넘어갈 준비가 됐습니다!

Follow · Ask · Work

이 글이 도움이 됐다면

짧은 작업 기록과 데모는 인스타그램에 올립니다. 만들고 싶은 것이 있다면 프로젝트 의뢰나 협업 제안도 편하게 보내 주세요.

글쓴이

파란여우

직접 만든 AI 앱·제품의 개발·배포를 공개하는 실전형 테크 크리에이터

파란여우 소개 Instagram @bluefox._.hi

이어서 보기

만들고 싶은 것이 있다면 프로젝트 의뢰로 알려 주세요.