PythonPython 기초 · 10입문

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

Python프로젝트OpenAIAPI실전

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

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

flowchart LR
    URL["뉴스 URL 입력"] --> FETCH["기사 내용 수집\n(HTTP 요청)"]
    FETCH --> CLEAN["텍스트 정리\n(파싱)"]
    CLEAN --> SUMMARY["AI 요약\n(OpenAI API)"]
    SUMMARY --> SAVE["결과 저장\n(JSON 파일)"]
    SAVE --> PRINT["화면 출력"]

사용하는 개념

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

프로젝트 구조

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

summarizer.py: 핵심 모듈

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 진입점

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()

실행

# 가상환경 활성화
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

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

flowchart LR
    E1["편 1\n변수·자료형"] --> E2["편 2\n조건·반복"]
    E2 --> E3["편 3\n함수·모듈"]
    E3 --> E4["편 4\n리스트·딕셔너리"]
    E4 --> E5["편 5\n파일 I/O"]
    E5 --> E6["편 6\n예외 처리"]
    E6 --> E7["편 7\n클래스"]
    E7 --> E8["편 8\n패키지·venv"]
    E8 --> E9["편 9\nHTTP 요청"]
    E9 --> E10["편 10\n프로젝트"]
핵심 개념
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 개발 시리즈로 넘어갈 준비가 됐습니다!

궁금한 점이 있으신가요?

협업·의뢰는 아래로, 가벼운 소통은 인스타그램 @bluefox._.hi도 환영이에요.