khjhs60199 commited on
Commit
0105605
·
verified ·
1 Parent(s): 7645752

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +278 -27
app.py CHANGED
@@ -8,6 +8,8 @@ import time
8
  from datetime import datetime, timedelta
9
  from typing import List, Dict, Optional
10
  import os
 
 
11
 
12
  from crawler import CnYesNewsCrawler
13
  from sentiment_analyzer import SentimentAnalyzer
@@ -19,18 +21,40 @@ from utils import setup_logging, format_news_for_display
19
  setup_logging()
20
  logger = logging.getLogger(__name__)
21
 
 
 
 
22
  class NewsApp:
23
  def __init__(self):
24
  self.db = NewsDatabase()
25
- self.crawler = CnYesNewsCrawler()
26
  self.sentiment_analyzer = SentimentAnalyzer()
 
 
 
 
27
  self.scheduler = NewsScheduler(self.db, self.crawler, self.sentiment_analyzer)
28
 
 
 
 
 
 
 
 
29
  # 啟動背景排程器
30
  self.scheduler.start()
31
 
32
  logger.info("新聞應用程式初始化完成")
33
 
 
 
 
 
 
 
 
 
 
34
  def get_latest_news(self, category: str = "all", limit: int = 50) -> str:
35
  """獲取最新新聞並格式化顯示"""
36
  try:
@@ -46,14 +70,28 @@ class NewsApp:
46
 
47
  def manual_crawl(self) -> str:
48
  """手動觸發爬蟲"""
 
 
 
49
  try:
50
- logger.info("🚀 手動觸發爬蟲開始")
51
- result = self.scheduler.run_crawl_task()
52
- logger.info(f"✅ 手動爬蟲完成: {result}")
53
- return f"✅ 手動爬蟲完成: {result}"
 
 
 
 
 
 
 
 
54
  except Exception as e:
55
- logger.error(f"❌ 手動爬蟲錯誤: {e}")
56
- return f"❌ 手動爬蟲失敗: {str(e)}"
 
 
 
57
 
58
  def get_statistics(self) -> str:
59
  """獲取統計資訊"""
@@ -64,18 +102,113 @@ class NewsApp:
64
  - 總新聞數量: {stats.get('total_news', 0)}
65
  - 美股新聞: {stats.get('us_stock_count', 0)}
66
  - 台股新聞: {stats.get('tw_stock_count', 0)}
67
- - 正面新聞: {stats.get('positive_count', 0)}
68
- - 負面新聞: {stats.get('negative_count', 0)}
69
- - 中性新聞: {stats.get('neutral_count', 0)}
70
  - 最後更新: {stats.get('last_update', 'N/A')}
71
  """
72
  except Exception as e:
73
  logger.error(f"獲取統計資訊錯誤: {e}")
74
  return f"❌ 獲取統計資訊失敗: {str(e)}"
75
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  # 初始化應用
77
  app = NewsApp()
78
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  # 創建 Gradio 介面
80
  def create_interface():
81
  with gr.Blocks(
@@ -90,23 +223,24 @@ def create_interface():
90
  .positive-badge { background: #28a745; color: white; }
91
  .negative-badge { background: #dc3545; color: white; }
92
  .neutral-badge { background: #6c757d; color: white; }
 
93
  """
94
  ) as interface:
95
 
96
  gr.Markdown("""
97
- # 📈 股市新聞情緒分析器
98
 
99
- 🤖 自動爬取鉅亨網美股和台股新聞,並進行中文情緒分析
100
 
101
- **自動更新**: 每30分鐘自動爬取最新新聞
102
  🎯 **智能分析**: 使用 RoBERTa 模型進行情緒分析
103
  🔄 **去重處理**: 自動過濾重複新聞
104
- 📅 **資料保留**: 保存兩週內的新聞資料
105
  """)
106
 
107
  with gr.Tab("📰 最新新聞"):
108
  with gr.Row():
109
- with gr.Column(scale=3):
110
  category_radio = gr.Radio(
111
  choices=["all", "us_stock", "tw_stock"],
112
  value="all",
@@ -117,10 +251,21 @@ def create_interface():
117
  refresh_btn = gr.Button("🔄 重新整理", variant="primary")
118
  manual_crawl_btn = gr.Button("🚀 手動爬取", variant="secondary")
119
 
 
 
 
 
 
 
 
 
120
  news_display = gr.HTML(label="新聞內容")
121
  crawl_result = gr.Textbox(label="爬取結果", visible=False)
122
 
123
- # 自動重新整理
 
 
 
124
  def auto_refresh():
125
  return app.get_latest_news("all")
126
 
@@ -133,6 +278,13 @@ def create_interface():
133
  news = app.get_latest_news("all")
134
  return result, news
135
 
 
 
 
 
 
 
 
136
  # 綁定事件
137
  refresh_btn.click(refresh_news, inputs=[category_radio], outputs=[news_display])
138
  manual_crawl_btn.click(
@@ -143,9 +295,6 @@ def create_interface():
143
  outputs=[crawl_result]
144
  )
145
  category_radio.change(refresh_news, inputs=[category_radio], outputs=[news_display])
146
-
147
- # 初始載入
148
- interface.load(auto_refresh, outputs=[news_display])
149
 
150
  with gr.Tab("📊 統計資訊"):
151
  stats_display = gr.Markdown()
@@ -154,10 +303,97 @@ def create_interface():
154
  stats_refresh_btn.click(app.get_statistics, outputs=[stats_display])
155
  interface.load(app.get_statistics, outputs=[stats_display])
156
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
  with gr.Tab("ℹ️ 關於"):
158
  gr.Markdown("""
159
  ## 🛠️ 技術特色
160
 
 
 
 
 
 
 
161
  ### 📊 情緒分析
162
  - **模型**: `uer/roberta-base-finetuned-jd-binary-chinese`
163
  - **分類**: 正面 (綠色) / 負面 (紅色) / 中性 (灰色)
@@ -167,27 +403,42 @@ def create_interface():
167
  - **來源**: 鉅亨網 (cnyes.com)
168
  - **分類**: 美股、台股新聞
169
  - **頻率**: 每30分鐘自動更新
170
- - **去重**: 基於標題相似度智能去重
171
 
172
  ### 💾 資料管理
173
- - **儲存**: SQLite 本地資料庫
 
174
  - **保留期**: 自動清理兩週前的資料
175
- - **效能**: 索引優化,快速查詢
176
 
177
- ### 🔧 系統功能
178
- - **反爬蟲**: 隨機延遲、User-Agent 輪換
179
- - **錯誤處理**: 完整的異常捕獲和日誌記錄
180
- - **監控**: 即時統計和狀態監控
 
181
 
182
  ---
183
 
184
- 💡 **提示**: 首次啟動可能需要幾分鐘下載模型和初始化資料庫
 
 
 
185
  """)
186
 
187
  return interface
188
 
189
  # 啟動應用
190
  if __name__ == "__main__":
 
 
 
 
 
 
 
 
 
 
191
  interface = create_interface()
192
  interface.launch(
193
  server_name="0.0.0.0",
 
8
  from datetime import datetime, timedelta
9
  from typing import List, Dict, Optional
10
  import os
11
+ from flask import Flask, jsonify, request
12
+ import json
13
 
14
  from crawler import CnYesNewsCrawler
15
  from sentiment_analyzer import SentimentAnalyzer
 
21
  setup_logging()
22
  logger = logging.getLogger(__name__)
23
 
24
+ # Flask API 應用
25
+ flask_app = Flask(__name__)
26
+
27
  class NewsApp:
28
  def __init__(self):
29
  self.db = NewsDatabase()
 
30
  self.sentiment_analyzer = SentimentAnalyzer()
31
+ self.crawler = CnYesNewsCrawler(
32
+ sentiment_analyzer=self.sentiment_analyzer,
33
+ database=self.db
34
+ )
35
  self.scheduler = NewsScheduler(self.db, self.crawler, self.sentiment_analyzer)
36
 
37
+ # 進度追蹤
38
+ self.current_progress = "系統已就緒"
39
+ self.is_crawling = False
40
+
41
+ # 設置爬蟲進度回調
42
+ self.crawler.set_progress_callback(self.update_progress)
43
+
44
  # 啟動背景排程器
45
  self.scheduler.start()
46
 
47
  logger.info("新聞應用程式初始化完成")
48
 
49
+ def update_progress(self, message: str):
50
+ """更新進度信息"""
51
+ self.current_progress = f"{datetime.now().strftime('%H:%M:%S')} - {message}"
52
+ logger.info(f"進度更新: {message}")
53
+
54
+ def get_progress(self) -> str:
55
+ """獲取當前進度"""
56
+ return self.current_progress
57
+
58
  def get_latest_news(self, category: str = "all", limit: int = 50) -> str:
59
  """獲取最新新聞並格式化顯示"""
60
  try:
 
70
 
71
  def manual_crawl(self) -> str:
72
  """手動觸發爬蟲"""
73
+ if self.is_crawling:
74
+ return "⚠️ 爬蟲正在運行中,請稍後再試"
75
+
76
  try:
77
+ self.is_crawling = True
78
+ self.update_progress("🚀 手動爬蟲開始")
79
+
80
+ # 使用新的即時爬蟲方法
81
+ results = self.crawler.crawl_all_categories(max_articles_per_category=5)
82
+
83
+ total_articles = sum(len(articles) for articles in results.values())
84
+ result_message = f"✅ 手動爬蟲完成,總共處理 {total_articles} 篇文章"
85
+
86
+ self.update_progress(result_message)
87
+ return result_message
88
+
89
  except Exception as e:
90
+ error_message = f"❌ 手動爬蟲失敗: {str(e)}"
91
+ self.update_progress(error_message)
92
+ return error_message
93
+ finally:
94
+ self.is_crawling = False
95
 
96
  def get_statistics(self) -> str:
97
  """獲取統計資訊"""
 
102
  - 總新聞數量: {stats.get('total_news', 0)}
103
  - 美股新聞: {stats.get('us_stock_count', 0)}
104
  - 台股新聞: {stats.get('tw_stock_count', 0)}
105
+ - 正面新聞: {stats.get('positive_count', 0)} 😊
106
+ - 負面新聞: {stats.get('negative_count', 0)} 😔
107
+ - 中性新聞: {stats.get('neutral_count', 0)} 😐
108
  - 最後更新: {stats.get('last_update', 'N/A')}
109
  """
110
  except Exception as e:
111
  logger.error(f"獲取統計資訊錯誤: {e}")
112
  return f"❌ 獲取統計資訊失敗: {str(e)}"
113
 
114
+ def get_news_api_data(self, category: str = "all", limit: int = 50) -> Dict:
115
+ """獲取新聞API數據"""
116
+ try:
117
+ news_data = self.db.get_recent_news(category=category, limit=limit)
118
+
119
+ # 轉換為JSON友好格式
120
+ api_data = []
121
+ for news in news_data:
122
+ api_news = {
123
+ 'id': news.get('id'),
124
+ 'title': news.get('title'),
125
+ 'content': news.get('content'),
126
+ 'url': news.get('url'),
127
+ 'source': news.get('source'),
128
+ 'category': news.get('category'),
129
+ 'published_date': news.get('published_date').isoformat() if news.get('published_date') else None,
130
+ 'sentiment': news.get('sentiment'),
131
+ 'sentiment_score': news.get('sentiment_score'),
132
+ 'created_date': news.get('created_date')
133
+ }
134
+ api_data.append(api_news)
135
+
136
+ return {
137
+ 'success': True,
138
+ 'count': len(api_data),
139
+ 'data': api_data
140
+ }
141
+
142
+ except Exception as e:
143
+ logger.error(f"獲取API數據錯誤: {e}")
144
+ return {
145
+ 'success': False,
146
+ 'error': str(e),
147
+ 'data': []
148
+ }
149
+
150
  # 初始化應用
151
  app = NewsApp()
152
 
153
+ # API 路由
154
+ @flask_app.route('/api/news', methods=['GET'])
155
+ def api_get_news():
156
+ """獲取新聞列表API"""
157
+ category = request.args.get('category', 'all')
158
+ limit = int(request.args.get('limit', 50))
159
+
160
+ result = app.get_news_api_data(category, limit)
161
+ return jsonify(result)
162
+
163
+ @flask_app.route('/api/stats', methods=['GET'])
164
+ def api_get_stats():
165
+ """獲取統計信息API"""
166
+ try:
167
+ stats = app.db.get_statistics()
168
+ return jsonify({
169
+ 'success': True,
170
+ 'data': stats
171
+ })
172
+ except Exception as e:
173
+ return jsonify({
174
+ 'success': False,
175
+ 'error': str(e)
176
+ })
177
+
178
+ @flask_app.route('/api/crawl', methods=['POST'])
179
+ def api_manual_crawl():
180
+ """手動觸發爬蟲API"""
181
+ try:
182
+ if app.is_crawling:
183
+ return jsonify({
184
+ 'success': False,
185
+ 'message': '爬蟲正在運行中'
186
+ })
187
+
188
+ # 在背景執行爬蟲
189
+ def run_crawl():
190
+ app.manual_crawl()
191
+
192
+ threading.Thread(target=run_crawl, daemon=True).start()
193
+
194
+ return jsonify({
195
+ 'success': True,
196
+ 'message': '爬蟲任務已啟動'
197
+ })
198
+ except Exception as e:
199
+ return jsonify({
200
+ 'success': False,
201
+ 'error': str(e)
202
+ })
203
+
204
+ @flask_app.route('/api/progress', methods=['GET'])
205
+ def api_get_progress():
206
+ """獲取爬蟲進度API"""
207
+ return jsonify({
208
+ 'progress': app.get_progress(),
209
+ 'is_crawling': app.is_crawling
210
+ })
211
+
212
  # 創建 Gradio 介面
213
  def create_interface():
214
  with gr.Blocks(
 
223
  .positive-badge { background: #28a745; color: white; }
224
  .negative-badge { background: #dc3545; color: white; }
225
  .neutral-badge { background: #6c757d; color: white; }
226
+ .progress-box { background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 5px; padding: 10px; font-family: monospace; }
227
  """
228
  ) as interface:
229
 
230
  gr.Markdown("""
231
+ # 📈 股市新聞情緒分析器 - 即時版
232
 
233
+ 🤖 自動爬取鉅亨網美股和台股新聞,並進行即時中文情緒分析
234
 
235
+ **即時處理**: 每篇文章完成後立即分析並存檔
236
  🎯 **智能分析**: 使用 RoBERTa 模型進行情緒分析
237
  🔄 **去重處理**: 自動過濾重複新聞
238
+ 📊 **API接口**: 提供RESTful API獲取分析結果
239
  """)
240
 
241
  with gr.Tab("📰 最新新聞"):
242
  with gr.Row():
243
+ with gr.Column(scale=2):
244
  category_radio = gr.Radio(
245
  choices=["all", "us_stock", "tw_stock"],
246
  value="all",
 
251
  refresh_btn = gr.Button("🔄 重新整理", variant="primary")
252
  manual_crawl_btn = gr.Button("🚀 手動爬取", variant="secondary")
253
 
254
+ # 進度顯示
255
+ progress_display = gr.Textbox(
256
+ label="📊 即時進度",
257
+ value=app.get_progress(),
258
+ interactive=False,
259
+ elem_classes=["progress-box"]
260
+ )
261
+
262
  news_display = gr.HTML(label="新聞內容")
263
  crawl_result = gr.Textbox(label="爬取結果", visible=False)
264
 
265
+ # 自動重新整理進度
266
+ def update_progress_display():
267
+ return app.get_progress()
268
+
269
  def auto_refresh():
270
  return app.get_latest_news("all")
271
 
 
278
  news = app.get_latest_news("all")
279
  return result, news
280
 
281
+ # 定期更新進度
282
+ interface.load(
283
+ lambda: [app.get_progress(), app.get_latest_news("all")],
284
+ outputs=[progress_display, news_display],
285
+ every=5 # 每5秒更新一次
286
+ )
287
+
288
  # 綁定事件
289
  refresh_btn.click(refresh_news, inputs=[category_radio], outputs=[news_display])
290
  manual_crawl_btn.click(
 
295
  outputs=[crawl_result]
296
  )
297
  category_radio.change(refresh_news, inputs=[category_radio], outputs=[news_display])
 
 
 
298
 
299
  with gr.Tab("📊 統計資訊"):
300
  stats_display = gr.Markdown()
 
303
  stats_refresh_btn.click(app.get_statistics, outputs=[stats_display])
304
  interface.load(app.get_statistics, outputs=[stats_display])
305
 
306
+ with gr.Tab("🔌 API接口"):
307
+ gr.Markdown("""
308
+ ## 📖 API 使用說明
309
+
310
+ ### 🔗 接口列表
311
+
312
+ #### 1. 獲取新聞列表
313
+ ```
314
+ GET /api/news?category={all|us_stock|tw_stock}&limit={數量}
315
+ ```
316
+
317
+ **參數:**
318
+ - `category`: 新聞分類 (可選,默認: all)
319
+ - `limit`: 返回數量 (可選,默認: 50)
320
+
321
+ **響應示例:**
322
+ ```json
323
+ {
324
+ "success": true,
325
+ "count": 10,
326
+ "data": [
327
+ {
328
+ "id": 1,
329
+ "title": "美股標題",
330
+ "content": "新聞內容...",
331
+ "url": "https://...",
332
+ "source": "鉅亨網",
333
+ "category": "us_stock",
334
+ "published_date": "2024-01-01T12:00:00",
335
+ "sentiment": "positive",
336
+ "sentiment_score": 0.85,
337
+ "created_date": "2024-01-01T12:05:00"
338
+ }
339
+ ]
340
+ }
341
+ ```
342
+
343
+ #### 2. 獲取統計信息
344
+ ```
345
+ GET /api/stats
346
+ ```
347
+
348
+ #### 3. 手動觸發爬蟲
349
+ ```
350
+ POST /api/crawl
351
+ ```
352
+
353
+ #### 4. 獲取爬蟲進度
354
+ ```
355
+ GET /api/progress
356
+ ```
357
+
358
+ ### 💡 使用示例
359
+
360
+ **Python:**
361
+ ```python
362
+ import requests
363
+
364
+ # 獲取所有新聞
365
+ response = requests.get('http://localhost:7860/api/news')
366
+ news_data = response.json()
367
+
368
+ # 獲取美股新聞
369
+ response = requests.get('http://localhost:7860/api/news?category=us_stock&limit=10')
370
+ us_news = response.json()
371
+ ```
372
+
373
+ **JavaScript:**
374
+ ```javascript
375
+ // 獲取新聞
376
+ fetch('/api/news?category=tw_stock')
377
+ .then(response => response.json())
378
+ .then(data => console.log(data));
379
+
380
+ // 觸發爬蟲
381
+ fetch('/api/crawl', {method: 'POST'})
382
+ .then(response => response.json())
383
+ .then(data => console.log(data));
384
+ ```
385
+ """)
386
+
387
  with gr.Tab("ℹ️ 關於"):
388
  gr.Markdown("""
389
  ## 🛠️ 技術特色
390
 
391
+ ### ⚡ 即時處理
392
+ - **實時分析**: 每篇文章爬取完成立即進行情緒分析
393
+ - **即時存檔**: 分析完成後立即保存到SQLite資料庫
394
+ - **進度追蹤**: 實時顯示爬蟲和分析進度
395
+ - **去重檢查**: 存檔前檢查標題相似度避免重複
396
+
397
  ### 📊 情緒分析
398
  - **模型**: `uer/roberta-base-finetuned-jd-binary-chinese`
399
  - **分類**: 正面 (綠色) / 負面 (紅色) / 中性 (灰色)
 
403
  - **來源**: 鉅亨網 (cnyes.com)
404
  - **分類**: 美股、台股新聞
405
  - **頻率**: 每30分鐘自動更新
406
+ - **反爬蟲**: 隨機延遲、User-Agent輪換
407
 
408
  ### 💾 資料管理
409
+ - **儲存**: SQLite 本地資料庫 (news.db)
410
+ - **索引**: 針對URL、分類、時間、情緒建立索引
411
  - **保留期**: 自動清理兩週前的資料
412
+ - **效能**: 優化查詢,支持並發存取
413
 
414
+ ### 🔌 API接口
415
+ - **REST API**: 提供完整的RESTful API
416
+ - **JSON格式**: 標準JSON響應格式
417
+ - **錯誤處理**: 完善的錯誤處理和狀態碼
418
+ - **跨域支持**: 支持CORS跨域請求
419
 
420
  ---
421
 
422
+ 💡 **提示**:
423
+ - 首次啟動可能需要幾分鐘下載模型
424
+ - 建議使用SSD硬碟提升SQLite性能
425
+ - API接口可用於整合其他應用系統
426
  """)
427
 
428
  return interface
429
 
430
  # 啟動應用
431
  if __name__ == "__main__":
432
+ import threading
433
+
434
+ # 在背景啟動Flask API
435
+ def run_flask():
436
+ flask_app.run(host='0.0.0.0', port=5000, debug=False)
437
+
438
+ flask_thread = threading.Thread(target=run_flask, daemon=True)
439
+ flask_thread.start()
440
+
441
+ # 啟動Gradio介面
442
  interface = create_interface()
443
  interface.launch(
444
  server_name="0.0.0.0",