Spaces:
Running
Running
FastAPI for Hugging Face Space: initial setup and files
Browse files- Dockerfile +20 -0
- Profile +1 -0
- embedding_module.py +23 -0
- keyword_module.py +116 -0
- main.py +341 -0
- news_model.pt +3 -0
- requirements.txt +17 -0
- stock_data.csv +0 -0
- stopwords-ko.txt +679 -0
- util/__pycache__/keywordExtract.cpython-310.pyc +0 -0
- util/__pycache__/keywordExtract.cpython-311.pyc +0 -0
- util/keywordExtract.py +160 -0
- util/상장법인목록.xls +0 -0
- 상장법인목록.xls +0 -0
Dockerfile
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.12-slim
|
| 2 |
+
|
| 3 |
+
# openjdk-21로 변경
|
| 4 |
+
RUN apt-get update && \
|
| 5 |
+
apt-get install -y --no-install-recommends openjdk-21-jre-headless wget && \
|
| 6 |
+
apt-get clean && rm -rf /var/lib/apt/lists/*
|
| 7 |
+
|
| 8 |
+
# JAVA_HOME 경로도 21 버전에 맞게 변경
|
| 9 |
+
ENV JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64
|
| 10 |
+
ENV PATH=$JAVA_HOME/bin:$PATH
|
| 11 |
+
|
| 12 |
+
WORKDIR /app
|
| 13 |
+
|
| 14 |
+
COPY requirements.txt .
|
| 15 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 16 |
+
|
| 17 |
+
COPY . .
|
| 18 |
+
|
| 19 |
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
| 20 |
+
|
Profile
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
web: uvicorn main:app --host 0.0.0.0 --port 7860
|
embedding_module.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import os
|
| 3 |
+
from gensim.models import KeyedVectors
|
| 4 |
+
|
| 5 |
+
MODEL_PATH_VEC = "ko.vec"
|
| 6 |
+
|
| 7 |
+
# 모델 로딩
|
| 8 |
+
if os.path.exists(MODEL_PATH_VEC):
|
| 9 |
+
print("🔁 Word2Vec 텍스트 모델 로드 중...")
|
| 10 |
+
model = KeyedVectors.load_word2vec_format(MODEL_PATH_VEC, binary=False)
|
| 11 |
+
print("✅ Word2Vec 모델 로드 완료")
|
| 12 |
+
else:
|
| 13 |
+
raise FileNotFoundError("❌ 'ko.vec' 파일을 찾을 수 없습니다.")
|
| 14 |
+
|
| 15 |
+
def embed_keywords(keywords: list[str]) -> np.ndarray:
|
| 16 |
+
"""
|
| 17 |
+
키워드 리스트를 벡터로 변환하고 평균 벡터 반환
|
| 18 |
+
"""
|
| 19 |
+
vectors = [model[word] for word in keywords if word in model]
|
| 20 |
+
if not vectors:
|
| 21 |
+
return np.zeros(model.vector_size)
|
| 22 |
+
return np.mean(vectors, axis=0)
|
| 23 |
+
|
keyword_module.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# multi/keyword_module.py
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
import requests
|
| 5 |
+
from transformers import PreTrainedTokenizerFast, BartForConditionalGeneration, AutoTokenizer, AutoModel
|
| 6 |
+
from konlpy.tag import Komoran
|
| 7 |
+
from keybert import KeyBERT
|
| 8 |
+
from bs4 import BeautifulSoup as bs
|
| 9 |
+
|
| 10 |
+
# --- 요약용 KoBART ---
|
| 11 |
+
summary_tokenizer = PreTrainedTokenizerFast.from_pretrained("gogamza/kobart-summarization")
|
| 12 |
+
summary_model = BartForConditionalGeneration.from_pretrained("gogamza/kobart-summarization")
|
| 13 |
+
|
| 14 |
+
def summarize_kobart(text, max_input_length=512):
|
| 15 |
+
# 입력을 자르기
|
| 16 |
+
input_ids = summary_tokenizer.encode(text, return_tensors="pt", truncation=True, max_length=max_input_length)
|
| 17 |
+
|
| 18 |
+
summary_ids = summary_model.generate(
|
| 19 |
+
input_ids,
|
| 20 |
+
max_length=160,
|
| 21 |
+
min_length=100,
|
| 22 |
+
num_beams=4,
|
| 23 |
+
repetition_penalty=2.5,
|
| 24 |
+
no_repeat_ngram_size=3,
|
| 25 |
+
early_stopping=True,
|
| 26 |
+
)
|
| 27 |
+
return summary_tokenizer.decode(summary_ids[0], skip_special_tokens=True)
|
| 28 |
+
|
| 29 |
+
# --- KoBERT 임베딩 클래스 ---
|
| 30 |
+
class KoBERTEmbedding:
|
| 31 |
+
def __init__(self, model, tokenizer):
|
| 32 |
+
self.model = model
|
| 33 |
+
self.tokenizer = tokenizer
|
| 34 |
+
|
| 35 |
+
def encode(self, documents):
|
| 36 |
+
if isinstance(documents, str):
|
| 37 |
+
documents = [documents]
|
| 38 |
+
encoded_input = self.tokenizer(
|
| 39 |
+
documents,
|
| 40 |
+
padding=True,
|
| 41 |
+
truncation=True,
|
| 42 |
+
max_length=512,
|
| 43 |
+
return_tensors="pt"
|
| 44 |
+
)
|
| 45 |
+
if "token_type_ids" not in encoded_input:
|
| 46 |
+
encoded_input["token_type_ids"] = torch.zeros_like(encoded_input["input_ids"])
|
| 47 |
+
with torch.no_grad():
|
| 48 |
+
output = self.model(**encoded_input)
|
| 49 |
+
return output.last_hidden_state[:, 0, :].numpy()
|
| 50 |
+
|
| 51 |
+
# --- 키워드 추출 ---
|
| 52 |
+
keyword_tokenizer = AutoTokenizer.from_pretrained("skt/kobert-base-v1", use_fast=False)
|
| 53 |
+
keyword_model = AutoModel.from_pretrained("skt/kobert-base-v1")
|
| 54 |
+
kobert_embedder = KoBERTEmbedding(keyword_model, keyword_tokenizer)
|
| 55 |
+
kw_model = KeyBERT(model=kobert_embedder)
|
| 56 |
+
|
| 57 |
+
# --- 불용어 로드 + 형태소 분석기 ---
|
| 58 |
+
komoran = Komoran()
|
| 59 |
+
|
| 60 |
+
def fetch_korean_stopwords():
|
| 61 |
+
url = "https://raw.githubusercontent.com/stopwords-iso/stopwords-ko/master/stopwords-ko.txt"
|
| 62 |
+
response = requests.get(url)
|
| 63 |
+
return response.text.splitlines()
|
| 64 |
+
|
| 65 |
+
stopwords = fetch_korean_stopwords()
|
| 66 |
+
|
| 67 |
+
def remove_stopwords(text, stopwords):
|
| 68 |
+
nouns = komoran.nouns(text)
|
| 69 |
+
return " ".join([w for w in nouns if w not in stopwords and len(w) > 1])
|
| 70 |
+
|
| 71 |
+
def extract_keywords(summary_text, top_n=5):
|
| 72 |
+
filtered = remove_stopwords(summary_text, stopwords)
|
| 73 |
+
keywords_1st = kw_model.extract_keywords(
|
| 74 |
+
filtered,
|
| 75 |
+
keyphrase_ngram_range=(1, 4),
|
| 76 |
+
stop_words=stopwords,
|
| 77 |
+
top_n=15
|
| 78 |
+
)
|
| 79 |
+
joined = " ".join([kw for kw, _ in keywords_1st])
|
| 80 |
+
keywords_2nd = kw_model.extract_keywords(joined, top_n=top_n)
|
| 81 |
+
return keywords_1st, keywords_2nd
|
| 82 |
+
|
| 83 |
+
# --- 뉴스 크롤링 ---
|
| 84 |
+
def fetch_html(url):
|
| 85 |
+
headers = {"User-Agent": "Mozilla/5.0"}
|
| 86 |
+
response = requests.get(url, headers=headers, timeout=5)
|
| 87 |
+
response.raise_for_status()
|
| 88 |
+
return bs(response.text, "html.parser")
|
| 89 |
+
|
| 90 |
+
def parse_naver(soup):
|
| 91 |
+
title = soup.select_one("h2.media_end_head_headline") or soup.title
|
| 92 |
+
time_tag = soup.select_one("span.media_end_head_info_datestamp_time")
|
| 93 |
+
content_area = soup.find("div", {"id": "newsct_article"}) or soup.find("div", {"id": "dic_area"})
|
| 94 |
+
|
| 95 |
+
title_text = title.get_text(strip=True) if title else "제목 없음"
|
| 96 |
+
time_text = time_tag.get_text(strip=True) if time_tag else "시간 없음"
|
| 97 |
+
if content_area:
|
| 98 |
+
paragraphs = content_area.find_all("p")
|
| 99 |
+
content = '\n'.join([p.get_text(strip=True) for p in paragraphs]) if paragraphs else content_area.get_text(strip=True)
|
| 100 |
+
else:
|
| 101 |
+
content = "본문 없음"
|
| 102 |
+
return title_text, time_text, content
|
| 103 |
+
|
| 104 |
+
def parse_daum(soup):
|
| 105 |
+
title = soup.select_one("h3.tit_view") or soup.title
|
| 106 |
+
time_tag = soup.select_one("span.num_date")
|
| 107 |
+
content_area = soup.find("div", {"class": "article_view"})
|
| 108 |
+
|
| 109 |
+
title_text = title.get_text(strip=True) if title else "제목 없음"
|
| 110 |
+
time_text = time_tag.get_text(strip=True) if time_tag else "시간 없음"
|
| 111 |
+
if content_area:
|
| 112 |
+
paragraphs = content_area.find_all("p")
|
| 113 |
+
content = '\n'.join([p.get_text(strip=True) for p in paragraphs]) if paragraphs else content_area.get_text(strip=True)
|
| 114 |
+
else:
|
| 115 |
+
content = "본문 없음"
|
| 116 |
+
return title_text, time_text, content
|
main.py
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI, HTTPException, Query
|
| 2 |
+
import uvicorn
|
| 3 |
+
from pydantic import BaseModel
|
| 4 |
+
import requests
|
| 5 |
+
from bs4 import BeautifulSoup as bs
|
| 6 |
+
import mysql.connector
|
| 7 |
+
import os
|
| 8 |
+
import google.generativeai as genai
|
| 9 |
+
import json
|
| 10 |
+
from util.keywordExtract import *
|
| 11 |
+
from typing import Optional,List, Dict, Any
|
| 12 |
+
import pandas as pd
|
| 13 |
+
import torch
|
| 14 |
+
import pandas as pd
|
| 15 |
+
from io import StringIO # pandas.read_html에 문자열을 전달할 때 필요
|
| 16 |
+
import logging # 로깅을 위해 추가
|
| 17 |
+
import time # 요청 간 지연을 위해 추가 (선택 사항이지만 권장)
|
| 18 |
+
from embedding_module import embed_keywords
|
| 19 |
+
from keyword_module import summarize_kobart as summarize, extract_keywords
|
| 20 |
+
from pykrx import stock
|
| 21 |
+
from functools import lru_cache
|
| 22 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 23 |
+
import traceback
|
| 24 |
+
from datetime import datetime, timedelta
|
| 25 |
+
from googletrans import Translator
|
| 26 |
+
from starlette.concurrency import run_in_threadpool
|
| 27 |
+
import FinanceDataReader as fdr
|
| 28 |
+
|
| 29 |
+
app = FastAPI()
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
# 로깅 설정
|
| 33 |
+
logging.basicConfig(level=logging.INFO)
|
| 34 |
+
logger = logging.getLogger(__name__)
|
| 35 |
+
|
| 36 |
+
API_KEY = os.getenv("GEMINI_API_KEY")
|
| 37 |
+
|
| 38 |
+
if not API_KEY:
|
| 39 |
+
# API 키가 없으면 에러를 발생시키거나 경고
|
| 40 |
+
print("❌ GEMINI_API_KEY 환경 변수가 설정되지 않았습니다.")
|
| 41 |
+
|
| 42 |
+
else:
|
| 43 |
+
genai.configure(api_key=API_KEY)
|
| 44 |
+
logger.info("✅ Gemini API 설정 완료 (환경 변수 사용)")
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class NewsRequest(BaseModel):
|
| 49 |
+
url: str
|
| 50 |
+
id: Optional[str] = None
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
# 🧠 학습 모델 구조 정의
|
| 54 |
+
class SimpleClassifier(torch.nn.Module):
|
| 55 |
+
def __init__(self, input_dim):
|
| 56 |
+
super().__init__()
|
| 57 |
+
self.net = torch.nn.Sequential(
|
| 58 |
+
torch.nn.Linear(input_dim, 64),
|
| 59 |
+
torch.nn.ReLU(),
|
| 60 |
+
torch.nn.Linear(64, 1),
|
| 61 |
+
torch.nn.Sigmoid()
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
def forward(self, x):
|
| 65 |
+
return self.net(x)
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def fetch_html(url):
|
| 70 |
+
headers = {"User-Agent": "Mozilla/5.0"}
|
| 71 |
+
response = requests.get(url, headers=headers, timeout=5)
|
| 72 |
+
response.raise_for_status()
|
| 73 |
+
return bs(response.text, "html.parser")
|
| 74 |
+
|
| 75 |
+
def parse_naver(soup):
|
| 76 |
+
title = soup.select_one("h2.media_end_head_headline") or soup.title
|
| 77 |
+
title_text = title.get_text(strip=True) if title else "제목 없음"
|
| 78 |
+
|
| 79 |
+
time_tag = soup.select_one("span.media_end_head_info_datestamp_time")
|
| 80 |
+
time_text = time_tag.get_text(strip=True) if time_tag else "시간 없음"
|
| 81 |
+
|
| 82 |
+
content_area = soup.find("div", {"id": "newsct_article"}) or soup.find("div", {"id": "dic_area"})
|
| 83 |
+
if content_area:
|
| 84 |
+
paragraphs = content_area.find_all("p")
|
| 85 |
+
content = '\n'.join([p.get_text(strip=True) for p in paragraphs]) if paragraphs else content_area.get_text(strip=True)
|
| 86 |
+
else:
|
| 87 |
+
content = "본문 없음"
|
| 88 |
+
|
| 89 |
+
return title_text, time_text, content
|
| 90 |
+
|
| 91 |
+
def parse_daum(soup):
|
| 92 |
+
title = soup.select_one("h3.tit_view") or soup.title
|
| 93 |
+
title_text = title.get_text(strip=True) if title else "제목 없음"
|
| 94 |
+
|
| 95 |
+
time_tag = soup.select_one("span.num_date")
|
| 96 |
+
time_text = time_tag.get_text(strip=True) if time_tag else "시간 없음"
|
| 97 |
+
|
| 98 |
+
content_area = soup.find("div", {"class": "article_view"})
|
| 99 |
+
if content_area:
|
| 100 |
+
paragraphs = content_area.find_all("p")
|
| 101 |
+
content = '\n'.join([p.get_text(strip=True) for p in paragraphs]) if paragraphs else content_area.get_text(strip=True)
|
| 102 |
+
else:
|
| 103 |
+
content = "본문 없음"
|
| 104 |
+
|
| 105 |
+
return title_text, time_text, content
|
| 106 |
+
|
| 107 |
+
def extract_thumbnail(soup):
|
| 108 |
+
tag = soup.find("meta", property="og:image")
|
| 109 |
+
return tag["content"] if tag and "content" in tag.attrs else None
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def gemini_use(resultK):
|
| 113 |
+
generation_config = genai.GenerationConfig(
|
| 114 |
+
temperature=1,
|
| 115 |
+
response_mime_type=None # 그냥 문자열로 응답받기
|
| 116 |
+
)
|
| 117 |
+
model = genai.GenerativeModel('gemini-2.0-flash', generation_config=generation_config)
|
| 118 |
+
|
| 119 |
+
prompt = f"""
|
| 120 |
+
아래 내용을 참고해서 가장 연관성이 높은 주식 상장 회사 이름 하나만 말해줘.
|
| 121 |
+
다른 설명 없이 회사 이름만 대답해.
|
| 122 |
+
|
| 123 |
+
"{resultK}"
|
| 124 |
+
"""
|
| 125 |
+
|
| 126 |
+
response = model.generate_content(prompt)
|
| 127 |
+
try:
|
| 128 |
+
result_text = response.text.strip()
|
| 129 |
+
except AttributeError:
|
| 130 |
+
result_text = response.candidates[0].content.parts[0].text.strip()
|
| 131 |
+
|
| 132 |
+
return result_text
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
@app.post("/ai/parse-news")
|
| 138 |
+
def parse_news(req: NewsRequest):
|
| 139 |
+
url = req.url.strip()
|
| 140 |
+
username = req.id.strip() if req.id else None
|
| 141 |
+
try:
|
| 142 |
+
soup = fetch_html(url)
|
| 143 |
+
|
| 144 |
+
if "naver.com" in url:
|
| 145 |
+
title, time, content = parse_naver(soup)
|
| 146 |
+
elif "daum.net" in url:
|
| 147 |
+
title, time, content = parse_daum(soup)
|
| 148 |
+
else:
|
| 149 |
+
raise HTTPException(status_code=400, detail="지원하지 않는 뉴스 사이트입니다.")
|
| 150 |
+
|
| 151 |
+
thumbnail_url = extract_thumbnail(soup)
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
resultK = resultKeyword(content)
|
| 155 |
+
sumce = classify_emotion(content)
|
| 156 |
+
targetCompany = gemini_use(resultK)
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
sentiment = analyze_sentiment(content)
|
| 160 |
+
pos_percent = int(sentiment["positive"] * 100)
|
| 161 |
+
neg_percent = int(sentiment["negative"] * 100)
|
| 162 |
+
|
| 163 |
+
sentiment_result = {
|
| 164 |
+
"positive": pos_percent,
|
| 165 |
+
"negative": neg_percent
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
summary = summarize(content)
|
| 169 |
+
print(summary)
|
| 170 |
+
|
| 171 |
+
_, keywords_2nd = extract_keywords(summary)
|
| 172 |
+
clean_keywords = [kw for kw, _ in keywords_2nd]
|
| 173 |
+
|
| 174 |
+
keyword_vec = embed_keywords(clean_keywords)
|
| 175 |
+
input_vec = torch.tensor(keyword_vec, dtype=torch.float32).unsqueeze(0) # (1, D)
|
| 176 |
+
|
| 177 |
+
input_dim = input_vec.shape[1]
|
| 178 |
+
model = SimpleClassifier(input_dim)
|
| 179 |
+
|
| 180 |
+
model.load_state_dict(torch.load("news_model.pt", map_location="cpu"))
|
| 181 |
+
model.eval()
|
| 182 |
+
|
| 183 |
+
with torch.no_grad():
|
| 184 |
+
prob = model(input_vec).item()
|
| 185 |
+
prediction = int(prob >= 0.5)
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
prediction = '📈 상승 (1)' if prediction == 1 else '📉 하락 (0)'
|
| 189 |
+
print(type(prob))
|
| 190 |
+
print(type(prediction))
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
return {
|
| 195 |
+
"message": "뉴스 파싱 및 저장 완료",
|
| 196 |
+
"title": title,
|
| 197 |
+
"time": time,
|
| 198 |
+
"content": content,
|
| 199 |
+
"thumbnail_url": thumbnail_url,
|
| 200 |
+
"url": url,
|
| 201 |
+
"summary": resultK["summary"],
|
| 202 |
+
"keyword": resultK["keyword"],
|
| 203 |
+
"company": targetCompany,
|
| 204 |
+
"sentiment": sumce,
|
| 205 |
+
"sentiment_value": sentiment_result,
|
| 206 |
+
"prediction": prediction,
|
| 207 |
+
"prob": prob,
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
except requests.exceptions.RequestException as e:
|
| 211 |
+
traceback.print_exc() # 전체 스택트레이스 콘솔에 출력
|
| 212 |
+
raise HTTPException(status_code=500, detail=f"서버 오류: {e}")
|
| 213 |
+
except Exception as e:
|
| 214 |
+
traceback.print_exc() # 전체 스택트레이스 콘솔에 출력
|
| 215 |
+
raise HTTPException(status_code=500, detail=f"서버 오류: {e}")
|
| 216 |
+
|
| 217 |
+
from fastapi.concurrency import run_in_threadpool # 동기 함수를 비동기처럼 실행하기 위해
|
| 218 |
+
from typing import List, Dict, Any # 반환 타입 명시를 위해 (선택 사항)
|
| 219 |
+
# --- 전역 변수 (서버 시작 시 초기화) ---
|
| 220 |
+
krx_listings: pd.DataFrame = None
|
| 221 |
+
us_listings: pd.DataFrame = None
|
| 222 |
+
translator: Translator = None
|
| 223 |
+
|
| 224 |
+
# --- 서버 시작 시 실행될 로직 ---
|
| 225 |
+
@app.on_event("startup")
|
| 226 |
+
async def load_initial_data():
|
| 227 |
+
"""
|
| 228 |
+
서버가 시작될 때 주식 목록과 번역기를 미리 로드하여
|
| 229 |
+
API 요청마다 반복적으로 로드하는 것을 방지합니다.
|
| 230 |
+
"""
|
| 231 |
+
global krx_listings, us_listings, translator
|
| 232 |
+
|
| 233 |
+
logger.info("✅ 서버 시작: 초기 데이터 로딩을 시작합니다...")
|
| 234 |
+
try:
|
| 235 |
+
krx_listings = await run_in_threadpool(fdr.StockListing, 'KRX')
|
| 236 |
+
logger.info("📊 한국 상장 기업 목록 로딩 완료.")
|
| 237 |
+
|
| 238 |
+
nasdaq = await run_in_threadpool(fdr.StockListing, 'NASDAQ')
|
| 239 |
+
nyse = await run_in_threadpool(fdr.StockListing, 'NYSE')
|
| 240 |
+
amex = await run_in_threadpool(fdr.StockListing, 'AMEX')
|
| 241 |
+
us_listings = pd.concat([nasdaq, nyse, amex], ignore_index=True)
|
| 242 |
+
logger.info("📊 미국 상장 기업 목록 로딩 완료.")
|
| 243 |
+
|
| 244 |
+
translator = Translator()
|
| 245 |
+
logger.info("🌐 번역기 초기화 완료.")
|
| 246 |
+
|
| 247 |
+
logger.info("✅ 모든 초기 데이터 로딩이 성공적으로 완료되었습니다.")
|
| 248 |
+
|
| 249 |
+
except Exception as e:
|
| 250 |
+
logger.error(f"🚨 초기 데이터 로딩 중 심각한 오류 발생: {e}", exc_info=True)
|
| 251 |
+
# 필요하다면 여기서 서버 실행을 중단시킬 수도 있습니다.
|
| 252 |
+
# raise RuntimeError("Failed to load initial stock listings.") from e
|
| 253 |
+
|
| 254 |
+
# --- 핵심 로직 함수 ---
|
| 255 |
+
def get_stock_info(company_name: str) -> Dict[str, str] | None:
|
| 256 |
+
"""
|
| 257 |
+
회사명을 받아 한국 또는 미국 시장에서 종목 정보를 찾아 반환합니다.
|
| 258 |
+
(정상 동작하는 스크립트의 로직을 그대로 적용)
|
| 259 |
+
"""
|
| 260 |
+
# 1. 한국 주식에서 먼저 검색
|
| 261 |
+
kr_match = krx_listings[krx_listings['Name'].str.contains(company_name, case=False, na=False)]
|
| 262 |
+
if not kr_match.empty:
|
| 263 |
+
stock = kr_match.iloc[0]
|
| 264 |
+
logger.info(f"KRX에서 '{company_name}' 발견: {stock['Name']} ({stock['Code']})")
|
| 265 |
+
return {"market": "KRX", "symbol": stock['Code'], "name": stock['Name']}
|
| 266 |
+
|
| 267 |
+
# 2. 한국에 없으면 미국 주식에서 검색 (번역기 사용)
|
| 268 |
+
try:
|
| 269 |
+
# 번역은 I/O 작업이므로 스레드풀에서 실행하는 것이 더 안전할 수 있으나,
|
| 270 |
+
# googletrans의 내부 구현에 따라 여기서 직접 호출해도 큰 문제가 없을 수 있습니다.
|
| 271 |
+
company_name_eng = translator.translate(company_name, src='ko', dest='en').text
|
| 272 |
+
logger.info(f"'{company_name}' -> 영어로 번역: '{company_name_eng}'")
|
| 273 |
+
|
| 274 |
+
# 이름 또는 심볼에서 검색
|
| 275 |
+
us_match = us_listings[
|
| 276 |
+
us_listings['Name'].str.contains(company_name_eng, case=False, na=False) |
|
| 277 |
+
us_listings['Symbol'].str.fullmatch(company_name_eng, case=False)
|
| 278 |
+
]
|
| 279 |
+
|
| 280 |
+
if not us_match.empty:
|
| 281 |
+
stock = us_match.iloc[0]
|
| 282 |
+
logger.info(f"US에서 '{company_name}' 발견: {stock['Name']} ({stock['Symbol']})")
|
| 283 |
+
return {"market": "US", "symbol": stock['Symbol'], "name": stock['Name']}
|
| 284 |
+
|
| 285 |
+
except Exception as e:
|
| 286 |
+
logger.error(f"'{company_name}' 번역 또는 미국 주식 검색 중 오류: {e}")
|
| 287 |
+
|
| 288 |
+
# 3. 최종적으로 찾지 못한 경우
|
| 289 |
+
logger.warning(f"'{company_name}'에 해당하는 종목을 찾지 못했습니다.")
|
| 290 |
+
return None
|
| 291 |
+
|
| 292 |
+
def fetch_stock_prices_sync(symbol: str, days: int = 365) -> pd.DataFrame:
|
| 293 |
+
"""
|
| 294 |
+
지정된 기간 동안의 주가 데이터를 가져옵니다 (동기 함수).
|
| 295 |
+
"""
|
| 296 |
+
end_date = datetime.today()
|
| 297 |
+
start_date = end_date - timedelta(days=days)
|
| 298 |
+
|
| 299 |
+
logger.info(f"FinanceDataReader로 '{symbol}'의 주가 데이터 조회를 시작합니다 ({start_date.date()} ~ {end_date.date()}).")
|
| 300 |
+
try:
|
| 301 |
+
df = fdr.DataReader(symbol, start=start_date, end=end_date)
|
| 302 |
+
if df.empty:
|
| 303 |
+
logger.warning(f"'{symbol}'에 대한 데이터가 없습니다.")
|
| 304 |
+
return None
|
| 305 |
+
return df
|
| 306 |
+
except Exception as e:
|
| 307 |
+
logger.error(f"'{symbol}' 데이터 조회 중 오류 발생: {e}", exc_info=True)
|
| 308 |
+
return None
|
| 309 |
+
|
| 310 |
+
# --- API 엔드포인트 ---
|
| 311 |
+
|
| 312 |
+
@app.get("/ai/stock-data/by-name",
|
| 313 |
+
summary="회사명으로 최근 1년 주가 데이터 조회 (JSON)",
|
| 314 |
+
description="회사명(예: 삼성전자, 애플)을 입력받아 최근 1년간의 일별 주가 데이터를 JSON 형식으로 반환합니다.")
|
| 315 |
+
async def get_stock_data_by_name(
|
| 316 |
+
company_name: str = Query(..., description="조회할 회사명")
|
| 317 |
+
) -> List[Dict[str, Any]]:
|
| 318 |
+
|
| 319 |
+
if not company_name or not company_name.strip():
|
| 320 |
+
raise HTTPException(status_code=400, detail="회사명을 입력해주세요.")
|
| 321 |
+
|
| 322 |
+
stock_info = await run_in_threadpool(get_stock_info, company_name.strip())
|
| 323 |
+
|
| 324 |
+
if not stock_info:
|
| 325 |
+
raise HTTPException(status_code=404, detail=f"'{company_name}'에 해당하는 종목을 찾을 수 없습니다.")
|
| 326 |
+
|
| 327 |
+
prices_df = await run_in_threadpool(fetch_stock_prices_sync, stock_info['symbol'], 365)
|
| 328 |
+
|
| 329 |
+
if prices_df is None or prices_df.empty:
|
| 330 |
+
raise HTTPException(status_code=404, detail=f"'{stock_info['name']}'의 시세 데이터를 찾을 수 없습니다.")
|
| 331 |
+
|
| 332 |
+
prices_df.index.name = 'Date' # 👈 이 줄을 추가하여 인덱스 이름을 명시적으로 설정
|
| 333 |
+
prices_df.reset_index(inplace=True)
|
| 334 |
+
prices_df['Date'] = prices_df['Date'].dt.strftime('%Y-%m-%d')
|
| 335 |
+
|
| 336 |
+
return prices_df.to_dict(orient='records')
|
| 337 |
+
|
| 338 |
+
|
| 339 |
+
|
| 340 |
+
if __name__ == "__main__":
|
| 341 |
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
news_model.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:41d5d6b46d8eb27a6bb599ac7e9aeaa8f45f427e1f02a2a0396fb19f2316a9eb
|
| 3 |
+
size 53864
|
requirements.txt
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi
|
| 2 |
+
uvicorn
|
| 3 |
+
torch
|
| 4 |
+
google-generativeai
|
| 5 |
+
transformers
|
| 6 |
+
keybert
|
| 7 |
+
konlpy
|
| 8 |
+
sentencepiece
|
| 9 |
+
mysql-connector-python
|
| 10 |
+
pandas
|
| 11 |
+
requests
|
| 12 |
+
pykrx
|
| 13 |
+
beautifulsoup4
|
| 14 |
+
gensim
|
| 15 |
+
finance-datareader
|
| 16 |
+
googletrans==4.0.0-rc1
|
| 17 |
+
openpyxl
|
stock_data.csv
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
stopwords-ko.txt
ADDED
|
@@ -0,0 +1,679 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
!
|
| 2 |
+
"
|
| 3 |
+
$
|
| 4 |
+
%
|
| 5 |
+
&
|
| 6 |
+
'
|
| 7 |
+
(
|
| 8 |
+
)
|
| 9 |
+
*
|
| 10 |
+
+
|
| 11 |
+
,
|
| 12 |
+
-
|
| 13 |
+
.
|
| 14 |
+
...
|
| 15 |
+
0
|
| 16 |
+
1
|
| 17 |
+
2
|
| 18 |
+
3
|
| 19 |
+
4
|
| 20 |
+
5
|
| 21 |
+
6
|
| 22 |
+
7
|
| 23 |
+
8
|
| 24 |
+
9
|
| 25 |
+
;
|
| 26 |
+
<
|
| 27 |
+
=
|
| 28 |
+
>
|
| 29 |
+
?
|
| 30 |
+
@
|
| 31 |
+
\
|
| 32 |
+
^
|
| 33 |
+
_
|
| 34 |
+
`
|
| 35 |
+
|
|
| 36 |
+
~
|
| 37 |
+
·
|
| 38 |
+
—
|
| 39 |
+
——
|
| 40 |
+
‘
|
| 41 |
+
’
|
| 42 |
+
“
|
| 43 |
+
”
|
| 44 |
+
…
|
| 45 |
+
、
|
| 46 |
+
。
|
| 47 |
+
〈
|
| 48 |
+
〉
|
| 49 |
+
《
|
| 50 |
+
》
|
| 51 |
+
가
|
| 52 |
+
가까스로
|
| 53 |
+
가령
|
| 54 |
+
각
|
| 55 |
+
각각
|
| 56 |
+
각자
|
| 57 |
+
각종
|
| 58 |
+
갖고말하자면
|
| 59 |
+
같다
|
| 60 |
+
같이
|
| 61 |
+
개의치않고
|
| 62 |
+
거니와
|
| 63 |
+
거바
|
| 64 |
+
거의
|
| 65 |
+
것
|
| 66 |
+
것과 같이
|
| 67 |
+
것들
|
| 68 |
+
게다가
|
| 69 |
+
게우다
|
| 70 |
+
겨우
|
| 71 |
+
견지에서
|
| 72 |
+
결과에 이르다
|
| 73 |
+
결국
|
| 74 |
+
결론을 낼 수 있다
|
| 75 |
+
겸사겸사
|
| 76 |
+
고려하면
|
| 77 |
+
고로
|
| 78 |
+
곧
|
| 79 |
+
공동으로
|
| 80 |
+
과
|
| 81 |
+
과연
|
| 82 |
+
관계가 있다
|
| 83 |
+
관계없이
|
| 84 |
+
관련이 있다
|
| 85 |
+
관하여
|
| 86 |
+
관한
|
| 87 |
+
관해서는
|
| 88 |
+
구
|
| 89 |
+
구체적으로
|
| 90 |
+
구토하다
|
| 91 |
+
그
|
| 92 |
+
그들
|
| 93 |
+
그때
|
| 94 |
+
그래
|
| 95 |
+
그래도
|
| 96 |
+
그래서
|
| 97 |
+
그러나
|
| 98 |
+
그러니
|
| 99 |
+
그러니까
|
| 100 |
+
그러면
|
| 101 |
+
그러므로
|
| 102 |
+
그러한즉
|
| 103 |
+
그런 까닭에
|
| 104 |
+
그런데
|
| 105 |
+
그런즉
|
| 106 |
+
그럼
|
| 107 |
+
그럼에도 불구하고
|
| 108 |
+
그렇게 함으로써
|
| 109 |
+
그렇지
|
| 110 |
+
그렇지 않다면
|
| 111 |
+
그렇지 않으면
|
| 112 |
+
그렇지만
|
| 113 |
+
그렇지않으면
|
| 114 |
+
그리고
|
| 115 |
+
그리하여
|
| 116 |
+
그만이다
|
| 117 |
+
그에 따르는
|
| 118 |
+
그위에
|
| 119 |
+
그저
|
| 120 |
+
그중에서
|
| 121 |
+
그치지 않다
|
| 122 |
+
근거로
|
| 123 |
+
근거하여
|
| 124 |
+
기대여
|
| 125 |
+
기점으로
|
| 126 |
+
기준으로
|
| 127 |
+
기타
|
| 128 |
+
까닭으로
|
| 129 |
+
까악
|
| 130 |
+
까지
|
| 131 |
+
까지 미치다
|
| 132 |
+
까지도
|
| 133 |
+
꽈당
|
| 134 |
+
끙끙
|
| 135 |
+
끼익
|
| 136 |
+
나
|
| 137 |
+
나머지는
|
| 138 |
+
남들
|
| 139 |
+
남짓
|
| 140 |
+
너
|
| 141 |
+
너희
|
| 142 |
+
너희들
|
| 143 |
+
네
|
| 144 |
+
넷
|
| 145 |
+
년
|
| 146 |
+
논하지 않다
|
| 147 |
+
놀라다
|
| 148 |
+
누가 알겠는가
|
| 149 |
+
누구
|
| 150 |
+
다른
|
| 151 |
+
다른 방면으로
|
| 152 |
+
다만
|
| 153 |
+
다섯
|
| 154 |
+
다소
|
| 155 |
+
다수
|
| 156 |
+
다시 말하자면
|
| 157 |
+
다시말하면
|
| 158 |
+
다음
|
| 159 |
+
다음에
|
| 160 |
+
다음으로
|
| 161 |
+
단지
|
| 162 |
+
답다
|
| 163 |
+
당신
|
| 164 |
+
당장
|
| 165 |
+
대로 하다
|
| 166 |
+
대하면
|
| 167 |
+
대하여
|
| 168 |
+
대해 말하자면
|
| 169 |
+
대해서
|
| 170 |
+
댕그
|
| 171 |
+
더구나
|
| 172 |
+
더군다나
|
| 173 |
+
더라도
|
| 174 |
+
더불어
|
| 175 |
+
더욱더
|
| 176 |
+
더욱이는
|
| 177 |
+
도달하다
|
| 178 |
+
도착하다
|
| 179 |
+
동시에
|
| 180 |
+
동안
|
| 181 |
+
된바에야
|
| 182 |
+
된이상
|
| 183 |
+
두번째로
|
| 184 |
+
둘
|
| 185 |
+
둥둥
|
| 186 |
+
뒤따라
|
| 187 |
+
뒤이어
|
| 188 |
+
든간에
|
| 189 |
+
들
|
| 190 |
+
등
|
| 191 |
+
등등
|
| 192 |
+
딩동
|
| 193 |
+
따라
|
| 194 |
+
따라서
|
| 195 |
+
따위
|
| 196 |
+
따지지 않다
|
| 197 |
+
딱
|
| 198 |
+
때
|
| 199 |
+
때가 되어
|
| 200 |
+
때문에
|
| 201 |
+
또
|
| 202 |
+
또한
|
| 203 |
+
뚝뚝
|
| 204 |
+
라 해도
|
| 205 |
+
령
|
| 206 |
+
로
|
| 207 |
+
로 인하여
|
| 208 |
+
로부터
|
| 209 |
+
로써
|
| 210 |
+
륙
|
| 211 |
+
를
|
| 212 |
+
마음대로
|
| 213 |
+
마저
|
| 214 |
+
마저도
|
| 215 |
+
마치
|
| 216 |
+
막론하고
|
| 217 |
+
만 못하다
|
| 218 |
+
만약
|
| 219 |
+
만약에
|
| 220 |
+
만은 아니다
|
| 221 |
+
만이 아니다
|
| 222 |
+
만일
|
| 223 |
+
만큼
|
| 224 |
+
말하자면
|
| 225 |
+
말할것도 없고
|
| 226 |
+
매
|
| 227 |
+
매번
|
| 228 |
+
메쓰겁다
|
| 229 |
+
몇
|
| 230 |
+
모
|
| 231 |
+
모두
|
| 232 |
+
무렵
|
| 233 |
+
무릎쓰고
|
| 234 |
+
무슨
|
| 235 |
+
무엇
|
| 236 |
+
무엇때문에
|
| 237 |
+
물론
|
| 238 |
+
및
|
| 239 |
+
바꾸어말하면
|
| 240 |
+
바꾸어말하자면
|
| 241 |
+
바꾸어서 말하면
|
| 242 |
+
바꾸어서 한다면
|
| 243 |
+
바꿔 말하면
|
| 244 |
+
바로
|
| 245 |
+
바와같이
|
| 246 |
+
밖에 안된다
|
| 247 |
+
반대로
|
| 248 |
+
반대로 말하자면
|
| 249 |
+
반드시
|
| 250 |
+
버금
|
| 251 |
+
보는데서
|
| 252 |
+
보다더
|
| 253 |
+
보드득
|
| 254 |
+
본대로
|
| 255 |
+
봐
|
| 256 |
+
봐라
|
| 257 |
+
부류의 사람들
|
| 258 |
+
부터
|
| 259 |
+
불구하고
|
| 260 |
+
불문하고
|
| 261 |
+
붕붕
|
| 262 |
+
비걱거리다
|
| 263 |
+
비교적
|
| 264 |
+
비길수 없다
|
| 265 |
+
비로소
|
| 266 |
+
비록
|
| 267 |
+
비슷하다
|
| 268 |
+
비추어 보아
|
| 269 |
+
비하면
|
| 270 |
+
뿐만 아니라
|
| 271 |
+
뿐만아니라
|
| 272 |
+
뿐이다
|
| 273 |
+
삐걱
|
| 274 |
+
삐걱거리다
|
| 275 |
+
사
|
| 276 |
+
삼
|
| 277 |
+
상대적으로 말하자면
|
| 278 |
+
생각한대로
|
| 279 |
+
설령
|
| 280 |
+
설마
|
| 281 |
+
설사
|
| 282 |
+
셋
|
| 283 |
+
소생
|
| 284 |
+
소인
|
| 285 |
+
솨
|
| 286 |
+
쉿
|
| 287 |
+
습니까
|
| 288 |
+
습니다
|
| 289 |
+
시각
|
| 290 |
+
시간
|
| 291 |
+
시작하여
|
| 292 |
+
시초에
|
| 293 |
+
시키다
|
| 294 |
+
실로
|
| 295 |
+
심지어
|
| 296 |
+
아
|
| 297 |
+
아니
|
| 298 |
+
아니나다를가
|
| 299 |
+
아니라면
|
| 300 |
+
아니면
|
| 301 |
+
아니었다면
|
| 302 |
+
아래윗
|
| 303 |
+
아무거나
|
| 304 |
+
아무도
|
| 305 |
+
아야
|
| 306 |
+
아울러
|
| 307 |
+
아이
|
| 308 |
+
아이고
|
| 309 |
+
아이구
|
| 310 |
+
아이야
|
| 311 |
+
아이쿠
|
| 312 |
+
아하
|
| 313 |
+
아홉
|
| 314 |
+
안 그러면
|
| 315 |
+
않기 위하여
|
| 316 |
+
않기 위해서
|
| 317 |
+
알 수 있다
|
| 318 |
+
알았어
|
| 319 |
+
앗
|
| 320 |
+
앞에서
|
| 321 |
+
앞의것
|
| 322 |
+
야
|
| 323 |
+
약간
|
| 324 |
+
양자
|
| 325 |
+
어
|
| 326 |
+
어기여차
|
| 327 |
+
어느
|
| 328 |
+
어느 년도
|
| 329 |
+
어느것
|
| 330 |
+
어느곳
|
| 331 |
+
어느때
|
| 332 |
+
어느쪽
|
| 333 |
+
어느해
|
| 334 |
+
어디
|
| 335 |
+
어때
|
| 336 |
+
어떠한
|
| 337 |
+
어떤
|
| 338 |
+
어떤것
|
| 339 |
+
어떤것들
|
| 340 |
+
어떻게
|
| 341 |
+
어떻해
|
| 342 |
+
어이
|
| 343 |
+
어째서
|
| 344 |
+
어쨋든
|
| 345 |
+
어쩔수 없다
|
| 346 |
+
어찌
|
| 347 |
+
어찌됏든
|
| 348 |
+
어찌됏어
|
| 349 |
+
어찌하든지
|
| 350 |
+
어찌하여
|
| 351 |
+
언제
|
| 352 |
+
언젠가
|
| 353 |
+
얼마
|
| 354 |
+
얼마 안 되는 것
|
| 355 |
+
얼마간
|
| 356 |
+
얼마나
|
| 357 |
+
얼마든지
|
| 358 |
+
얼마만큼
|
| 359 |
+
얼마큼
|
| 360 |
+
엉엉
|
| 361 |
+
에
|
| 362 |
+
에 가서
|
| 363 |
+
에 달려 있다
|
| 364 |
+
에 대해
|
| 365 |
+
에 있다
|
| 366 |
+
에 한하다
|
| 367 |
+
에게
|
| 368 |
+
에서
|
| 369 |
+
여
|
| 370 |
+
여기
|
| 371 |
+
여덟
|
| 372 |
+
여러분
|
| 373 |
+
여보시오
|
| 374 |
+
여부
|
| 375 |
+
여섯
|
| 376 |
+
여전히
|
| 377 |
+
여차
|
| 378 |
+
연관되다
|
| 379 |
+
연이서
|
| 380 |
+
영
|
| 381 |
+
영차
|
| 382 |
+
옆사람
|
| 383 |
+
예
|
| 384 |
+
예를 들면
|
| 385 |
+
예를 들자면
|
| 386 |
+
예컨대
|
| 387 |
+
예하면
|
| 388 |
+
오
|
| 389 |
+
오로지
|
| 390 |
+
오르다
|
| 391 |
+
오자마자
|
| 392 |
+
오직
|
| 393 |
+
오호
|
| 394 |
+
오히려
|
| 395 |
+
와
|
| 396 |
+
와 같은 사람들
|
| 397 |
+
와르르
|
| 398 |
+
와아
|
| 399 |
+
왜
|
| 400 |
+
왜냐하면
|
| 401 |
+
외에도
|
| 402 |
+
요만큼
|
| 403 |
+
요만한 것
|
| 404 |
+
요만한걸
|
| 405 |
+
요컨대
|
| 406 |
+
우르르
|
| 407 |
+
우리
|
| 408 |
+
우리들
|
| 409 |
+
우선
|
| 410 |
+
우에 종합한것과같이
|
| 411 |
+
운운
|
| 412 |
+
월
|
| 413 |
+
위에서 서술한바와같이
|
| 414 |
+
위하여
|
| 415 |
+
위해서
|
| 416 |
+
윙윙
|
| 417 |
+
육
|
| 418 |
+
으로
|
| 419 |
+
으로 인하여
|
| 420 |
+
으로서
|
| 421 |
+
으로써
|
| 422 |
+
을
|
| 423 |
+
응
|
| 424 |
+
응당
|
| 425 |
+
의
|
| 426 |
+
의거하여
|
| 427 |
+
의지하여
|
| 428 |
+
의해
|
| 429 |
+
의해되다
|
| 430 |
+
의해서
|
| 431 |
+
이
|
| 432 |
+
이 되다
|
| 433 |
+
이 때문에
|
| 434 |
+
이 밖에
|
| 435 |
+
이 외에
|
| 436 |
+
이 정도의
|
| 437 |
+
이것
|
| 438 |
+
이곳
|
| 439 |
+
이때
|
| 440 |
+
이라면
|
| 441 |
+
이래
|
| 442 |
+
이러이러하다
|
| 443 |
+
이러한
|
| 444 |
+
이런
|
| 445 |
+
이럴정도로
|
| 446 |
+
이렇게 많은 것
|
| 447 |
+
이렇게되면
|
| 448 |
+
이렇게말하자면
|
| 449 |
+
이렇구나
|
| 450 |
+
이로 인하여
|
| 451 |
+
이르기까지
|
| 452 |
+
이리하여
|
| 453 |
+
이만큼
|
| 454 |
+
이번
|
| 455 |
+
이봐
|
| 456 |
+
이상
|
| 457 |
+
이어서
|
| 458 |
+
이었다
|
| 459 |
+
이와 같다
|
| 460 |
+
이와 같은
|
| 461 |
+
이와 반대로
|
| 462 |
+
이와같다면
|
| 463 |
+
이외에도
|
| 464 |
+
이용하여
|
| 465 |
+
이유만으로
|
| 466 |
+
이젠
|
| 467 |
+
이지만
|
| 468 |
+
이쪽
|
| 469 |
+
이천구
|
| 470 |
+
이천육
|
| 471 |
+
이천칠
|
| 472 |
+
이천팔
|
| 473 |
+
인 듯하다
|
| 474 |
+
인젠
|
| 475 |
+
일
|
| 476 |
+
일것이다
|
| 477 |
+
일곱
|
| 478 |
+
일단
|
| 479 |
+
일때
|
| 480 |
+
일반적으로
|
| 481 |
+
일지라도
|
| 482 |
+
임에 틀림없다
|
| 483 |
+
입각하여
|
| 484 |
+
입장에서
|
| 485 |
+
잇따라
|
| 486 |
+
있다
|
| 487 |
+
자
|
| 488 |
+
자기
|
| 489 |
+
자기집
|
| 490 |
+
자마자
|
| 491 |
+
자신
|
| 492 |
+
잠깐
|
| 493 |
+
잠시
|
| 494 |
+
저
|
| 495 |
+
저것
|
| 496 |
+
저것만큼
|
| 497 |
+
저기
|
| 498 |
+
저쪽
|
| 499 |
+
저희
|
| 500 |
+
전부
|
| 501 |
+
전자
|
| 502 |
+
전후
|
| 503 |
+
점에서 보아
|
| 504 |
+
정도에 이르다
|
| 505 |
+
제
|
| 506 |
+
제각기
|
| 507 |
+
제외하고
|
| 508 |
+
조금
|
| 509 |
+
조차
|
| 510 |
+
조차도
|
| 511 |
+
졸졸
|
| 512 |
+
좀
|
| 513 |
+
좋아
|
| 514 |
+
좍좍
|
| 515 |
+
주룩주룩
|
| 516 |
+
주저하지 않고
|
| 517 |
+
줄은 몰랏다
|
| 518 |
+
줄은모른다
|
| 519 |
+
중에서
|
| 520 |
+
중의하나
|
| 521 |
+
즈음하여
|
| 522 |
+
즉
|
| 523 |
+
즉시
|
| 524 |
+
지든지
|
| 525 |
+
지만
|
| 526 |
+
지말고
|
| 527 |
+
진짜로
|
| 528 |
+
쪽으로
|
| 529 |
+
차라리
|
| 530 |
+
참
|
| 531 |
+
참나
|
| 532 |
+
첫번째로
|
| 533 |
+
쳇
|
| 534 |
+
총적으로
|
| 535 |
+
총적으로 말하면
|
| 536 |
+
총적으로 보면
|
| 537 |
+
칠
|
| 538 |
+
콸콸
|
| 539 |
+
쾅쾅
|
| 540 |
+
쿵
|
| 541 |
+
타다
|
| 542 |
+
타인
|
| 543 |
+
탕탕
|
| 544 |
+
토하다
|
| 545 |
+
통하여
|
| 546 |
+
툭
|
| 547 |
+
퉤
|
| 548 |
+
틈타
|
| 549 |
+
팍
|
| 550 |
+
팔
|
| 551 |
+
퍽
|
| 552 |
+
펄렁
|
| 553 |
+
하
|
| 554 |
+
하게될것이다
|
| 555 |
+
하게하다
|
| 556 |
+
하겠는가
|
| 557 |
+
하고 있다
|
| 558 |
+
하고있었다
|
| 559 |
+
하곤하였다
|
| 560 |
+
하구나
|
| 561 |
+
하기 때문에
|
| 562 |
+
하기 위하여
|
| 563 |
+
하기는한데
|
| 564 |
+
하기만 하면
|
| 565 |
+
하기보다는
|
| 566 |
+
하기에
|
| 567 |
+
하나
|
| 568 |
+
하느니
|
| 569 |
+
하는 김에
|
| 570 |
+
하는 편이 낫다
|
| 571 |
+
하는것도
|
| 572 |
+
하는것만 못하다
|
| 573 |
+
하는것이 낫다
|
| 574 |
+
하는바
|
| 575 |
+
하더라도
|
| 576 |
+
하도다
|
| 577 |
+
하도록시키다
|
| 578 |
+
하도록하다
|
| 579 |
+
하든지
|
| 580 |
+
하려고하다
|
| 581 |
+
하마터면
|
| 582 |
+
하면 할수록
|
| 583 |
+
하면된다
|
| 584 |
+
하면서
|
| 585 |
+
하물며
|
| 586 |
+
하여금
|
| 587 |
+
하여야
|
| 588 |
+
하자마자
|
| 589 |
+
하지 않는다면
|
| 590 |
+
하지 않도록
|
| 591 |
+
하지마
|
| 592 |
+
하지마라
|
| 593 |
+
하지만
|
| 594 |
+
하하
|
| 595 |
+
한 까닭에
|
| 596 |
+
한 이유는
|
| 597 |
+
한 후
|
| 598 |
+
한다면
|
| 599 |
+
한다면 몰라도
|
| 600 |
+
한데
|
| 601 |
+
한마디
|
| 602 |
+
한적이있다
|
| 603 |
+
한켠으로는
|
| 604 |
+
한항목
|
| 605 |
+
할 따름이다
|
| 606 |
+
할 생각이다
|
| 607 |
+
할 줄 안다
|
| 608 |
+
할 지경이다
|
| 609 |
+
할 힘이 있다
|
| 610 |
+
할때
|
| 611 |
+
할만하다
|
| 612 |
+
할망정
|
| 613 |
+
할뿐
|
| 614 |
+
할수있다
|
| 615 |
+
할수있어
|
| 616 |
+
할줄알다
|
| 617 |
+
할지라도
|
| 618 |
+
할지언정
|
| 619 |
+
함께
|
| 620 |
+
해도된다
|
| 621 |
+
해도좋다
|
| 622 |
+
해봐요
|
| 623 |
+
해서는 안된다
|
| 624 |
+
해야한다
|
| 625 |
+
해요
|
| 626 |
+
했어요
|
| 627 |
+
향하다
|
| 628 |
+
향하여
|
| 629 |
+
향해서
|
| 630 |
+
허
|
| 631 |
+
허걱
|
| 632 |
+
허허
|
| 633 |
+
헉
|
| 634 |
+
헉헉
|
| 635 |
+
헐떡헐떡
|
| 636 |
+
형식으로 쓰여
|
| 637 |
+
혹시
|
| 638 |
+
혹은
|
| 639 |
+
혼자
|
| 640 |
+
훨씬
|
| 641 |
+
휘익
|
| 642 |
+
휴
|
| 643 |
+
흐흐
|
| 644 |
+
흥
|
| 645 |
+
힘입어
|
| 646 |
+
︿
|
| 647 |
+
!
|
| 648 |
+
#
|
| 649 |
+
$
|
| 650 |
+
%
|
| 651 |
+
&
|
| 652 |
+
(
|
| 653 |
+
)
|
| 654 |
+
*
|
| 655 |
+
+
|
| 656 |
+
,
|
| 657 |
+
0
|
| 658 |
+
1
|
| 659 |
+
2
|
| 660 |
+
3
|
| 661 |
+
4
|
| 662 |
+
5
|
| 663 |
+
6
|
| 664 |
+
7
|
| 665 |
+
8
|
| 666 |
+
9
|
| 667 |
+
:
|
| 668 |
+
;
|
| 669 |
+
<
|
| 670 |
+
>
|
| 671 |
+
?
|
| 672 |
+
@
|
| 673 |
+
[
|
| 674 |
+
]
|
| 675 |
+
{
|
| 676 |
+
|
|
| 677 |
+
}
|
| 678 |
+
~
|
| 679 |
+
¥
|
util/__pycache__/keywordExtract.cpython-310.pyc
ADDED
|
Binary file (5.66 kB). View file
|
|
|
util/__pycache__/keywordExtract.cpython-311.pyc
ADDED
|
Binary file (10.1 kB). View file
|
|
|
util/keywordExtract.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from transformers import PreTrainedTokenizerFast, BartForConditionalGeneration, AutoTokenizer, AutoModel, AutoModelForSequenceClassification
|
| 2 |
+
from konlpy.tag import Komoran
|
| 3 |
+
from keybert import KeyBERT
|
| 4 |
+
import textwrap
|
| 5 |
+
import os
|
| 6 |
+
import requests
|
| 7 |
+
import torch
|
| 8 |
+
import pandas as pd
|
| 9 |
+
import torch.nn.functional as F
|
| 10 |
+
from transformers import BertTokenizer, BertForSequenceClassification
|
| 11 |
+
|
| 12 |
+
# ✅ 1. 상장기업 목록 불러오기
|
| 13 |
+
def load_company_list(file_path='상장법인목록.xls'):
|
| 14 |
+
|
| 15 |
+
df_list = pd.read_html(file_path)
|
| 16 |
+
df = df_list[0]
|
| 17 |
+
|
| 18 |
+
return df['회사명'].dropna().tolist()
|
| 19 |
+
|
| 20 |
+
# ✅ 요약용 KoBART
|
| 21 |
+
summary_tokenizer = PreTrainedTokenizerFast.from_pretrained("gogamza/kobart-summarization")
|
| 22 |
+
summary_model = BartForConditionalGeneration.from_pretrained("gogamza/kobart-summarization")
|
| 23 |
+
|
| 24 |
+
def summarize_kobart(text):
|
| 25 |
+
input_ids = summary_tokenizer.encode(text, return_tensors="pt")
|
| 26 |
+
summary_ids = summary_model.generate(
|
| 27 |
+
input_ids,
|
| 28 |
+
max_length=160,
|
| 29 |
+
min_length=100,
|
| 30 |
+
num_beams=4,
|
| 31 |
+
repetition_penalty=2.5,
|
| 32 |
+
no_repeat_ngram_size=4,
|
| 33 |
+
early_stopping=True
|
| 34 |
+
)
|
| 35 |
+
return summary_tokenizer.decode(summary_ids[0], skip_special_tokens=True)
|
| 36 |
+
|
| 37 |
+
# ✅ 키워드 추출용 KoBERT
|
| 38 |
+
class KoBERTEmbedding:
|
| 39 |
+
def __init__(self, model, tokenizer):
|
| 40 |
+
self.model = model
|
| 41 |
+
self.tokenizer = tokenizer
|
| 42 |
+
|
| 43 |
+
def encode(self, documents, **kwargs):
|
| 44 |
+
if isinstance(documents, str):
|
| 45 |
+
documents = [documents]
|
| 46 |
+
encoded_input = self.tokenizer(documents, padding=True, truncation=True, return_tensors="pt")
|
| 47 |
+
with torch.no_grad():
|
| 48 |
+
output = self.model(**encoded_input)
|
| 49 |
+
cls_embeddings = output.last_hidden_state[:, 0, :]
|
| 50 |
+
return cls_embeddings.numpy()
|
| 51 |
+
|
| 52 |
+
keyword_model_name = "skt/kobert-base-v1"
|
| 53 |
+
keyword_tokenizer = AutoTokenizer.from_pretrained("skt/kobert-base-v1", use_fast=False)
|
| 54 |
+
keyword_model = AutoModel.from_pretrained(keyword_model_name)
|
| 55 |
+
kobert_embedder = KoBERTEmbedding(keyword_model, keyword_tokenizer)
|
| 56 |
+
kw_model = KeyBERT(model=kobert_embedder)
|
| 57 |
+
|
| 58 |
+
STOPWORDS_FILE = "stopwords-ko.txt"
|
| 59 |
+
|
| 60 |
+
# ✅ 감성 분석용 모델 (예: kykim/bert-kor-base 사용 가정)
|
| 61 |
+
sentiment_model_name = "kykim/bert-kor-base"
|
| 62 |
+
bert_tokenizer = AutoTokenizer.from_pretrained(sentiment_model_name)
|
| 63 |
+
bert_model = AutoModelForSequenceClassification.from_pretrained(sentiment_model_name)
|
| 64 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 65 |
+
bert_model = bert_model.to(device)
|
| 66 |
+
|
| 67 |
+
def classify_emotion(text):
|
| 68 |
+
tokens = bert_tokenizer(text, padding=True, truncation=True, return_tensors="pt").to(device)
|
| 69 |
+
with torch.no_grad():
|
| 70 |
+
prediction = bert_model(**tokens)
|
| 71 |
+
prediction = F.softmax(prediction.logits, dim=1)
|
| 72 |
+
output = prediction.argmax(dim=1).item()
|
| 73 |
+
labels = ["부정적", "중립적", "긍정적"]
|
| 74 |
+
return labels[output]
|
| 75 |
+
|
| 76 |
+
sentiment_tokenizer = BertTokenizer.from_pretrained("kykim/bert-kor-base")
|
| 77 |
+
sentiment_model = BertForSequenceClassification.from_pretrained("kykim/bert-kor-base")
|
| 78 |
+
|
| 79 |
+
def analyze_sentiment(text):
|
| 80 |
+
inputs = sentiment_tokenizer(text, return_tensors="pt", truncation=True, padding=True)
|
| 81 |
+
with torch.no_grad():
|
| 82 |
+
outputs = sentiment_model(**inputs)
|
| 83 |
+
probs = F.softmax(outputs.logits, dim=1)
|
| 84 |
+
return {
|
| 85 |
+
"positive": round(float(probs[0][1]), 4),
|
| 86 |
+
"negative": round(float(probs[0][0]), 4)
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
def get_or_download_stopwords():
|
| 90 |
+
# 1. 파일이 있으면 읽어서 반환
|
| 91 |
+
if os.path.exists(STOPWORDS_FILE):
|
| 92 |
+
with open(STOPWORDS_FILE, "r", encoding="utf-8") as f:
|
| 93 |
+
return [line.strip() for line in f.readlines()]
|
| 94 |
+
|
| 95 |
+
# 2. 파일이 없으면 다운로드 후 저장
|
| 96 |
+
url = "https://raw.githubusercontent.com/stopwords-iso/stopwords-ko/master/stopwords-ko.txt"
|
| 97 |
+
response = requests.get(url)
|
| 98 |
+
stopwords = response.text.splitlines()
|
| 99 |
+
|
| 100 |
+
with open(STOPWORDS_FILE, "w", encoding="utf-8") as f:
|
| 101 |
+
f.write(response.text)
|
| 102 |
+
|
| 103 |
+
return stopwords
|
| 104 |
+
|
| 105 |
+
korean_stopwords = get_or_download_stopwords()
|
| 106 |
+
|
| 107 |
+
# ✅ 형태소 분석기 (komoran) 사용하여 명사 추출
|
| 108 |
+
komoran = Komoran()
|
| 109 |
+
|
| 110 |
+
def remove_stopwords(text, stopwords):
|
| 111 |
+
words = komoran.nouns(text) # Komoran은 복합명사 더 잘 잡음
|
| 112 |
+
filtered_words = [word for word in words if word not in stopwords and len(word) > 1]
|
| 113 |
+
return " ".join(filtered_words)
|
| 114 |
+
|
| 115 |
+
def resultKeyword(content) :
|
| 116 |
+
|
| 117 |
+
company_names = load_company_list()
|
| 118 |
+
|
| 119 |
+
# ✅ 요약
|
| 120 |
+
summary = summarize_kobart(content)
|
| 121 |
+
wrapped_summary = textwrap.fill(summary, width=80) # 80자마다 줄바꿈
|
| 122 |
+
|
| 123 |
+
# ✅ 핵심 키워드 추출
|
| 124 |
+
|
| 125 |
+
# 불용어 처리 후 요약 텍스트에서 키워드 추출
|
| 126 |
+
filtered_summary = remove_stopwords(summary, korean_stopwords)
|
| 127 |
+
keywords = kw_model.extract_keywords(
|
| 128 |
+
filtered_summary,
|
| 129 |
+
keyphrase_ngram_range=(1, 2), # 복합명사 유지 가능
|
| 130 |
+
stop_words=None,
|
| 131 |
+
top_n=5
|
| 132 |
+
)
|
| 133 |
+
# 요약문에서 상장기업명 탐지
|
| 134 |
+
summary_words = set(filtered_summary.split())
|
| 135 |
+
matched_companies = [name for name in company_names if name in summary_words]
|
| 136 |
+
|
| 137 |
+
# 가중치 반영
|
| 138 |
+
weighted_keywords = {}
|
| 139 |
+
for kw, score in keywords:
|
| 140 |
+
if kw in matched_companies:
|
| 141 |
+
weighted_keywords[kw] = score + 0.3
|
| 142 |
+
else:
|
| 143 |
+
weighted_keywords[kw] = score
|
| 144 |
+
|
| 145 |
+
# 기업명 강제 삽입
|
| 146 |
+
for company in matched_companies:
|
| 147 |
+
if company not in weighted_keywords:
|
| 148 |
+
weighted_keywords[company] = 0.9
|
| 149 |
+
|
| 150 |
+
# 1차 키워드 결과 정렬
|
| 151 |
+
sorted_keywords = sorted(weighted_keywords.items(), key=lambda x: x[1], reverse=True)
|
| 152 |
+
top_keywords = sorted_keywords[:5]
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
return {
|
| 158 |
+
"summary": wrapped_summary,
|
| 159 |
+
"keyword": [{"word": kw, "score": float(f"{score:.4f}")} for kw, score in top_keywords]
|
| 160 |
+
}
|
util/상장법인목록.xls
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
상장법인목록.xls
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|