Leesn465 commited on
Commit
748bd71
·
1 Parent(s): ac3f5d2

FastAPI for Hugging Face Space: initial setup and files

Browse files
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
+
658
+
659
+
660
+
661
+
662
+
663
+
664
+
665
+
666
+
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