2025年10月25日 星期六

🤖 打造一個生產級的後端作品集:FastAPI 非同步爬蟲 + PostgreSQL + 排程自動化

🚀 專案解構:從教學範例到生產級服務 —— FastAPI 非同步爬蟲的升級之路

🎯 專案動機:從「能跑」到「可部署、安全、可擴充」

作為一名資深全端工程師,我深知一個好的技術作品集必須超越基礎功能實現。它必須展示:系統級的架構思維生產環境的安全性考量,以及面對 I/O 密集挑戰的解決方案

這個專案的起點是一個簡單的 FastAPI + requests 教學範例。它的問題顯而易見:同步爬蟲會阻塞整個服務、使用 SQLite 缺乏擴展性、且沒有任何安全或自動化機制

您可以透過以下 GitHub 連結檢閱本專案的原始碼:https://github.com/BpsEason/fastapi-news-crawler.git


我的目標是將它升級為一個具備以下特性的「作品集級別」後端服務:

  1. 高效能:全面採用非同步 I/O。

  2. 高穩定性:排程任務具備穩固的 Session 處理機制。

  3. 高安全性:實作 API Key 雜湊驗證,消除明文密鑰風險。

  4. 可自動化:具備定時爬取與自動測試 CI/CD 流程。

🧠 技術架構設計思維:I/O 密集型任務的全面非同步化

現代後端服務的核心瓶頸往往不在 CPU,而在於 I/O 操作(網絡請求、資料庫讀寫)。因此,我決定採用全面非同步化的架構,以確保整個系統的非阻塞性。

1. 核心技術棧選擇與優勢

技術模組選擇架構決策背後的原因
框架核心FastAPI基於 Starlette/Pydantic 的 ASGI 框架,原生支援非同步,天生適合 I/O 密集型服務。
HTTP 客戶端httpx作為 requests 的非同步替代品,確保爬蟲在等待網絡回應時,能釋放控制權,讓服務器處理其他請求。
資料庫PostgreSQL + AsyncPGPostgreSQL 是生產環境標準;AsyncPG 是一個專為 asyncio 設計的原生驅動,比傳統 ORM 轉接異步的方式效能更佳。
ORM 介面SQLModel整合了 Pydantic 的資料模型驗證和 SQLAlchemy 的 ORM 功能,極大地簡化了程式碼的定義與序列化。
自動化排程APScheduler輕量且支持 asyncio,適合在單一服務實例中嵌入定時任務,避免引入 Celery 等複雜系統。

2. 模組化與職責分離

專案採用清晰的模組化結構,將 API 路由、業務邏輯、配置和數據庫連接完全解耦:

  • /app/core: 專注於安全性(security.py)和配置(config.py)。

  • /app/services: 專注於業務邏輯(crawler.pyscheduler.py)。

  • /app/api/v1: 專注於路由和 API 驗證。

這種設計極大地提高了單元測試的效率和未來的可擴充性。

🕷️ 實戰細節一:非同步爬蟲與數據品質控制

1. 異步 I/O:發揮 httpx 的最大效益

app/services/crawler.py 中,我們使用 async with httpx.AsyncClient() 發起請求。這使得我們能夠在單個 Python 進程中同時發出數十甚至數百個 HTTP 請求,將總體爬取時間縮短數倍。

Python
# 爬蟲核心 (httpx)
async def fetch_news_titles(url: str) -> list[dict]:
    # 使用 async with 確保連線正確關閉
    async with httpx.AsyncClient(timeout=10) as client: 
        resp = await client.get(url)
        # ... BeautifulSoup 解析邏輯 ...

2. 數據品質與冪等性:實作去重邏輯

一個作品集級別的爬蟲必須處理數據重複問題。我採用的去重策略是 「資料庫唯一性檢查 + 異步事務」

  1. 資料庫約束:Article 模型中,將 url 欄位設定為 unique=True

  2. 業務邏輯去重:save_articles 服務中,對每個新抓取的文章,先執行異步 SELECT 查詢,檢查 URL 是否已存在。

這確保了數據的乾淨度。若未來需要處理高併發插入,則應考慮使用 PostgreSQL 的 ON CONFLICT DO NOTHING,但當前架構下的檢查更具程式碼可讀性。

🔐 實戰細節二:安全性升級 —— API Key 雜湊驗證

這是最能展現資深工程師思維的一環:消除 API Key 明文洩露的風險

踩坑經驗:為何明文比較不可接受?

在原始教學範例中,API Key 驗證可能只是簡單的 if api_key == settings.API_KEY_SECRET。這種做法會將明文密鑰硬編碼或存儲在未加密的環境變數中。一旦配置洩露,系統將完全暴露。

解決方案:Bcrypt 雜湊驗證

app/core/security.py 中,我引入了 passlib[bcrypt] 實現密鑰雜湊:

  1. 配置存儲: 環境變數中存儲的是 API Key 的 Bcrypt 雜湊值(預先計算好)。

  2. 運行時驗證:

    • 使用者傳入明文 Key。

    • 驗證函數使用 pwd_context.verify(傳入Key, 預期雜湊值)

    • bcrypt 在內部計算傳入 Key 的雜湊,並與預期雜湊值進行比較。

這種設計使得程式碼中永遠不會出現明文密鑰,大大提高了系統的安全性。我們透過 FastAPI 的 Security(APIKeyHeader(...)) 依賴注入,將這項安全措施無縫應用於所有受保護的 API 端點。

⏱️ 實戰細節三:穩定性與自動化 —— 排程任務的資源管理

挑戰:排程任務中的連線池洩露 (Connection Pool Leak)

APScheduler 作為一個獨立的線程或進程運行,當它嘗試從 FastAPI 的異步依賴生成器 (AsyncGenerator) 中獲取資料庫 Session 時,如果沒有正確處理,極易導致連接池資源洩露。

錯誤模式是在任務結束時忘記或無法可靠地呼叫 Session 生成器的 aclose() 方法。

解決方案:contextlib.asynccontextmanager

為了確保資源的穩健釋放,我在 app/services/scheduler.py 中使用了 Python 標準庫的 contextlib

Python
# 確保安全的 Session 管理
@asynccontextmanager
async def get_safe_db_session():
    gen = get_db_session()
    try:
        session = await anext(gen)
        yield session # 執行爬蟲/DB操作
    finally:
        # 關鍵:確保無論成功或失敗,都調用 aclose() 關閉 Session
        await gen.aclose() 

透過 async with get_safe_db_session() as session: 語句,我將定時任務包裹在一個異步上下文管理器中。這從架構上保證了 Session 在任務結束時會被安全釋放,徹底消除了連線池洩露的風險,實現了生產級的穩定性。

🧪 測試與部署:建立可信賴的 CI/CD 流程

1. 異步測試與環境模擬

為了測試整個系統,我們使用了 pytest-asynciohttpx.AsyncClient

  • API 測試: tests/test_api.py 驗證了 API Key 驗證邏輯(測試無效 Key 時是否返回 403 FORBIDDEN)。

  • CI/CD 技巧: 在 GitHub Actions 的 CI 流程中,我們利用環境變數將資料庫 URL 切換為 SQLite + aiosqlite。這樣做的目的是在運行測試時,避免啟動笨重的 PostgreSQL 容器,大幅加快 CI 速度並簡化設置。只有在集成測試階段,我們才需要完整的 Docker Compose 環境。

2. Docker Compose:標準化部署

最終的 docker-compose.yml 將整個專案封裝為兩個可攜帶的服務:db (PostgreSQL) 和 web (FastAPI)。

這不僅簡化了開發環境的啟動,也讓專案具備了極高的可移植性——只需 docker compose up --build,即可在任何支持 Docker 的雲端平台(如 Render、Fly.io)上線,為後續的部署演示打下堅實基礎。

總結與未來展望

這個 FastAPI 爬蟲專案已經從一個基礎教學,蛻變為一個架構嚴謹、安全可靠、且具備自動化能力的後端服務。

它展示了作為資深工程師所需的關鍵技能:

  • 非同步架構設計

  • 連線池穩定性管理

  • 生產級密鑰安全實踐

  • CI/CD 部署思維

沒有留言:

張貼留言

熱門文章