Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -450,8 +450,193 @@ def api_get_progress():
|
|
| 450 |
|
| 451 |
# 創建 Gradio 介面
|
| 452 |
def create_interface():
|
| 453 |
-
|
| 454 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 455 |
|
| 456 |
# 啟動應用
|
| 457 |
if __name__ == "__main__":
|
|
@@ -469,7 +654,7 @@ if __name__ == "__main__":
|
|
| 469 |
print("⚡ 自動功能: 系統啟動後自動檢測並爬取新聞")
|
| 470 |
|
| 471 |
# 啟動Gradio介面
|
| 472 |
-
interface = create_interface()
|
| 473 |
interface.launch(
|
| 474 |
server_name="0.0.0.0",
|
| 475 |
server_port=7860,
|
|
|
|
| 450 |
|
| 451 |
# 創建 Gradio 介面
|
| 452 |
def create_interface():
|
| 453 |
+
with gr.Blocks(
|
| 454 |
+
title="📈 股市新聞情緒分析器",
|
| 455 |
+
theme=gr.themes.Soft(),
|
| 456 |
+
css="""
|
| 457 |
+
.news-positive { background: linear-gradient(90deg, #d4edda 0%, #c3e6cb 100%); border-left: 4px solid #28a745; }
|
| 458 |
+
.news-negative { background: linear-gradient(90deg, #f8d7da 0%, #f5c6cb 100%); border-left: 4px solid #dc3545; }
|
| 459 |
+
.news-neutral { background: linear-gradient(90deg, #e2e3e5 0%, #d6d8db 100%); border-left: 4px solid #6c757d; }
|
| 460 |
+
.news-card { margin: 10px 0; padding: 15px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
| 461 |
+
.sentiment-badge { padding: 4px 8px; border-radius: 12px; font-size: 12px; font-weight: bold; }
|
| 462 |
+
.positive-badge { background: #28a745; color: white; }
|
| 463 |
+
.negative-badge { background: #dc3545; color: white; }
|
| 464 |
+
.neutral-badge { background: #6c757d; color: white; }
|
| 465 |
+
.progress-box { background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 5px; padding: 10px; font-family: monospace; font-size: 14px; }
|
| 466 |
+
"""
|
| 467 |
+
) as interface:
|
| 468 |
+
|
| 469 |
+
gr.Markdown("""
|
| 470 |
+
# 📈 股市新聞情緒分析器 - 自動版
|
| 471 |
+
|
| 472 |
+
🤖 自動爬取鉅亨網美股和台股新聞,並進行即時中文情緒分析
|
| 473 |
+
|
| 474 |
+
⚡ **自動啟動**: 程式啟動後自動開始爬取新聞
|
| 475 |
+
🎯 **智能分析**: 使用 RoBERTa 模型進行情緒分析
|
| 476 |
+
🔍 **多條件篩選**: 支援時間段、關鍵字、情緒篩選
|
| 477 |
+
📊 **即時統計**: 提供詳細的新聞統計資訊
|
| 478 |
+
""")
|
| 479 |
+
|
| 480 |
+
with gr.Tab("📰 最新新聞"):
|
| 481 |
+
with gr.Row():
|
| 482 |
+
with gr.Column(scale=1):
|
| 483 |
+
category_radio = gr.Radio(
|
| 484 |
+
choices=[
|
| 485 |
+
("所有新聞", "all"),
|
| 486 |
+
("美股新聞", "us_stock"),
|
| 487 |
+
("台股新聞", "tw_stock")
|
| 488 |
+
],
|
| 489 |
+
value="all",
|
| 490 |
+
label="📋 新聞分類"
|
| 491 |
+
)
|
| 492 |
+
|
| 493 |
+
days_slider = gr.Slider(
|
| 494 |
+
minimum=0,
|
| 495 |
+
maximum=30,
|
| 496 |
+
value=7,
|
| 497 |
+
step=1,
|
| 498 |
+
label="📅 時間範圍 (天)",
|
| 499 |
+
info="0表示不限制時間"
|
| 500 |
+
)
|
| 501 |
+
|
| 502 |
+
keyword_input = gr.Textbox(
|
| 503 |
+
label="🔍 關鍵字搜尋",
|
| 504 |
+
placeholder="輸入關鍵字搜尋新聞...",
|
| 505 |
+
value=""
|
| 506 |
+
)
|
| 507 |
+
|
| 508 |
+
sentiment_radio = gr.Radio(
|
| 509 |
+
choices=[
|
| 510 |
+
("所有情緒", "all"),
|
| 511 |
+
("正面情緒", "positive"),
|
| 512 |
+
("負面情緒", "negative"),
|
| 513 |
+
("中性情緒", "neutral")
|
| 514 |
+
],
|
| 515 |
+
value="all",
|
| 516 |
+
label="😊 情緒篩選"
|
| 517 |
+
)
|
| 518 |
+
|
| 519 |
+
# 爬蟲模式選擇
|
| 520 |
+
crawl_mode = gr.Radio(
|
| 521 |
+
choices=[
|
| 522 |
+
("無限制爬取 (全部文章)", True),
|
| 523 |
+
("限制爬取 (20篇)", False)
|
| 524 |
+
],
|
| 525 |
+
value=True,
|
| 526 |
+
label="🚀 爬蟲模式",
|
| 527 |
+
info="選擇爬取模式"
|
| 528 |
+
)
|
| 529 |
+
|
| 530 |
+
with gr.Column(scale=2):
|
| 531 |
+
with gr.Row():
|
| 532 |
+
search_btn = gr.Button("🔍 搜尋新聞", variant="primary")
|
| 533 |
+
refresh_btn = gr.Button("🔄 重新整理", variant="secondary")
|
| 534 |
+
manual_crawl_btn = gr.Button("🚀 手動爬取", variant="secondary")
|
| 535 |
+
|
| 536 |
+
# 進度顯示
|
| 537 |
+
progress_display = gr.Textbox(
|
| 538 |
+
label="📊 系統狀態",
|
| 539 |
+
value=app.current_progress,
|
| 540 |
+
interactive=False,
|
| 541 |
+
elem_classes=["progress-box"],
|
| 542 |
+
lines=1
|
| 543 |
+
)
|
| 544 |
+
|
| 545 |
+
news_display = gr.HTML(
|
| 546 |
+
label="新聞內容",
|
| 547 |
+
value="⏳ 系統正在初始化並自動爬取新聞,請稍候..."
|
| 548 |
+
)
|
| 549 |
+
crawl_result = gr.Textbox(label="爬取結果", visible=False)
|
| 550 |
+
|
| 551 |
+
# 更新函數
|
| 552 |
+
def update_progress_only():
|
| 553 |
+
"""只更新進度,不更新新聞"""
|
| 554 |
+
progress, needs_update = app.get_progress()
|
| 555 |
+
if needs_update or app.is_crawling:
|
| 556 |
+
return progress
|
| 557 |
+
else:
|
| 558 |
+
return gr.update()
|
| 559 |
+
|
| 560 |
+
def update_news_automatically():
|
| 561 |
+
"""自動更新新聞內容"""
|
| 562 |
+
if app.auto_crawl_completed:
|
| 563 |
+
return app.get_latest_news("all", 7, "", "all", force_refresh=True)
|
| 564 |
+
else:
|
| 565 |
+
return gr.update()
|
| 566 |
+
|
| 567 |
+
def search_news(category, days, keyword, sentiment):
|
| 568 |
+
"""搜尋新聞"""
|
| 569 |
+
logger.info(f"搜尋新聞 - 分類: {category}, 天數: {days}, 關鍵字: '{keyword}', 情緒: {sentiment}")
|
| 570 |
+
return app.get_latest_news(category, days, keyword, sentiment, force_refresh=True)
|
| 571 |
+
|
| 572 |
+
def refresh_current_search(category, days, keyword, sentiment):
|
| 573 |
+
"""刷新當前搜尋"""
|
| 574 |
+
return app.get_latest_news(category, days, keyword, sentiment, force_refresh=True)
|
| 575 |
+
|
| 576 |
+
def handle_manual_crawl(category, days, keyword, sentiment, unlimited_mode):
|
| 577 |
+
"""處理手動爬蟲"""
|
| 578 |
+
result = app.manual_crawl(unlimited=unlimited_mode)
|
| 579 |
+
# 爬取完成後自動刷新當前搜尋
|
| 580 |
+
news = app.get_latest_news(category, days, keyword, sentiment, force_refresh=True)
|
| 581 |
+
return result, news
|
| 582 |
+
|
| 583 |
+
# 進度更新定時器
|
| 584 |
+
progress_timer = gr.Timer(value=10)
|
| 585 |
+
progress_timer.tick(
|
| 586 |
+
fn=update_progress_only,
|
| 587 |
+
outputs=[progress_display]
|
| 588 |
+
)
|
| 589 |
+
|
| 590 |
+
# 新聞自動更新定時器
|
| 591 |
+
news_timer = gr.Timer(value=15) # 每15秒檢查一次
|
| 592 |
+
news_timer.tick(
|
| 593 |
+
fn=update_news_automatically,
|
| 594 |
+
outputs=[news_display]
|
| 595 |
+
)
|
| 596 |
+
|
| 597 |
+
# 綁定事件
|
| 598 |
+
search_btn.click(
|
| 599 |
+
search_news,
|
| 600 |
+
inputs=[category_radio, days_slider, keyword_input, sentiment_radio],
|
| 601 |
+
outputs=[news_display]
|
| 602 |
+
)
|
| 603 |
+
|
| 604 |
+
refresh_btn.click(
|
| 605 |
+
refresh_current_search,
|
| 606 |
+
inputs=[category_radio, days_slider, keyword_input, sentiment_radio],
|
| 607 |
+
outputs=[news_display]
|
| 608 |
+
)
|
| 609 |
+
|
| 610 |
+
manual_crawl_btn.click(
|
| 611 |
+
handle_manual_crawl,
|
| 612 |
+
inputs=[category_radio, days_slider, keyword_input, sentiment_radio, crawl_mode],
|
| 613 |
+
outputs=[crawl_result, news_display]
|
| 614 |
+
).then(
|
| 615 |
+
lambda: gr.update(visible=True),
|
| 616 |
+
outputs=[crawl_result]
|
| 617 |
+
)
|
| 618 |
+
|
| 619 |
+
# 分類改變時自動搜尋
|
| 620 |
+
category_radio.change(
|
| 621 |
+
search_news,
|
| 622 |
+
inputs=[category_radio, days_slider, keyword_input, sentiment_radio],
|
| 623 |
+
outputs=[news_display]
|
| 624 |
+
)
|
| 625 |
+
|
| 626 |
+
# 初始載入時顯示等待訊息
|
| 627 |
+
interface.load(
|
| 628 |
+
lambda: "⏳ 系統正在自動爬取新聞,請稍候...",
|
| 629 |
+
outputs=[news_display]
|
| 630 |
+
)
|
| 631 |
+
|
| 632 |
+
with gr.Tab("📊 統計資訊"):
|
| 633 |
+
stats_display = gr.Markdown()
|
| 634 |
+
stats_refresh_btn = gr.Button("🔄 更新統計")
|
| 635 |
+
|
| 636 |
+
stats_refresh_btn.click(app.get_statistics, outputs=[stats_display])
|
| 637 |
+
interface.load(app.get_statistics, outputs=[stats_display])
|
| 638 |
+
|
| 639 |
+
return interface # ⬅️ 這裡是關鍵!一定要返回 interface
|
| 640 |
|
| 641 |
# 啟動應用
|
| 642 |
if __name__ == "__main__":
|
|
|
|
| 654 |
print("⚡ 自動功能: 系統啟動後自動檢測並爬取新聞")
|
| 655 |
|
| 656 |
# 啟動Gradio介面
|
| 657 |
+
interface = create_interface() # 修正:正確獲取interface
|
| 658 |
interface.launch(
|
| 659 |
server_name="0.0.0.0",
|
| 660 |
server_port=7860,
|