최종 프로젝트: 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프로젝트"]
| 편 | 핵심 개념 |
|---|---|
| 1 | int, str, float, bool, None |
| 2 | if/elif/else, for, while, break/continue |
| 3 | def, return, *args, **kwargs, import |
| 4 | list, dict, tuple, set, 컴프리헨션 |
| 5 | open(), json.dump/load, pathlib |
| 6 | try/except, raise, logging |
| 7 | class, init, 상속, Pydantic |
| 8 | pip, venv, .env, requirements.txt |
| 9 | requests.get/post, 헤더, 에러 처리 |
| 10 | 전체 통합 프로젝트 |
이것으로 Python 기초 시리즈를 마칩니다. 이제 LLM API 개발 시리즈로 넘어갈 준비가 됐습니다!