Spaces:
Building
Building
Update app.py
Browse files
app.py
CHANGED
@@ -3,7 +3,7 @@ import requests
|
|
3 |
import json
|
4 |
import os
|
5 |
from datetime import datetime, timedelta
|
6 |
-
from concurrent.futures import ThreadPoolExecutor
|
7 |
from functools import lru_cache
|
8 |
from requests.adapters import HTTPAdapter
|
9 |
from requests.packages.urllib3.util.retry import Retry
|
@@ -45,6 +45,9 @@ def convert_to_seoul_time(timestamp_str):
|
|
45 |
return timestamp_str
|
46 |
|
47 |
def analyze_sentiment_batch(articles, client):
|
|
|
|
|
|
|
48 |
try:
|
49 |
# 모든 기사의 제목과 내용을 하나의 텍스트로 결합
|
50 |
combined_text = "\n\n".join([
|
@@ -52,7 +55,6 @@ def analyze_sentiment_batch(articles, client):
|
|
52 |
for article in articles
|
53 |
])
|
54 |
|
55 |
-
# f""" ... """ 형태로 여러 줄 문자열을 정확히 사용
|
56 |
prompt = f"""다음 뉴스 모음에 대해 전반적인 감성 분석을 수행하세요:
|
57 |
|
58 |
뉴스 내용:
|
@@ -96,6 +98,9 @@ def init_db():
|
|
96 |
conn.close()
|
97 |
|
98 |
def save_to_db(keyword, country, results):
|
|
|
|
|
|
|
99 |
conn = sqlite3.connect("search_results.db")
|
100 |
c = conn.cursor()
|
101 |
seoul_tz = pytz.timezone('Asia/Seoul')
|
@@ -110,6 +115,9 @@ def save_to_db(keyword, country, results):
|
|
110 |
conn.close()
|
111 |
|
112 |
def load_from_db(keyword, country):
|
|
|
|
|
|
|
113 |
conn = sqlite3.connect("search_results.db")
|
114 |
c = conn.cursor()
|
115 |
c.execute("SELECT results, timestamp FROM searches WHERE keyword=? AND country=? ORDER BY timestamp DESC LIMIT 1",
|
@@ -121,6 +129,9 @@ def load_from_db(keyword, country):
|
|
121 |
return None, None
|
122 |
|
123 |
def display_results(articles):
|
|
|
|
|
|
|
124 |
output = ""
|
125 |
for idx, article in enumerate(articles, 1):
|
126 |
output += f"### {idx}. {article['title']}\n"
|
@@ -131,6 +142,9 @@ def display_results(articles):
|
|
131 |
return output
|
132 |
|
133 |
def search_company(company):
|
|
|
|
|
|
|
134 |
error_message, articles = serphouse_search(company, "United States")
|
135 |
if not error_message and articles:
|
136 |
save_to_db(company, "United States", articles)
|
@@ -138,17 +152,29 @@ def search_company(company):
|
|
138 |
return f"{company}에 대한 검색 결과가 없습니다."
|
139 |
|
140 |
def load_company(company):
|
|
|
|
|
|
|
141 |
results, timestamp = load_from_db(company, "United States")
|
142 |
if results:
|
143 |
return f"### {company} 검색 결과\n저장 시간: {timestamp}\n\n" + display_results(results)
|
144 |
return f"{company}에 대한 저장된 결과가 없습니다."
|
145 |
|
146 |
def show_stats():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
147 |
conn = sqlite3.connect("search_results.db")
|
148 |
c = conn.cursor()
|
149 |
|
150 |
output = "## 한국 기업 뉴스 분석 리포트\n\n"
|
151 |
|
|
|
|
|
152 |
for company in KOREAN_COMPANIES:
|
153 |
c.execute("""
|
154 |
SELECT results, timestamp
|
@@ -158,36 +184,70 @@ def show_stats():
|
|
158 |
LIMIT 1
|
159 |
""", (company,))
|
160 |
|
161 |
-
|
162 |
-
if
|
163 |
-
results_json, timestamp =
|
164 |
articles = json.loads(results_json)
|
165 |
seoul_time = convert_to_seoul_time(timestamp)
|
166 |
-
|
167 |
-
output += f"### {company}\n"
|
168 |
-
output += f"- 마지막 업데이트: {seoul_time}\n"
|
169 |
-
output += f"- 저장된 기사 수: {len(articles)}건\n\n"
|
170 |
-
|
171 |
-
if articles:
|
172 |
-
sentiment_analysis = analyze_sentiment_batch(articles, client)
|
173 |
-
output += "#### 뉴스 감성 분석\n"
|
174 |
-
output += f"{sentiment_analysis}\n\n"
|
175 |
-
|
176 |
-
output += "---\n\n"
|
177 |
|
178 |
conn.close()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
179 |
return output
|
180 |
|
|
|
|
|
181 |
def search_all_companies():
|
|
|
|
|
|
|
|
|
182 |
overall_result = "# [전체 검색 결과]\n\n"
|
183 |
-
|
184 |
-
|
185 |
-
|
186 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
187 |
return overall_result
|
188 |
|
189 |
def load_all_companies():
|
|
|
|
|
|
|
|
|
190 |
overall_result = "# [전체 출력 결과]\n\n"
|
|
|
191 |
for comp in KOREAN_COMPANIES:
|
192 |
overall_result += f"## {comp}\n"
|
193 |
overall_result += load_company(comp)
|
@@ -195,17 +255,23 @@ def load_all_companies():
|
|
195 |
return overall_result
|
196 |
|
197 |
def full_summary_report():
|
198 |
-
|
|
|
|
|
|
|
|
|
199 |
search_result_text = search_all_companies()
|
200 |
-
|
|
|
201 |
load_result_text = load_all_companies()
|
202 |
-
|
|
|
203 |
stats_text = show_stats()
|
204 |
|
205 |
combined_report = (
|
206 |
"# 전체 분석 보고 요약\n\n"
|
207 |
"아래 순서로 실행되었습니다:\n"
|
208 |
-
"1. 모든 종목 검색 → 2. 모든 종목 DB 결과 출력 → 3. 전체 감성 분석 통계\n\n"
|
209 |
f"{search_result_text}\n\n"
|
210 |
f"{load_result_text}\n\n"
|
211 |
"## [전체 감성 분석 통계]\n\n"
|
@@ -213,6 +279,47 @@ def full_summary_report():
|
|
213 |
)
|
214 |
return combined_report
|
215 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
216 |
ACCESS_TOKEN = os.getenv("HF_TOKEN")
|
217 |
if not ACCESS_TOKEN:
|
218 |
raise ValueError("HF_TOKEN environment variable is not set")
|
@@ -224,6 +331,8 @@ client = OpenAI(
|
|
224 |
|
225 |
API_KEY = os.getenv("SERPHOUSE_API_KEY")
|
226 |
|
|
|
|
|
227 |
COUNTRY_LANGUAGES = {
|
228 |
"United States": "en",
|
229 |
"KOREA": "ko",
|
@@ -366,8 +475,12 @@ COUNTRY_LOCATIONS = {
|
|
366 |
"Iceland": "Iceland"
|
367 |
}
|
368 |
|
|
|
369 |
@lru_cache(maxsize=100)
|
370 |
def translate_query(query, country):
|
|
|
|
|
|
|
371 |
try:
|
372 |
if is_english(query):
|
373 |
return query
|
@@ -404,6 +517,10 @@ def is_english(text):
|
|
404 |
return all(ord(char) < 128 for char in text.replace(' ', '').replace('-', '').replace('_', ''))
|
405 |
|
406 |
def search_serphouse(query, country, page=1, num_result=10):
|
|
|
|
|
|
|
|
|
407 |
url = "https://api.serphouse.com/serp/live"
|
408 |
|
409 |
now = datetime.utcnow()
|
@@ -420,7 +537,7 @@ def search_serphouse(query, country, page=1, num_result=10):
|
|
420 |
"lang": COUNTRY_LANGUAGES.get(country, "en"),
|
421 |
"device": "desktop",
|
422 |
"serp_type": "news",
|
423 |
-
"page":
|
424 |
"num": "100",
|
425 |
"date_range": date_range,
|
426 |
"sort_by": "date"
|
@@ -474,6 +591,10 @@ def search_serphouse(query, country, page=1, num_result=10):
|
|
474 |
}
|
475 |
|
476 |
def format_results_from_raw(response_data):
|
|
|
|
|
|
|
|
|
477 |
if "error" in response_data:
|
478 |
return "Error: " + response_data["error"], []
|
479 |
|
@@ -481,10 +602,12 @@ def format_results_from_raw(response_data):
|
|
481 |
results = response_data["results"]
|
482 |
translated_query = response_data["translated_query"]
|
483 |
|
|
|
484 |
news_results = results.get('results', {}).get('results', {}).get('news', [])
|
485 |
if not news_results:
|
486 |
return "검색 결과가 없습니다.", []
|
487 |
|
|
|
488 |
korean_domains = [
|
489 |
'.kr', 'korea', 'korean', 'yonhap', 'hankyung', 'chosun',
|
490 |
'donga', 'joins', 'hani', 'koreatimes', 'koreaherald'
|
@@ -502,9 +625,10 @@ def format_results_from_raw(response_data):
|
|
502 |
|
503 |
is_korean_content = (
|
504 |
any(domain in url or domain in channel for domain in korean_domains) or
|
505 |
-
any(keyword in title
|
506 |
)
|
507 |
|
|
|
508 |
if not is_korean_content:
|
509 |
filtered_articles.append({
|
510 |
"index": idx,
|
@@ -522,9 +646,14 @@ def format_results_from_raw(response_data):
|
|
522 |
return f"결과 처리 중 오류 발생: {str(e)}", []
|
523 |
|
524 |
def serphouse_search(query, country):
|
|
|
|
|
|
|
525 |
response_data = search_serphouse(query, country)
|
526 |
return format_results_from_raw(response_data)
|
527 |
|
|
|
|
|
528 |
css = """
|
529 |
/* 전역 스타일 */
|
530 |
footer {visibility: hidden;}
|
@@ -700,15 +829,44 @@ footer {visibility: hidden;}
|
|
700 |
}
|
701 |
"""
|
702 |
|
|
|
|
|
703 |
with gr.Blocks(theme="Yntec/HaleyCH_Theme_Orange", css=css, title="NewsAI 서비스") as iface:
|
704 |
init_db()
|
705 |
|
706 |
with gr.Tabs():
|
707 |
-
# 첫 번째 탭
|
708 |
with gr.Tab("Earnbot"):
|
709 |
gr.Markdown("## EarnBot: 글로벌 빅테크 기업 및 투자 전망 AI 자동 분석")
|
710 |
-
gr.Markdown(" '전체 분석 보고 요약'
|
711 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
712 |
with gr.Row():
|
713 |
full_report_btn = gr.Button("전체 분석 보고 요약", variant="primary")
|
714 |
full_report_display = gr.Markdown()
|
@@ -718,6 +876,7 @@ with gr.Blocks(theme="Yntec/HaleyCH_Theme_Orange", css=css, title="NewsAI 서비
|
|
718 |
outputs=full_report_display
|
719 |
)
|
720 |
|
|
|
721 |
with gr.Column():
|
722 |
for i in range(0, len(KOREAN_COMPANIES), 2):
|
723 |
with gr.Row():
|
|
|
3 |
import json
|
4 |
import os
|
5 |
from datetime import datetime, timedelta
|
6 |
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
7 |
from functools import lru_cache
|
8 |
from requests.adapters import HTTPAdapter
|
9 |
from requests.packages.urllib3.util.retry import Retry
|
|
|
45 |
return timestamp_str
|
46 |
|
47 |
def analyze_sentiment_batch(articles, client):
|
48 |
+
"""
|
49 |
+
OpenAI API를 통해 뉴스 기사들의 종합 감성 분석을 수행
|
50 |
+
"""
|
51 |
try:
|
52 |
# 모든 기사의 제목과 내용을 하나의 텍스트로 결합
|
53 |
combined_text = "\n\n".join([
|
|
|
55 |
for article in articles
|
56 |
])
|
57 |
|
|
|
58 |
prompt = f"""다음 뉴스 모음에 대해 전반적인 감성 분석을 수행하세요:
|
59 |
|
60 |
뉴스 내용:
|
|
|
98 |
conn.close()
|
99 |
|
100 |
def save_to_db(keyword, country, results):
|
101 |
+
"""
|
102 |
+
특정 (keyword, country) 조합에 대한 검색 결과를 DB에 저장
|
103 |
+
"""
|
104 |
conn = sqlite3.connect("search_results.db")
|
105 |
c = conn.cursor()
|
106 |
seoul_tz = pytz.timezone('Asia/Seoul')
|
|
|
115 |
conn.close()
|
116 |
|
117 |
def load_from_db(keyword, country):
|
118 |
+
"""
|
119 |
+
특정 (keyword, country) 조합에 대한 가장 최근 검색 결과를 DB에서 불러오기
|
120 |
+
"""
|
121 |
conn = sqlite3.connect("search_results.db")
|
122 |
c = conn.cursor()
|
123 |
c.execute("SELECT results, timestamp FROM searches WHERE keyword=? AND country=? ORDER BY timestamp DESC LIMIT 1",
|
|
|
129 |
return None, None
|
130 |
|
131 |
def display_results(articles):
|
132 |
+
"""
|
133 |
+
뉴스 기사 목록을 Markdown 문자열로 변환하여 반환
|
134 |
+
"""
|
135 |
output = ""
|
136 |
for idx, article in enumerate(articles, 1):
|
137 |
output += f"### {idx}. {article['title']}\n"
|
|
|
142 |
return output
|
143 |
|
144 |
def search_company(company):
|
145 |
+
"""
|
146 |
+
단일 기업(또는 키워드)에 대해 미국 뉴스 검색, DB 저장 후 결과 Markdown 반환
|
147 |
+
"""
|
148 |
error_message, articles = serphouse_search(company, "United States")
|
149 |
if not error_message and articles:
|
150 |
save_to_db(company, "United States", articles)
|
|
|
152 |
return f"{company}에 대한 검색 결과가 없습니다."
|
153 |
|
154 |
def load_company(company):
|
155 |
+
"""
|
156 |
+
DB에서 단일 기업(또는 키워드)의 미국 뉴스 검색 결과를 불러와 Markdown 반환
|
157 |
+
"""
|
158 |
results, timestamp = load_from_db(company, "United States")
|
159 |
if results:
|
160 |
return f"### {company} 검색 결과\n저장 시간: {timestamp}\n\n" + display_results(results)
|
161 |
return f"{company}에 대한 저장된 결과가 없습니다."
|
162 |
|
163 |
def show_stats():
|
164 |
+
"""
|
165 |
+
KOREAN_COMPANIES 목록 내 모든 기업에 대해:
|
166 |
+
- 가장 최근 DB 저장 일자
|
167 |
+
- 기사 수
|
168 |
+
- 감성 분석 결과
|
169 |
+
를 순차(또는 병렬)로 조회하여 보고서 형태로 반환
|
170 |
+
"""
|
171 |
conn = sqlite3.connect("search_results.db")
|
172 |
c = conn.cursor()
|
173 |
|
174 |
output = "## 한국 기업 뉴스 분석 리포트\n\n"
|
175 |
|
176 |
+
# 모든 기업에 대해 DB에서 읽어올 (company, timestamp, articles) 목록 수집
|
177 |
+
data_list = []
|
178 |
for company in KOREAN_COMPANIES:
|
179 |
c.execute("""
|
180 |
SELECT results, timestamp
|
|
|
184 |
LIMIT 1
|
185 |
""", (company,))
|
186 |
|
187 |
+
row = c.fetchone()
|
188 |
+
if row:
|
189 |
+
results_json, timestamp = row
|
190 |
articles = json.loads(results_json)
|
191 |
seoul_time = convert_to_seoul_time(timestamp)
|
192 |
+
data_list.append((company, seoul_time, articles))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
193 |
|
194 |
conn.close()
|
195 |
+
|
196 |
+
# (옵션) 각 기업 감성 분석을 병렬 처리
|
197 |
+
def analyze_data(item):
|
198 |
+
comp, tstamp, arts = item
|
199 |
+
sentiment = ""
|
200 |
+
if arts:
|
201 |
+
sentiment = analyze_sentiment_batch(arts, client)
|
202 |
+
return (comp, tstamp, len(arts), sentiment)
|
203 |
+
|
204 |
+
# ThreadPoolExecutor로 병렬 감성 분석
|
205 |
+
results_list = []
|
206 |
+
with ThreadPoolExecutor(max_workers=5) as executor:
|
207 |
+
futures = [executor.submit(analyze_data, dl) for dl in data_list]
|
208 |
+
for future in as_completed(futures):
|
209 |
+
results_list.append(future.result())
|
210 |
+
|
211 |
+
# 결과 정렬(원하는 순서대로) - 여기서는 기업명 기준 or 그냥 순서 없음
|
212 |
+
for comp, tstamp, count, sentiment in results_list:
|
213 |
+
output += f"### {comp}\n"
|
214 |
+
output += f"- 마지막 업데이트: {tstamp}\n"
|
215 |
+
output += f"- 저장된 기사 수: {count}건\n\n"
|
216 |
+
if sentiment:
|
217 |
+
output += "#### 뉴스 감성 분석\n"
|
218 |
+
output += f"{sentiment}\n\n"
|
219 |
+
output += "---\n\n"
|
220 |
+
|
221 |
return output
|
222 |
|
223 |
+
|
224 |
+
### (1) 전체 검색: 멀티스레드 적용
|
225 |
def search_all_companies():
|
226 |
+
"""
|
227 |
+
KOREAN_COMPANIES 리스트 내 모든 기업에 대해,
|
228 |
+
검색을 병렬(쓰레드)로 수행 후 결과를 합쳐 Markdown 형태로 반환
|
229 |
+
"""
|
230 |
overall_result = "# [전체 검색 결과]\n\n"
|
231 |
+
|
232 |
+
def do_search(comp):
|
233 |
+
return comp, search_company(comp)
|
234 |
+
|
235 |
+
with ThreadPoolExecutor(max_workers=5) as executor:
|
236 |
+
futures = [executor.submit(do_search, c) for c in KOREAN_COMPANIES]
|
237 |
+
for future in as_completed(futures):
|
238 |
+
comp, res_text = future.result()
|
239 |
+
overall_result += f"## {comp}\n"
|
240 |
+
overall_result += res_text + "\n\n"
|
241 |
+
|
242 |
return overall_result
|
243 |
|
244 |
def load_all_companies():
|
245 |
+
"""
|
246 |
+
KOREAN_COMPANIES 리스트 내 모든 기업에 대해,
|
247 |
+
DB에서 불러온 결과를 순차(또는 병렬)로 합쳐서 Markdown 형태로 반환
|
248 |
+
"""
|
249 |
overall_result = "# [전체 출력 결과]\n\n"
|
250 |
+
|
251 |
for comp in KOREAN_COMPANIES:
|
252 |
overall_result += f"## {comp}\n"
|
253 |
overall_result += load_company(comp)
|
|
|
255 |
return overall_result
|
256 |
|
257 |
def full_summary_report():
|
258 |
+
"""
|
259 |
+
(1) 모든 기업 검색 -> (2) DB에서 모든 기업 불러오기 -> (3) 감성 분석 통계
|
260 |
+
순서대로 실행하여, 전체 리포트를 합쳐 반환
|
261 |
+
"""
|
262 |
+
# 1) 전체 검색(병렬)
|
263 |
search_result_text = search_all_companies()
|
264 |
+
|
265 |
+
# 2) 전체 출력(순차)
|
266 |
load_result_text = load_all_companies()
|
267 |
+
|
268 |
+
# 3) 전체 통계(감성 분석)
|
269 |
stats_text = show_stats()
|
270 |
|
271 |
combined_report = (
|
272 |
"# 전체 분석 보고 요약\n\n"
|
273 |
"아래 순서로 실행되었습니다:\n"
|
274 |
+
"1. 모든 종목 검색(병렬) → 2. 모든 종목 DB 결과 출력 → 3. 전체 감성 분석 통계\n\n"
|
275 |
f"{search_result_text}\n\n"
|
276 |
f"{load_result_text}\n\n"
|
277 |
"## [전체 감성 분석 통계]\n\n"
|
|
|
279 |
)
|
280 |
return combined_report
|
281 |
|
282 |
+
|
283 |
+
### (2) 사용자 임의 검색 + 국가 선택 기능
|
284 |
+
def search_custom(query, country):
|
285 |
+
"""
|
286 |
+
사용자가 입력한 (query, country)를 대상으로
|
287 |
+
- 검색 (API 요청)
|
288 |
+
- DB 저장
|
289 |
+
- DB 로드 후 감성 분석
|
290 |
+
- 최종 결과를 Markdown 형태로 반환
|
291 |
+
"""
|
292 |
+
# 1) 검색
|
293 |
+
error_message, articles = serphouse_search(query, country)
|
294 |
+
if error_message:
|
295 |
+
return f"오류 발생: {error_message}"
|
296 |
+
if not articles:
|
297 |
+
return "검색 결과가 없습니다."
|
298 |
+
|
299 |
+
# 2) DB 저장
|
300 |
+
save_to_db(query, country, articles)
|
301 |
+
|
302 |
+
# 3) DB에서 다시 불러오기
|
303 |
+
results, timestamp = load_from_db(query, country)
|
304 |
+
if not results:
|
305 |
+
return f"DB 로드 실패: 저장된 결과가 없습니다."
|
306 |
+
|
307 |
+
# 4) 감성 분석
|
308 |
+
sentiment_analysis = analyze_sentiment_batch(results, client)
|
309 |
+
|
310 |
+
# 5) 최종 리포트(기사 목록 + 감성 분석)
|
311 |
+
output = f"## [사용자 임의 검색 결과]\n\n"
|
312 |
+
output += f"**키워드**: {query}\n\n"
|
313 |
+
output += f"**국가**: {country}\n\n"
|
314 |
+
output += f"**저장 시간**: {timestamp}\n\n"
|
315 |
+
output += display_results(results)
|
316 |
+
|
317 |
+
output += "### 뉴스 감성 분석\n"
|
318 |
+
output += f"{sentiment_analysis}\n"
|
319 |
+
return output
|
320 |
+
|
321 |
+
|
322 |
+
### (필수) API 인증
|
323 |
ACCESS_TOKEN = os.getenv("HF_TOKEN")
|
324 |
if not ACCESS_TOKEN:
|
325 |
raise ValueError("HF_TOKEN environment variable is not set")
|
|
|
331 |
|
332 |
API_KEY = os.getenv("SERPHOUSE_API_KEY")
|
333 |
|
334 |
+
|
335 |
+
### 국가별 설정
|
336 |
COUNTRY_LANGUAGES = {
|
337 |
"United States": "en",
|
338 |
"KOREA": "ko",
|
|
|
475 |
"Iceland": "Iceland"
|
476 |
}
|
477 |
|
478 |
+
|
479 |
@lru_cache(maxsize=100)
|
480 |
def translate_query(query, country):
|
481 |
+
"""
|
482 |
+
Google Translation API(비공식) 사용하여 검색어를 해당 국가 언어로 번역
|
483 |
+
"""
|
484 |
try:
|
485 |
if is_english(query):
|
486 |
return query
|
|
|
517 |
return all(ord(char) < 128 for char in text.replace(' ', '').replace('-', '').replace('_', ''))
|
518 |
|
519 |
def search_serphouse(query, country, page=1, num_result=10):
|
520 |
+
"""
|
521 |
+
SerpHouse API에 실시간 검색 요청을 보내어,
|
522 |
+
'뉴스' 탭 (sort_by=date)에서 해당 query에 대한 기사 목록을 가져온다.
|
523 |
+
"""
|
524 |
url = "https://api.serphouse.com/serp/live"
|
525 |
|
526 |
now = datetime.utcnow()
|
|
|
537 |
"lang": COUNTRY_LANGUAGES.get(country, "en"),
|
538 |
"device": "desktop",
|
539 |
"serp_type": "news",
|
540 |
+
"page": str(page),
|
541 |
"num": "100",
|
542 |
"date_range": date_range,
|
543 |
"sort_by": "date"
|
|
|
591 |
}
|
592 |
|
593 |
def format_results_from_raw(response_data):
|
594 |
+
"""
|
595 |
+
SerpHouse API의 응답 데이터를 가공하여,
|
596 |
+
(에러메시지, 기사리스트) 형태로 반환.
|
597 |
+
"""
|
598 |
if "error" in response_data:
|
599 |
return "Error: " + response_data["error"], []
|
600 |
|
|
|
602 |
results = response_data["results"]
|
603 |
translated_query = response_data["translated_query"]
|
604 |
|
605 |
+
# 실제 뉴스 결과
|
606 |
news_results = results.get('results', {}).get('results', {}).get('news', [])
|
607 |
if not news_results:
|
608 |
return "검색 결과가 없습니다.", []
|
609 |
|
610 |
+
# 한국 도메인 및 한국 관련 키워드 포함 기사 제외
|
611 |
korean_domains = [
|
612 |
'.kr', 'korea', 'korean', 'yonhap', 'hankyung', 'chosun',
|
613 |
'donga', 'joins', 'hani', 'koreatimes', 'koreaherald'
|
|
|
625 |
|
626 |
is_korean_content = (
|
627 |
any(domain in url or domain in channel for domain in korean_domains) or
|
628 |
+
any(keyword in title for keyword in korean_keywords)
|
629 |
)
|
630 |
|
631 |
+
# 한국어 뉴스(또는 한국 도메인) 제외
|
632 |
if not is_korean_content:
|
633 |
filtered_articles.append({
|
634 |
"index": idx,
|
|
|
646 |
return f"결과 처리 중 오류 발생: {str(e)}", []
|
647 |
|
648 |
def serphouse_search(query, country):
|
649 |
+
"""
|
650 |
+
검색 및 결과 포매팅까지 일괄 처리
|
651 |
+
"""
|
652 |
response_data = search_serphouse(query, country)
|
653 |
return format_results_from_raw(response_data)
|
654 |
|
655 |
+
|
656 |
+
# CSS (UI 커스터마이징)
|
657 |
css = """
|
658 |
/* 전역 스타일 */
|
659 |
footer {visibility: hidden;}
|
|
|
829 |
}
|
830 |
"""
|
831 |
|
832 |
+
import gradio as gr
|
833 |
+
|
834 |
with gr.Blocks(theme="Yntec/HaleyCH_Theme_Orange", css=css, title="NewsAI 서비스") as iface:
|
835 |
init_db()
|
836 |
|
837 |
with gr.Tabs():
|
838 |
+
# 첫 번째 탭
|
839 |
with gr.Tab("Earnbot"):
|
840 |
gr.Markdown("## EarnBot: 글로벌 빅테크 기업 및 투자 전망 AI 자동 분석")
|
841 |
+
gr.Markdown(" * '전체 분석 보고 요약' 클릭 시 전체 자동 보고 생성.\n * 아래 개별 종목의 '검색(DB 자동 저장)'과 '출력(DB 자동 호출)'도 가능.\n * 추가로, 원하는 임의 키워드 및 국가로 검색/분석할 수도 있습니다.")
|
842 |
|
843 |
+
# (2) 사용자 임의 검색 섹션
|
844 |
+
with gr.Group():
|
845 |
+
gr.Markdown("### 사용자 임의 검색")
|
846 |
+
with gr.Row():
|
847 |
+
with gr.Column():
|
848 |
+
user_input = gr.Textbox(
|
849 |
+
label="검색어 입력",
|
850 |
+
placeholder="예) Apple, Samsung 등 자유롭게"
|
851 |
+
)
|
852 |
+
with gr.Column():
|
853 |
+
country_selection = gr.Dropdown(
|
854 |
+
choices=list(COUNTRY_LOCATIONS.keys()),
|
855 |
+
value="United States",
|
856 |
+
label="국가 선택"
|
857 |
+
)
|
858 |
+
with gr.Column():
|
859 |
+
custom_search_btn = gr.Button("실행", variant="primary")
|
860 |
+
|
861 |
+
custom_search_output = gr.Markdown()
|
862 |
+
|
863 |
+
custom_search_btn.click(
|
864 |
+
fn=search_custom,
|
865 |
+
inputs=[user_input, country_selection],
|
866 |
+
outputs=custom_search_output
|
867 |
+
)
|
868 |
+
|
869 |
+
# 전체 분석 보고 요약 버튼
|
870 |
with gr.Row():
|
871 |
full_report_btn = gr.Button("전체 분석 보고 요약", variant="primary")
|
872 |
full_report_display = gr.Markdown()
|
|
|
876 |
outputs=full_report_display
|
877 |
)
|
878 |
|
879 |
+
# 기존 개별 기업 검색/출력 영역
|
880 |
with gr.Column():
|
881 |
for i in range(0, len(KOREAN_COMPANIES), 2):
|
882 |
with gr.Row():
|