HSEOM GeckoHSEOM
Instagram

흑섬 TECH 블로그 - 데이터 기반 브리딩 기술

레오파드게코 브리딩에 데이터 분석과 AI 기술을 접목합니다. Python, NumPy를 활용한 체중 관리, 성장 추이 분석, 환경 데이터 시각화 등 실무에서 직접 사용하는 기술을 일반인도 이해하기 쉽게 설명합니다.

주요 카테고리

Play 카테고리

흑섬 TECH 블로그의 Play 카테고리입니다.

9개의 글이 있습니다.

[NLP 프로젝트 2편] 뉴스 수집부터 요약까지 — 자동 파이프라인 만들기

1편에서 만든 BERT 분류기를 실제 뉴스에 적용해봤습니다. NewsAPI로 최신 기사를 수집하고, 전처리로 텍스트를 정리하고, BERT로 카테고리를 분류하고, KoBART로 요약까지 — 수집에서 요약까지 한 번에 돌아가는 end-to-end 파이프라인을 Colab에서 직접 만들어봤습니다.

카테고리: Play

작성일: 2026-05-31

예상 읽기 시간: 30

Back to Tech
Play·30min read·

[NLP 프로젝트 2편] 뉴스 수집부터 요약까지 — 자동 파이프라인 만들기

1편에서 만든 BERT 분류기를 실제 뉴스에 적용해봤습니다. NewsAPI로 최신 기사를 수집하고, 전처리로 텍스트를 정리하고, BERT로 카테고리를 분류하고, KoBART로 요약까지 — 수집에서 요약까지 한 번에 돌아가는 end-to-end 파이프라인을 Colab에서 직접 만들어봤습니다.

시작하며 — 분류기는 만들었는데, 뉴스는 어디서?

1편에서 BERT fine-tuning으로 한국어 뉴스 분류기를 만들었습니다.
KLUE-YNAT 기준 87% 정확도. TF-IDF 대비 +19%p.

근데 학습 데이터로만 테스트하면 진짜 성능인지 알 수가 없잖아요.
실제 뉴스를 가져와서 넣어봐야 의미가 있습니다. API 키 하나면 된다길래 바로 해봤어요. ㅎㅎ

이번 글에서는 뉴스 수집 → 전처리 → 분류 → 요약까지 end-to-end 파이프라인을 만들어봅니다.
수집은 NewsAPI, 분류는 1편 BERT 모델, 요약은 KoBART를 사용합니다.
Colab에서 전부 돌아가는 코드라서, 따라 하면서 직접 확인할 수 있습니다.

Colab 환경 설정: 셀 0에서 한국어 폰트(NanumGothic)를 설치합니다.
!apt-get install -y fonts-nanum — 이걸 안 하면 셀 7 차트에서 한글이 깨집니다.

Colab 실습 — 패키지 설치 + 한국어 폰트 (셀 0)
!pip install transformers torch feedparser pandas matplotlib -q

# 한국어 폰트 설치 (차트에서 한글 깨짐 방지)
!apt-get install -y fonts-nanum > /dev/null 2>&1
import matplotlib
matplotlib.font_manager._rebuild()
matplotlib.rcParams['font.family'] = 'NanumGothic'
matplotlib.rcParams['axes.unicode_minus'] = False

transformers, feedparser 등 필수 패키지를 설치하고, NanumGothic 폰트를 설치합니다.

셀0 실행 결과 — 한국어 폰트 설치


뉴스를 코드로 수집하려면 어떤 방법이 있나요?

뉴스를 코드로 수집하는 방법은 크게 세 가지입니다. NewsAPI는 REST API로 구조화된 JSON을 받아오고, RSS 피드는 XML을 파싱해서 제목과 링크를 가져오고, 웹 스크래핑은 HTML을 직접 파싱해서 본문까지 수집합니다. 각각 장단점이 다르기 때문에 목적에 따라 골라 쓰면 됩니다.

뉴스를 코드로 가져오는 방법은 크게 세 가지입니다.

NewsAPI는 REST API로 뉴스를 JSON 형태로 받아옵니다.
키워드, 카테고리, 국가별로 검색할 수 있고, 제목/본문/출처가 구조화되어 나옵니다.
무료 플랜은 하루 100건까지 가능합니다.

RSS 피드는 뉴스 사이트가 제공하는 XML 피드를 파싱해서 가져옵니다.
API 키가 필요 없어서 간편하지만, 대부분 제목과 링크만 제공됩니다.

웹 스크래핑은 requests + BeautifulSoup으로 HTML을 직접 파싱합니다.
기사 본문까지 수집할 수 있지만, 사이트 구조가 바뀌면 코드도 바꿔야 하고 robots.txt를 반드시 확인해야 합니다.
(API는 정리된 택배 박스, RSS는 신문 배달, 스크래핑은 직접 가서 가져오는 거예요.)

뉴스 수집 방법 3가지 비교 — NewsAPI, RSS, 웹 스크래핑

이번 프로젝트에서는 NewsAPI를 메인으로, RSS를 보조로 같이 구현해봤습니다.



NewsAPI로 한국 뉴스는 어떻게 가져오나요?

NewsAPI는 newsapi.org에서 무료 API 키를 발급받으면 바로 사용할 수 있습니다.
top-headlines 엔드포인트에 country=kr만 넣으면 한국 뉴스가 나옵니다.

응답 JSON에서 title, description, content, source를 꺼내서 pandas DataFrame으로 정리하면 끝입니다.
코드가 짧아서 수집 자체는 금방 끝나요. ㅋㅋ

주의: 아래 코드의 YOUR_API_KEY_HERE에 본인의 NewsAPI 키를 넣어야 동작합니다.
newsapi.org/register에서 무료 가입 후 발급받을 수 있습니다.

Colab 실습 — NewsAPI 수집 코드 (셀 1)
# NewsAPI로 한국 뉴스 수집
import requests, pandas as pd

NEWS_API_KEY = 'YOUR_API_KEY_HERE'  # ← 본인 키 입력
url = 'https://newsapi.org/v2/everything'
params = {'q': '한국', 'language': 'ko', 'pageSize': 20,
          'sortBy': 'publishedAt', 'apiKey': NEWS_API_KEY}

response = requests.get(url, params=params)
data = response.json()

articles = []
for a in data.get('articles', []):
    articles.append({
        'title': a.get('title', ''),
        'description': a.get('description', ''),
        'content': a.get('content', ''),
        'source': a['source'].get('name', '')
    })

df_news = pd.DataFrame(articles)
print(f'수집 결과: {len(df_news)}건')
df_news[['title', 'source']].head(10)

무료 플랜에서는 everything 엔드포인트가 안정적입니다. description 필드가 content보다 깨끗해서 요약에 유리합니다.

셀1 실행 결과 — NewsAPI 수집 20건

대안으로 Google News RSS도 같이 구현해봤습니다.
feedparser 라이브러리로 XML을 파싱하면 카테고리별 뉴스 피드를 가져올 수 있어요.
API 키 만료됐을 때 백업으로 쓸 수 있어서, 같이 만들어두면 좋습니다.

Colab 실습 — Google News RSS 수집 (셀 2, 대안)
# Google News RSS 수집 (API 키 불필요)
import feedparser

feed = feedparser.parse(
    'https://news.google.com/rss?hl=ko&gl=KR&ceid=KR:ko'
)

rss_articles = []
for entry in feed.entries[:15]:
    rss_articles.append({
        'title': entry.get('title', ''),
        'link': entry.get('link', ''),
        'published': entry.get('published', ''),
    })

df_rss = pd.DataFrame(rss_articles)
print(f'RSS 수집: {len(df_rss)}건')
df_rss.head(10)

API 키 없이 Google News 전체 피드를 가져옵니다. Colab 환경에서는 카테고리별 토픽 URL이 안 될 수 있어서 기본 RSS URL을 사용합니다.

셀2 실행 결과 — Google News RSS 수집


수집한 뉴스 텍스트는 어떻게 정리하나요?

수집된 뉴스를 바로 모델에 넣으면 안 돼요.
HTML 태그 잔재, 특수문자, [속보]나 [포토] 같은 prefix가 섞여 있거든요.
NewsAPI content 끝에 [+1234 chars] 같은 게 붙어 있기도 하고요.
(기사 원문을 복사하면 광고 문구나 [포토] 같은 게 같이 딸려오잖아요. 그런 잡음을 지우는 작업이에요.)

re.sub()으로 패턴별로 하나씩 제거하고, 필요하면 konlpy Okt 형태소 분석기로 불용어까지 제거합니다.
전처리 전후 결과를 비교해보면 차이가 바로 눈에 띕니다.

텍스트 전처리 파이프라인 — HTML 태그, prefix, 특수문자 제거 흐름
Colab 실습 — 전처리 함수 + 전후 비교 (셀 3)
import re

def clean_text(text):
    if not text or not isinstance(text, str):
        return ''
    text = re.sub(r'<[^>]+>', '', text)           # HTML 태그
    text = re.sub(r'\[\w+\]\s*', '', text)      # [속보], [포토] prefix
    text = re.sub(r'[^\w\s가-힣a-zA-Z0-9.,!?\-]', '', text)  # 특수문자
    text = re.sub(r'\s+', ' ', text).strip()       # 연속 공백
    text = re.sub(r'\[\+\d+ chars\]$', '', text).strip()  # [+N chars]
    return text

# DataFrame에 전처리 컬럼 추가
df_news['title_clean'] = df_news['title'].apply(clean_text)
df_news['desc_clean'] = df_news['description'].apply(clean_text)
df_news['content_clean'] = df_news['content'].apply(clean_text)

# 전후 비교
for i in range(min(3, len(df_news))):
    print(f'원본: {df_news.iloc[i]["title"]}')
    print(f'정제: {df_news.iloc[i]["title_clean"]}')
    print()

re.sub()으로 5단계 정리를 거칩니다. title_clean, desc_clean, content_clean 3개 컬럼을 만들어서 이후 분류와 요약에 사용합니다.

셀3 실행 결과 — 전처리 전후 비교


1편에서 만든 BERT 모델을 어떻게 재활용하나요?

이제 1편에서 학습한 klue/bert-base fine-tuned 모델을 꺼내봅니다.
추가 학습 없이 바로 예측에 쓸 수 있어요.
(Fine-tuning이 끝난 가중치를 그대로 가져오는 거라서, 모델 학습 시간은 0초입니다. 로드만 하면 바로 예측이 시작돼요.)

수집한 뉴스 제목을 모델에 넣으면 7개 카테고리 중 하나와 신뢰도(softmax 확률)가 같이 나옵니다.
배치 분류 함수를 만들어서 20건을 한 번에 돌려보면, 대부분 90% 넘는 신뢰도가 나옵니다.

1편 BERT 모델로 실시간 뉴스 분류하는 흐름도
Colab 실습 — BERT 모델 로드 + 배치 분류 (셀 4)
import os

MODEL_NAME = 'klue/bert-base'
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
model = AutoModelForSequenceClassification.from_pretrained(MODEL_NAME, num_labels=7)

# 1편에서 저장한 모델이 있으면 로드 (없으면 base 모델로 데모)
SAVED_MODEL = './bert-news-output/checkpoint-best'
if os.path.exists(SAVED_MODEL):
    model = AutoModelForSequenceClassification.from_pretrained(SAVED_MODEL)
    print(f'fine-tuned 모델 로드: {SAVED_MODEL}')
else:
    print(f'fine-tuned 모델 없음 — base 모델({MODEL_NAME})로 데모 진행')

label_map = {0: 'IT/과학', 1: '경제', 2: '사회', 3: '생활문화',
             4: '세계', 5: '스포츠', 6: '정치'}

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model.to(device)
model.eval()

def classify_batch(titles):
    results = []
    for title in titles:
        enc = tokenizer(title, return_tensors='pt',
                        truncation=True, max_length=64, padding='max_length')
        enc = {k: v.to(device) for k, v in enc.items()}
        with torch.no_grad():
            logits = model(**enc).logits
        probs = torch.softmax(logits, dim=1)[0].cpu().numpy()
        pred = int(probs.argmax())
        results.append({'title': title, 'category': label_map[pred],
                        'confidence': f'{probs[pred]*100:.1f}%'})
    return pd.DataFrame(results)

df_classified = classify_batch(df_news['title_clean'].tolist())
df_classified.head(10)

1편에서 저장한 fine-tuned 모델이 있으면 자동으로 로드하고, 없으면 klue/bert-base로 데모를 진행합니다.

셀4 실행 결과 — BERT 배치 분류


KoBART로 기사 요약은 어떻게 하나요?

분류까지 했으니, 이번엔 기사 내용을 짧게 요약해볼 차례예요.
텍스트 요약에는 두 가지 방식이 있습니다.

Extractive 요약은 원문에서 중요한 문장을 그대로 뽑아오고,
Abstractive 요약은 원문을 읽은 뒤 새로운 문장으로 다시 써줍니다.
(Extractive는 교과서에 형광펜 치는 거고, Abstractive는 읽고 나서 자기 노트에 정리하는 거예요.)

이번 프로젝트에서는 KoBART (digit82/kobart-summarization)를 사용합니다.
Abstractive 방식이라 원문의 핵심을 자연스러운 문장으로 요약해줍니다.
BartForConditionalGeneration으로 로드하면 바로 사용할 수 있습니다.

Extractive vs Abstractive 요약 방식 비교

실제로 돌려보면 5000자짜리 기사가 2~3줄로 줄어듭니다. ㅋㅋ
다만 입력 길이 제한(1024 토큰)이 있어서, 긴 기사는 앞부분만 잘라서 넣어야 합니다.
이 부분은 나중에 피드백 섹션에서 다시 정리합니다.

Colab 실습 — KoBART 요약 (셀 5)
from transformers import PreTrainedTokenizerFast, BartForConditionalGeneration

sum_tokenizer = PreTrainedTokenizerFast.from_pretrained('digit82/kobart-summarization')
sum_model = BartForConditionalGeneration.from_pretrained('digit82/kobart-summarization')
sum_model.to(device)
sum_model.eval()

def summarize_text(text, max_length=128, min_length=20):
    if not text or len(text) < 30:
        return text
    text = text[:1000]
    inputs = sum_tokenizer(text, return_tensors='pt',
                           truncation=True, max_length=512).to(device)
    with torch.no_grad():
        output = sum_model.generate(**inputs, max_length=max_length,
                                    min_length=min_length, do_sample=False,
                                    repetition_penalty=1.5)
    return sum_tokenizer.decode(output[0], skip_special_tokens=True)

# 기사 3개 요약 테스트 (description 기반)
for i in range(min(3, len(df_news))):
    desc = df_news.iloc[i]['desc_clean']
    if desc and len(desc) > 30:
        summary = summarize_text(desc)
        print(f'[기사 {i+1}] {df_news.iloc[i]["title_clean"]}')
        print(f'  원문: {desc[:100]}')
        print(f'  요약: {summary}')
        print()

digit82/kobart-summarization 모델을 사용합니다. content보다 description이 깨끗해서 요약 품질이 더 좋습니다.

셀5 실행 결과 — KoBART 모델 로드 셀5 실행 결과 — 기사 요약 결과


수집부터 요약까지 파이프라인은 어떻게 합치나요?

여기까지 수집, 전처리, 분류, 요약을 하나씩 만들어봤습니다.
이걸 하나로 엮으면 한 번에 돌아가는 파이프라인이 됩니다.
앞에서 만든 함수들을 collect_news → clean_text → classify_batch → summarize_text 순서로 감싸서 통합 함수로 정리했습니다.

collect_news()clean_text()classify_batch()summarize_text()
이 4개 함수를 순서대로 호출하는 run_pipeline(keyword) 함수를 만들었습니다.

키워드를 넣으면 관련 뉴스만 수집하고, 키워드 없이 실행하면 전체 헤드라인을 가져옵니다.
최종 결과는 제목, 카테고리, 신뢰도, 요약이 포함된 DataFrame으로 나옵니다.
(카페에서 커피 주문하듯이, run_pipeline("경제") 한 줄이면 경제 뉴스 수집부터 요약까지 전부 끝나요.)

End-to-End 뉴스 파이프라인 아키텍처
Colab 실습 — 통합 파이프라인 실행 (셀 6)
def run_pipeline(keyword='한국', page_size=20):
    # 1단계: 수집
    print(f'[1/4] 뉴스 수집 중... (keyword={keyword})')
    url = 'https://newsapi.org/v2/everything'
    params = {'q': keyword, 'language': 'ko', 'pageSize': page_size,
              'sortBy': 'publishedAt', 'apiKey': NEWS_API_KEY}
    resp = requests.get(url, params=params)
    data = resp.json()
    articles = []
    for a in data.get('articles', []):
        articles.append({
            'title': a.get('title', ''),
            'description': a.get('description', ''),
            'source': a['source'].get('name', '')
        })
    df = pd.DataFrame(articles)
    print(f'  → {len(df)}건 수집 완료')

    # 2단계: 전처리
    print('[2/4] 전처리 중...')
    df['title_clean'] = df['title'].apply(clean_text)
    df['desc_clean'] = df['description'].apply(clean_text)

    # 3단계: BERT 분류
    print('[3/4] BERT 분류 중...')
    df_cls = classify_batch(df['title_clean'].tolist())
    df['category'] = df_cls['category'].values
    df['confidence'] = df_cls['confidence'].values

    # 4단계: KoBART 요약
    print('[4/4] KoBART 요약 중...')
    df['summary'] = df['desc_clean'].apply(summarize_text)

    return df

df_result = run_pipeline(keyword='한국', page_size=20)
print(f'\n총 {len(df_result)}건 처리 완료')
df_result[['title_clean', 'category', 'confidence', 'summary']].head(5)

4단계가 순서대로 실행되고, 최종 결과가 DataFrame으로 출력됩니다.

셀6 실행 결과 — 통합 파이프라인 20건 처리


결과 살펴보기 — 카테고리별 뉴스 요약 리포트

파이프라인을 돌리고 나면 결과를 카테고리별로 묶어서 볼 수 있습니다.
어떤 카테고리에 기사가 몰려 있는지, 분류 신뢰도는 어느 구간에 많은지 차트로 확인했습니다.

카테고리별 뉴스 분포 + 분류 신뢰도 분포 차트

카테고리별로 대표 기사의 요약 결과를 뽑아봤는데요.
경제 기사는 숫자 중심으로, 스포츠 기사는 경기 결과 중심으로 요약이 나옵니다.
카테고리에 따라 요약 스타일이 달라지는 게 눈에 띕니다.

Colab 실습 — 결과 시각화 + 카테고리별 리포트 (셀 7)
import matplotlib.pyplot as plt
import numpy as np

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5), facecolor='#0A0A0F')

# 좌: 카테고리별 분포
cat_counts = df_result['category'].value_counts()
ax1.bar(cat_counts.index, cat_counts.values, color='#4FD1C5')
ax1.set_facecolor('#0e0e18')
ax1.set_title('카테고리별 분포', color='white', fontsize=14)
ax1.tick_params(colors='#AAAAAA')

# 우: 신뢰도 분포
confidences = df_result['confidence'].str.rstrip('%').astype(float)
ax2.hist(confidences, bins=8, color='#4FD1C5')
ax2.set_facecolor('#0e0e18')
ax2.set_title('분류 신뢰도 분포', color='white', fontsize=14)
ax2.set_xlabel('신뢰도 (%)', color='#AAAAAA')
ax2.tick_params(colors='#AAAAAA')

plt.tight_layout()
plt.show()

# 카테고리별 요약 리포트
for cat in df_result['category'].unique():
    subset = df_result[df_result['category'] == cat]
    print(f'\n[{cat}] — {len(subset)}건')
    for _, row in subset.head(2).iterrows():
        print(f'  제목: {row["title_clean"]}')
        print(f'  요약: {row["summary"][:80]}')

카테고리별 기사 수 분포와 분류 신뢰도 히스토그램을 그립니다. 아래 리포트에서는 각 카테고리별 대표 기사와 요약 결과를 출력합니다.

셀7 실행 코드 셀7 실행 결과 — 카테고리 분포 + 신뢰도 차트 셀7 실행 결과 — 카테고리별 요약 리포트


실제로 돌려보니 뭐가 잘 되고 뭐가 아쉬웠나요?

직접 돌려보니까 잘 되는 부분과 아쉬운 부분이 같이 보이더라고요.

잘 된 점:

  • NewsAPI 수집이 안정적이고, JSON 파싱이 깔끔합니다.
  • BERT 분류 신뢰도가 대부분 90% 이상으로 높게 나옵니다.
  • KoBART 요약 품질도 양호합니다. 핵심 내용을 2~3문장으로 잘 줄여줍니다.

아쉬운 점:

  • NewsAPI 무료 플랜은 하루 100건까지, 24시간 이전 기사만 가능합니다.
  • 긴 기사 본문은 1024 토큰에서 잘리기 때문에 뒷부분이 요약에 반영되지 않습니다.
  • "부동산 대출 규제 강화" 같은 기사는 경제인지 사회인지 사람이 봐도 헷갈립니다. 경계 사례에서의 혼동은 예상 범위입니다.

개선 방향:

  • 스케줄러(cron/APScheduler)를 붙이면 매일 자동으로 수집할 수 있습니다.
  • 분류 결과를 DB에 쌓아서 피드백 루프를 만들면 정확도를 점진적으로 개선할 수 있습니다.
  • 요약 품질은 ROUGE 메트릭으로 정량 평가할 수 있습니다.

(지금은 수동 실행이지만, 자동화와 품질 평가를 붙이면 실제 서비스에 가까운 구조가 됩니다.)

현재 파이프라인 vs 개선 방향 비교

정리

이번 글에서 다룬 핵심만 짧게 정리해봅니다.

  1. 뉴스 수집은 NewsAPI, RSS, 스크래핑 3가지 방법이 있고, API가 가장 깔끔합니다.
  2. 전처리에서 HTML 태그, 특수문자, [속보] prefix 제거가 핵심입니다. re.sub() 5단계로 처리합니다.
  3. 1편 BERT 모델을 재활용하면 추가 학습 없이 새 뉴스를 7개 카테고리로 바로 분류할 수 있습니다.
  4. KoBART Abstractive 요약으로 긴 기사를 2~3문장으로 줄일 수 있습니다.
  5. collect_news → clean_text → classify_batch → summarize_text 체이닝으로 end-to-end 파이프라인을 완성했습니다.

1편에서 분류기를 만들고, 2편에서 그걸 실제 뉴스에 적용하는 데까지 왔습니다.
모델 학습도 중요하지만, 실제 데이터를 넣어서 돌려보면 그때 비로소 보이는 것들이 있어요.

여기에 스케줄러를 붙여서 매일 자동 실행하면?
매일 아침 카테고리별 뉴스 요약 리포트가 쌓이는 구조가 됩니다.
Slack 알림이나 이메일 연동까지 하면 실제 서비스에 가까워지겠죠.

#NewsAPI#웹스크래핑#KoBART#뉴스파이프라인#BERT#텍스트요약#전처리#Colab