나의 개발일지
[네이버 뉴스 요약 프로젝트] 요약 (Summary) 본문
목차
프로젝트 소개 : https://study-yoon.tistory.com/224
1. 크롤링 : https://study-yoon.tistory.com/225
2. 군집화 : https://study-yoon.tistory.com/226
3. 요약 : https://study-yoon.tistory.com/227
Github : https://github.com/Yoon-juhan/naverNewsCrawling
군집화 단계에서 생성된 상위 군집 데이터 프레임을 사용
1. 여러 기사를 하나의 기사로 요약
- 여기서 content는 같은 군집 내의 여러 기사들이다. (매개 변수로 받음)
- 군집 내의 기사들을 하나의 기사로 합치고 문장 단위로 분리
- split() 하면서 마지막에 빈 문자열이 생기는 경우가 있어 따로 처리
- "다.\n"를 기준으로 분리하면서 사라진 "다."를 다시 붙여주기
sentence_list = "\n".join(content)
sentence_list = sentence_list.split("다.\n")
if sentence_list[-1] == "":
sentence_list.pop()
sentence_list = [sentence + "다." for sentence in sentence_list if sentence[-2:] != "다."]
['그룹 르세라핌이 미국의 각종 차트에서 자체 최고 성적을 거두며 인기 상승세를 입증했다.', "21일 세계 최대 음원 스트리밍 플랫폼 스포티파이가 발표한 최신 차트에 따르면, 르세라핌 미니 3집의 타이틀곡 'EASY'가 '데일리 톱 송 미국' 151위를 차지했다.", ' 역대 르세라핌의 곡으로는 공개 첫날 이 차트에 가장 높은 순위로 진입한 신기록이다.', " 'EASY'는 또한 같은 날 이 차트에 신규 진입한 곡 중 최고 순위에 자리했다.", '이 곡은 지난 19일 스포티파이에서 총 145만 1,523회 재생됐는데, 약 25%가 미국에서 스트리밍됐다.', " 발매 첫날의 미국 스트리밍 비율이 전작인 정규 1집 타이틀곡 'UNFORGIVEN '과 첫 영어 싱글 'Perfect Night' 대비 상승해 미국 시장에서 르세라핌의 인기가 높아졌음을 방증한다.", "타이틀곡과 동명의 신보는 지난 20일 정오 미국 아이튠즈 '톱 앨범' 차트 1위를 차지했다.", " 르세라핌이 미국 아이튠즈 '톱 앨범' 정상에 오른 것은 데뷔 이래 이번이 처음이다.", "21일 오전 9시 르세라핌의 영상 4편이 미국 인기 급상승 동영상 차트 '톱 20'에 자리했다.", " 지난 20일 이 차트 1위에 오른 'EASY' 뮤직비디오를 비롯해 팀 공식 유튜브 채널에 게재된 'EASY'와 수록곡 'Swan Song' 퍼포먼스 영상 3편이 차트인했다.", " 한편, 르세라핌의 미니 3집 타이틀곡 'EASY'는 19일 자 스포티파이 '데일리 톱 송 글로벌' 111위에 오르며 전 세계를 아우르는 인기를 얻고 있다.", " 이 곡은 발매 첫날 싱가포르, 한국, 대만 등 총 13개 국가지역의 '데일리 톱 송' 차트에 진입했다.", ' 특히, 한국, 대만, 싱가포르, 홍콩에서는 앨범에 수록된 5개 트랙 모두 순위권에 들어 눈길을 끌었다.', ' 그룹 르세라핌이 미국의 각종 차트에서 자체 최고 성적을 거두며 인기 상승세를 입증했다.', "21일 세계 최대 음원 스트리밍 플랫폼 스포티파이가 발표한 최신 차트에 따르면, 르세라핌 미니 3집의 타이틀곡 'EASY'가 '데일리 톱 송 미국' 151위를 차지했다.", ' 역대 르세라핌의 곡으로는 공개 첫날 이 차트에 가장 높은 순위로 진입한 신기록이다.', " 'EASY'는 또한 같은 날 이 차트에 신규 진입한 곡 중 최고 순위에 자리했다.", '이 곡은 지난 19일 스포티파이에서 총 145만 1,523회 재생됐는데, 약 25%가 미국에서 스트리밍됐다.', " 발매 첫날의 미국 스트리밍 비율이 전작인 정규 1집 타이틀곡 'UNFORGIVEN '과 첫 영어 싱글 'Perfect Night' 대비 상승해 미국 시장에서 르세라핌의 인기가 높아졌음을 방증한다.", "타이틀곡과 동명의 신보는 지난 20일 정오 미국 아이튠즈 '톱 앨범' 차트 1위를 차지했다.", " 르세라핌이 미국 아이튠즈 '톱 앨범' 정상에 오른 것은 데뷔 이래 이번이 처음이다.", "21일 오전 9시 르세라핌의 영상 4편이 미국 인기 급상승 동영상 차트 '톱 20'에 자리했다.", " 지난 20일 이 차트 1위에 오른 'EASY' 뮤직비디오를 비롯해 팀 공식 유튜브 채널에 게재된 'EASY'와 수록곡 'Swan Song' 퍼포먼스 영상 3편이 차트인했다.", " 한편, 르세라핌의 미니 3집 타이틀곡 'EASY'는 19일 자 스포티파이 '데일리 톱 송 글로벌' 111위에 오르며 전 세계를 아우르는 인기를 얻고 있다.", " 이 곡은 발매 첫날 싱가포르, 한국, 대만 등 총 13개 국가지역의 '데일리 톱 송' 차트에 진입했다."]
- 위 기사는 군집 내 2개의 기사를 이어 붙인 리스트이다.
- 유사한 기사들을 하나로 이어 붙였기 때문에 유사한 문장이 있음 (위 기사처럼 복붙한 것 같은 기사도 많음)
- 유사한 문장들의 유사도를 측정해서 하나의 문장만을 남긴다.
- 유사도 측정에는 코사인 유사도와 자카드 유사도를 사용
- 두 개의 유사도 중 하나라도 n이상이면 없애도록 했다.
- 코사인 유사도
- 두 벡터 간의 코사인 각도를 이용하여 구할 수 있는 두 벡터의 유사도
- 문장에서 단어의 순서가 영향을 받음
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
def cosine(x, y):
data = (x, y)
try:
tfidf_vectorizer = TfidfVectorizer()
tfidf_matrix = tfidf_vectorizer.fit_transform(data)
similarity = cosine_similarity(tfidf_matrix[0], tfidf_matrix[1])
except:
return 0
return round(similarity[0][0] * 100, 2)
- 자카드 유사도
- 2개의 집합 A, B가 있을 때 두 집합의 합집합 중 교집합의 비율
- 문장에서 단어의 순서는 영향을 받지 않음
def jaccard(x, y):
intersection_cardinality = len(set.intersection(*[set(x), set(y)]))
union_cardinality = len(set.union(*[set(x), set(y)]))
similarity = intersection_cardinality / float(union_cardinality)
return round(similarity * 100, 2)
- 같은 내용을 쓰더라도 사람마다 쓰는 방식이 다를 수 있어서 자카드 유사도까지 고려해서 제거할 문장을 선택했다.
idx = []
n = 60
for i in range(len(sentence_list)):
for j in range(i+1, len(sentence_list)):
cosine_similarity = cosine(sentence_list[i], sentence_list[j])
jaccard_similarity = jaccard(sentence_list[i], sentence_list[j])
if cosine_similarity >= n or jaccard_similarity >= n:
idx.append(j)
content = []
for i, sentence in enumerate(sentence_list):
if i not in idx:
content.append(sentence)
- 남겨진 문장들 요약
- 리스트에 담긴 문장들을 하나의 문자열로 변환
- summa 라이브러리의 summarize 함수로 요약
- pip install summa
- 인자로 words, ratio를 통해 요약 정도를 설정할 수 있다.
- words : 단어 수를 몇 개로 할지 설정
- ratio : 비율을 설정 (ratio = 0.2)
content = "\n".join(content)
summary_content = summarize(content, words=60) # 단어 수
summary_content = re.sub('다\.\n', '다.\n\n', summary_content)
- 요약 결과
- 요약 코드
def multiDocumentSummarization(content):
# 군집된 기사들을 하나의 문서로 합치고 문장 단위로 나눈다.
sentence_list = "\n".join(content)
sentence_list = sentence_list.split("다.\n")
if sentence_list[-1] == "":
sentence_list.pop()
# split 할 때 사라진 '다.' 를 다시 붙여준다.
sentence_list = [sentence + "다." for sentence in sentence_list if sentence[-2:] != "다."]
# 문장간 유사도 측정
idx = []
n = 60
for i in range(len(sentence_list)):
for j in range(i+1, len(sentence_list)):
cosine_similarity = cosine(sentence_list[i], sentence_list[j])
jaccard_similarity = jaccard(sentence_list[i], sentence_list[j])
if cosine_similarity >= n or jaccard_similarity >= n:
idx.append(j)
content = []
for i, sentence in enumerate(sentence_list):
if i not in idx:
content.append(sentence)
content = "\n".join(content)
summary_content = summarize(content, words=60) # 단어 수
summary_content = re.sub('다\.\n', '다.\n\n', summary_content)
return summary_content
# 코사인 유사도
def cosine(x, y):
data = (x, y)
try:
tfidf_vectorizer = TfidfVectorizer()
tfidf_matrix = tfidf_vectorizer.fit_transform(data)
similarity = cosine_similarity(tfidf_matrix[0], tfidf_matrix[1])
except:
return 0
return round(similarity[0][0] * 100, 2)
# 자카드 유사도
def jaccard(x, y):
intersection_cardinality = len(set.intersection(*[set(x), set(y)]))
union_cardinality = len(set.union(*[set(x), set(y)]))
similarity = intersection_cardinality / float(union_cardinality)
return round(similarity * 100, 2)
2. 최종 데이터 생성
- 카테고리, 제목, 요약된 본문, 이미지, 기사 주소, 키워드 열을 가지는 데이터 프레임으로 생성
- 제목은 군집 내의 첫 번째 기사 제목을 전처리를 해서 넣어주었다.
- 제목과 요약된 내용에서 키워드를 추출
- 바른 : https://bareun.ai/
- bareun.ai에서 만든 형태소 분석기를 사용
- 사용하기 위해 회원가입 후 API KEY를 발급받아야 한다.
- 밑 코드 Tagger(apikey=key) key 부분에 발급받은 key를 입력
- 키워드는 2글자 이상 고유 명사만 추출
- KoNLPy에 있는 형태소 분석기도 사용해봤지만 바른의 형태소 분석기가 품질이 더 좋았다.
def summary(news_df, cluster_counts_df):
summary_news = pd.DataFrame()
for i in tqdm(range(len(cluster_counts_df)), desc="요약"):
category_name, cluster_number = cluster_counts_df.iloc[i, 0:2] # 카테고리 이름, 군집 번호
# 군집내 기사들 df
temp_df = news_df[(news_df['category'] == category_name) & (news_df['cluster_number'] == cluster_number)]
# 카테고리
category = temp_df["category"].iloc[0]
# 뉴스 제목
title = cleanTitle(temp_df["title"].iloc[0])
# 본문 요약
summary_content = multiDocumentSummarization(temp_df["content"])
# 링크
url = ",".join(list(temp_df["url"]))
# 이미지
img = list(temp_df["img"])
if any(img):
img = ",".join(list(temp_df["img"]))
else:
img = ""
# 키워드
keyword = getKeyword(title + " " + summary_content)
# 데이터프레임 생성
summary_news = summary_news.append({
"category" : category,
"title" : title,
"content" : summary_content,
"img" : img,
"url" : url,
"keyword" : keyword
}, ignore_index=True)
summary_news.drop(summary_news[summary_news.content == ""].index, inplace=True)
return summary_news
# 제목 전처리
def cleanTitle(text):
title = text
title = re.sub('\([^)]+\)', '', title)
title = re.sub('\[[^\]]+\]', '',title)
title = re.sub('[ㄱ-ㅎㅏ-ㅣ]+','',title)
title = re.sub('[“”]','"',title)
title = re.sub('[‘’]','\'',title)
title = re.sub('\.{2,3}','...',title)
title = re.sub('…','...',title)
title = re.sub('\·{3}','...',title)
title = re.sub('[=+#/^$@*※&ㆍ!』\\|\<\>`》■□ㅁ◆◇▶◀▷◁△▽▲▼○●━]','',title)
if not title: # 제목이 다 사라졌으면 원래 제목으로
title = text
return title.strip()
# 키워드 추출
def getKeyword(summary_content):
tagger = Tagger(apikey=key)
result = []
res = tagger.tags([summary_content])
pa = res.pos()
for word, type in pa:
if type == 'NNP' and len(word) >= 2:
result.append(word)
return " ".join(result)
- 키워드 결과
- 데이터 프레임
전체 코드
import pandas as pd
import re
from tqdm.notebook import tqdm
from summa.summarizer import summarize
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
from bareunpy import Tagger
from bareunpy_api_key import key
def summary(news_df, cluster_counts_df):
summary_news = pd.DataFrame()
for i in tqdm(range(len(cluster_counts_df)), desc="요약"):
category_name, cluster_number = cluster_counts_df.iloc[i, 0:2] # 카테고리 이름, 군집 번호
# 군집내 기사들 df
temp_df = news_df[(news_df['category'] == category_name) & (news_df['cluster_number'] == cluster_number)]
# 카테고리
category = temp_df["category"].iloc[0]
# 뉴스 제목
title = cleanTitle(temp_df["title"].iloc[0])
# 본문 요약
summary_content = multiDocumentSummarization(temp_df["content"])
# 링크
url = ",".join(list(temp_df["url"]))
# 이미지
img = list(temp_df["img"])
if any(img):
img = ",".join(list(temp_df["img"]))
else:
img = ""
# 키워드
keyword = getKeyword(title + " " + summary_content)
# 데이터프레임 생성
summary_news = summary_news.append({
"category" : category,
"title" : title,
"content" : summary_content,
"img" : img,
"url" : url,
"keyword" : keyword
}, ignore_index=True)
summary_news.drop(summary_news[summary_news.content == ""].index, inplace=True)
return summary_news
def multiDocumentSummarization(content):
# 군집된 기사들을 하나의 문서로 합치고 문장 단위로 나눈다.
sentence_list = "\n".join(content)
sentence_list = sentence_list.split("다.\n")
if sentence_list[-1] == "":
sentence_list.pop()
# split 할 때 사라진 '다.' 를 다시 붙여준다.
sentence_list = [sentence + "다." for sentence in sentence_list if sentence[-2:] != "다."]
# 문장간 유사도 측정
idx = []
n = 60
for i in range(len(sentence_list)):
for j in range(i+1, len(sentence_list)):
cosine_similarity = cosine(sentence_list[i], sentence_list[j])
jaccard_similarity = jaccard(sentence_list[i], sentence_list[j])
if cosine_similarity >= n or jaccard_similarity >= n:
idx.append(j)
content = []
for i, sentence in enumerate(sentence_list):
if i not in idx:
content.append(sentence)
content = "\n".join(content)
summary_content = summarize(content, words=60) # 단어 수
summary_content = re.sub('다\.\n', '다.\n\n', summary_content)
return summary_content
# 코사인 유사도
def cosine(x, y):
data = (x, y)
try:
tfidf_vectorizer = TfidfVectorizer()
tfidf_matrix = tfidf_vectorizer.fit_transform(data)
similarity = cosine_similarity(tfidf_matrix[0], tfidf_matrix[1])
except:
return 0
return round(similarity[0][0] * 100, 2)
# 자카드 유사도
def jaccard(x, y):
intersection_cardinality = len(set.intersection(*[set(x), set(y)]))
union_cardinality = len(set.union(*[set(x), set(y)]))
similarity = intersection_cardinality / float(union_cardinality)
return round(similarity * 100, 2)
# 제목 전처리
def cleanTitle(text):
title = text
title = re.sub('\([^)]+\)', '', title)
title = re.sub('\[[^\]]+\]', '',title)
title = re.sub('[ㄱ-ㅎㅏ-ㅣ]+','',title)
title = re.sub('[“”]','"',title)
title = re.sub('[‘’]','\'',title)
title = re.sub('\.{2,3}','...',title)
title = re.sub('…','...',title)
title = re.sub('\·{3}','...',title)
title = re.sub('[=+#/^$@*※&ㆍ!』\\|\<\>`》■□ㅁ◆◇▶◀▷◁△▽▲▼○●━]','',title)
if not title: # 제목이 다 사라졌으면 원래 제목으로
title = text
return title.strip()
# 키워드 추출
def getKeyword(summary_content):
tagger = Tagger(apikey=key)
result = []
res = tagger.tags([summary_content])
pa = res.pos()
for word, type in pa:
if type == 'NNP' and len(word) >= 2:
result.append(word)
return " ".join(result)
def startSummary(news_df, cluster_counts_df):
summary_news = summary(news_df, cluster_counts_df)
return summary_news
크롤링 ~ 요약을 1시간 마다 수행
- schedule모듈을 사용해 일정 시간마다 코드를 실행
- https://pypi.org/project/schedule/
# coding: utf-8
import schedule
import time
from crawling import startCrawling
from clustering import startClustering
from summary import startSummary
from remove import startRemove
def start():
news_df = startCrawling() # 크롤링
startRemove(news_df) # 필요없는 기사 삭제
cluster_counts_df = startClustering(news_df) # 군집화
summary_news = startSummary(news_df, cluster_counts_df) # 요약
start()
# n시간마다 호출
schedule.every(1).hour.do(start)
while True:
schedule.run_pending()
time.sleep(1)
'네이버 뉴스 요약 프로젝트' 카테고리의 다른 글
[네이버 뉴스 요약 프로젝트] 전처리(Preprocessing) + 군집화(Clustering) (0) | 2023.12.22 |
---|---|
[네이버 뉴스 요약 프로젝트] 크롤링 (Crawling) (0) | 2023.12.22 |
[네이버 뉴스 요약 프로젝트] (1) | 2023.12.22 |