本專案是一個用於抓取中華職棒(CPBL)官方網站數據的後端服務,旨在提供一個通用、穩健且可擴展的數據平台。它採用容器化技術(Docker)封裝,並已成功部署至 Fly.io 雲端平台,且與 GitHub Actions 整合,實現了推送即部署的 CI/CD 流程與成本優化的自動化排程。
服務核心功能是自動化爬取比賽賽程、逐場比賽的詳細攻守數據(包含所有球員的逐打席紀錄),以及球員的球季數據歷史。架構上,採用非同步任務佇列(Dramatiq + Redis)來處理耗時的爬蟲任務,並透過一個受 API 金鑰保護的 RESTful API 提供從基本查詢到複雜情境分析的多種數據需求。
- 通用數據抓取: 自動抓取並儲存賽季中所有球隊、所有球員的逐打席紀錄,而非針對特定目標。
- 歷史數據追蹤: 記錄球員球季數據的歷史快照,可供分析其表現趨勢。
- 高效能 API: 透過資料庫查詢優化與應用層快取,確保複雜的分析型 API 也能快速回應。
- 穩健的背景任務: 使用 Dramatiq 與 Redis,將耗時的爬蟲工作與 API 伺服器分離。
- 現代化依賴項管理: 全面採用 Poetry 進行精確、可重複的依賴項管理,以
pyproject.toml作為唯一事實來源。 - 簡化的開發者體驗: 提供
Makefile將複雜的 Docker 指令封裝為make bulk-scrape等易於記憶的指令。 - 豐富的 RESTful API: 基於 FastAPI,提供多層次的數據查詢與分析能力,並內建互動式 API 文件。
- 容器化開發與部署: 使用 Docker 與 Docker Compose 建立標準化的開發與生產環境。
- 資料庫版本控制: 使用 Alembic 管理資料庫結構的遷移。
- 雲端原生架構: 部署於 Fly.io,將 API (
web) 與爬蟲 (worker) 拆分為獨立服務,並整合Xvfb繞過反爬蟲機制。 - 自動化品質與安全: 整合 pre-commit (Ruff) 與 GitHub Actions CI/CD 流程,並透過
pip-audit進行依賴項安全掃描。
本專案在 Fly.io 上的生產環境由以下幾個核心元件組成:
- Fly App (
cpbl-takao-today-be): 專案的主應用程式容器。- Web Service (
web): 運行 FastAPI 的 Uvicorn 伺服器,負責接收所有 API 請求,並將耗時任務發送至佇列。 - Worker Service (
worker): 運行 Dramatiq Worker,專門執行由app/workers.py中定義的背景任務。它在Xvfb虛擬顯示環境中運行,使其能以headless=False模式操作瀏覽器,應對複雜的網站反爬蟲機制。
- Web Service (
- Fly PostgreSQL: 由 Fly.io 管理的獨立 PostgreSQL 資料庫服務。
- Aiven Redis: 作為外部第三方服務,同時肩負兩種職責:
- 訊息代理 (Broker): 供 Dramatiq 使用 (db0),並啟用 Result Backend 以支援任務狀態查詢。
- 應用層快取 (Cache): 供 Web 服務使用 (db1),實現操作隔離。
graph TD
subgraph "使用者/觸發器"
Client[外部客戶端 / 前端]
GHA_Deploy["GHA: CI/CD<br/>(部署成功時觸發)"]
GHA_Cron["GHA: Cron Job<br/>(每日定時觸發)"]
end
subgraph "GitHub Actions 工作流"
GHA_E2E["e2e_workflow_health_check.yml"]
GHA_Daily["daily_crawl.yml"]
end
subgraph "Fly.io 雲端平台 (Scale-to-Zero)"
subgraph "Fly App: cpbl-takao-today-be"
Web[Web Service / FastAPI]
Worker["Worker Service / Dramatiq<br/>(運行於 Xvfb 環境)"]
end
DB[(Fly PostgreSQL)]
end
subgraph "第三方服務"
Redis[(Aiven Redis db0: Broker db1: Cache)]
end
subgraph "外部網站"
CPBL[CPBL 官網]
end
Client -- "API 請求" --> Web
GHA_Deploy -- "觸發" --> GHA_E2E
GHA_Cron -- "觸發" --> GHA_Daily
GHA_E2E -- "1. 啟動機器" --> Web
GHA_E2E -- "1. 啟動機器" --> Worker
GHA_E2E -- "2. 觸發 E2E 測試 API" --> Web
GHA_E2E -- "3. 輪詢任務狀態 API" --> Web
GHA_E2E -- "5. 關閉機器" --> Web
GHA_E2E -- "5. 關閉機器" --> Worker
GHA_Daily -- "執行類似 E2E 的流程<br/>但觸發真實爬蟲任務" --> Web
Web -- "資料庫查詢" --> DB
Web -- "快取讀寫" --> Redis
Web -- "發送背景任務" --> Redis
Redis -- "4. 任務入隊" --> Worker
Worker -- "執行爬蟲" --> CPBL
Worker -- "將結果寫入資料庫" --> DB
Worker -- "清除快取(可選)" --> Web
本專案導入了一系列 GitHub Actions 工作流,以實現高度自動化、強健且具成本效益的維運模式。
為了解決傳統排程器依賴服務常駐運行的問題,本專案已將排程的觸發與控制權完全轉移至 GitHub Actions,實現了按需啟停 (Scale-to-Zero) 的架構,大幅降低維運成本。
核心工作流程 (.github/workflows/daily_crawl.yml) 如下:
- 啟動: 每日固定時間,workflow 自動執行
fly machines start指令,啟動web與worker機器。 - 觸發: 機器啟動後,workflow 會呼叫
POST /api/system/trigger-daily-crawl端點,將每日爬蟲任務送入 Dramatiq 佇列並取得task_id。 - 監控: 接著,workflow 會進入輪詢階段,週期性地呼叫
GET /api/system/task-status/{task_id}端點,主動監控任務的真實執行狀態 (succeeded,failed)。 - 關閉: 一旦任務完成(無論成功或失敗),workflow 會立刻執行
fly machines stop指令關閉所有機器,確保運算資源只在必要時運行。
此架構不僅提升了排程的健壯性,更透過精準的按需控制,將服務的每日運行時間從 24 小時縮減至約 30 分鐘。
在每次成功部署到 main 分支後,.github/workflows/e2e_workflow_health_check.yml 工作流會被自動觸發,執行一個與每日排程類似的生命週期,來驗證生產環境的端對端健康度:
- 啟動測試機器: 啟動一台
web和一台worker。 - 驗證 API 與背景任務: 透過 API 觸發一個輕量級的 E2E 測試任務 (
task_e2e_workflow_test)。 - 監控與斷言: 輪詢任務狀態,直到確認其在預期時間內成功完成。
- 自動清理: 無論成功或失敗,最後都會關閉所有機器。
此流程確保了每次部署的核心功能(API 接收、任務入隊、Worker 執行、結果回報)都正常運作。
.github/workflows/ci.yml 工作流已全面改用 Poetry 來執行所有步驟(安裝依賴、Linter、測試、安全掃描),取代了原先的 pip 和 requirements.txt。這帶來了以下好處:
- 單一事實來源:
pyproject.toml和poetry.lock成為唯一的依賴項定義,確保了 CI 環境與本地開發環境的完全一致。 - 指令統一: 所有指令都透過
poetry run <command>執行,更為簡潔清晰。
| 類別 | 技術 |
|---|---|
| 後端框架 | FastAPI, Uvicorn |
| 資料庫 | PostgreSQL, SQLAlchemy (ORM), Alembic |
| 背景任務 / 快取 | Dramatiq, Redis (Aiven) |
| 網頁爬蟲 | Playwright, BeautifulSoup4, Requests |
| 容器化 | Docker, Docker Compose |
| 雲端平台 | Fly.io |
| 依賴項管理 | Poetry |
| CI/CD 與程式碼品質 | GitHub Actions, pre-commit, Ruff |
| 測試框架 | pytest, pytest-mock, pytest-playwright |
| 負載測試 | Locust |
| 設定管理 | pydantic-settings |
| 日誌 | python-json-logger |
| 虛擬顯示 | Xvfb (X virtual framebuffer) |
| 依賴項安全 | pip-audit |
請遵循以下步驟在你的本地機器上設定並運行此專案。
git clone <YOUR_REPOSITORY_URL>
cd <PROJECT_DIRECTORY>本專案透過 .env 檔案管理本地開發環境的設定。請從範例檔案複製一份來開始:
cp .env.example .env接著,請修改 .env 檔案的內容。所有必要的環境變數都定義在 app/config.py 中,並透過 Pydantic 進行驗證。
本專案的組態管理遵循職責分離原則:
-
敏感資訊 (Secrets): 如
DATABASE_URL,API_KEY等。這類資訊絕不能提交至版本控制。- 生產環境: 由 Fly.io 的 Secrets 功能管理 (
fly secrets set)。 - CI/CD 流程: 由 GitHub Actions 的 Secrets 提供。
- 本地開發: 存放於
.env檔案中 (此檔案已被.gitignore排除)。
- 生產環境: 由 Fly.io 的 Secrets 功能管理 (
-
非敏感組態 (Configuration): 如
TARGET_TEAMS等應用層面的設定。這類資訊應受版本控制。- 生產環境: 定義於
fly.toml的[env]區塊中。 - 本地開發: 同樣存放於
.env檔案中,方便覆寫與測試。
- 生產環境: 定義於
重要: fly.toml 中 TARGET_TEAMS 和 TARGET_PLAYERS 這類列表型別的變數,必須使用標準的 JSON 陣列字串格式(且內部引號需轉義),以確保 Pydantic 能正確解析。
本專案使用 Docker Compose 管理所有服務,並透過 Makefile 簡化了常用指令。
-
啟動所有服務容器:
# 首次啟動或 Dockerfile/docker-compose.yml 變更後,使用 --build docker compose up -d --build -
初始化資料庫 (僅首次設定必要): 此指令會執行 Alembic,建立所有必要的資料表。
make migrate
-
首次初始化賽程: 此指令會觸發背景任務,抓取整年度的賽程。
make bulk-update-schedule
本專案採用 FastAPI 框架,它會自動生成互動式的 API 文件。這份文件是查詢所有可用端點、參數及請求範例最準確的來源。
當本地開發環境啟動後,請直接在瀏覽器中開啟以下任一網址:
-
互動式 API 文件 (Swagger UI): http://127.0.0.1:8000/docs
-
另一種 API 文件格式 (ReDoc): http://127.0.0.1:8000/redoc
在 /docs 頁面中,你可以直接在線上測試每一個 API 端點,並查看其請求與回應的詳細結構。
本專案提供一個 Makefile 來簡化所有常見的開發與維護任務。開發者應優先使用 make 指令,而非直接執行冗長的 docker compose 或 python 指令。
-
爬取指定日期範圍的比賽資料:
# 爬取今天的比賽資料 make bulk-scrape # 爬取從 2025-05-01 到今天的資料 make bulk-scrape start=2025-05-01 # 爬取 2025-03-01 到 2025-04-30 的資料 make bulk-scrape start=2025-03-01 end=2025-04-30
-
爬取所有球員的生涯數據:
make bulk-scrape-career
-
更新賽程:
make bulk-update-schedule
-
上傳暫存資料至生產資料庫:
make bulk-upload
-
回填指定日期範圍的球員歷史數據:
# 為 2024-05-01 到 2024-05-10 的每一個有比賽日建立歷史數據快照 make backfill-history start=2024-05-01 end=2024-05-10 # 為 2024 整年度的每一個有比賽日建立歷史數據快照 make backfill-history start=2024-01-01
-
重設本地開發資料庫:
# 警告:此操作會刪除所有資料 make reset-db # 只重設指定的資料表 (以逗號分隔) make reset-db tables=player_season_stats_history,game_results
-
執行壓力測試:
# 注意:此指令需要在你的本機 (非 Docker) 安裝 Locust make load-test -
建立金絲雀測試樣本資料:
make create-canary
-
重新產生此份 README 文件:
make gen-readme
本專案使用 Alembic 來管理資料庫結構的變更。
-
當你修改
app/models.py中的模型後,你需要產生一個新的遷移腳本:# 在 worker 容器中自動產生遷移腳本 docker compose run --rm worker alembic revision --autogenerate -m "描述你的變更"
-
將變更應用到資料庫:
# 在 worker 容器中執行 upgrade make migrate
-
pre-commit: 本專案使用
pre-commit在每次git commit時自動執行 Ruff (linter + formatter),以確保所有提交的程式碼都符合一致的風格與品質標準。初次設定請運行pre-commit install。 -
GitHub Actions: 每次推送到
main分支時,會觸發 CI/CD 工作流程,包含:-
程式碼品質與格式檢查。
-
執行完整的
pytest測試套件(排除 e2e 與 canary 測試)。 -
若測試通過,自動部署應用至 Fly.io。
-
本專案使用 Pytest Marker 來分類測試 (e2e, canary)。你可以使用 -m 參數來篩選要執行或跳過的測試。
-
僅執行單元測試 (CI 的主要流程):
pytest -m "not e2e and not canary" -
僅執行 e2e 測試:
pytest -m e2e
-
僅執行金絲雀測試:
pytest -m canary
本專案已設定 CI/CD,任何推送到 main 分支且通過所有測試的提交,都會被自動部署到 Fly.io。這是標準的部署流程。
在 CI/CD 流程無法使用或需要緊急部署時,可以透過 flyctl CLI 工具手動部署。
-
設定秘密 (Secrets): 在首次部署或 Secret 變更時,你仍需手動設定生產環境的敏感資訊。這些資訊絕不應存放在版本控制中。
fly secrets set \ DATABASE_URL="<YOUR_FLY_POSTGRES_URL>" \ DRAMATIQ_BROKER_URL="<YOUR_AIVEN_REDIS_URL>" \ API_KEY="<YOUR_PRODUCTION_API_KEY>" \
-
部署應用: 設定完 secrets 後,在專案根目錄執行以下指令即可觸發部署。
fly deploy
flyctl會讀取fly.toml檔案,在本機建置 Docker 映像檔,並將其推送到 Fly.io 平台,更新web與worker服務。
由於主控台日誌已改為結構化的 JSON 格式,建議搭配使用命令列工具 jq 來美化、上色及過濾日誌。
前置需求:
請先確保你的系統已安裝 jq (例如在 Ubuntu/WSL 中執行 sudo apt-get install jq)。
美化輸出:
# 查看 web 服務的美化日誌
docker compose logs web | jq
# 持續追蹤 worker 服務的美化日誌
docker compose logs -f worker | jq依 request_id 過濾:
當你需要追蹤單一 API 請求的完整生命週期時,可利用我們新增的 request_id 欄位進行過濾。
# 只顯示特定 request_id 的日誌記錄
docker compose logs web | jq 'select(.request_id == "YOUR_REQUEST_ID_HERE")'