本文 AI 產出,尚未審核

FastAPI – 表單與檔案上傳(Form & File Upload)

單檔上傳與多檔上傳


簡介

在 Web 應用程式中,檔案上傳是最常見的需求之一:使用者可能需要上傳頭像、簡歷、報表或多張圖片。
FastAPI 以 非同步型別安全 為核心設計,讓我們能以最少的程式碼即完成單檔與多檔上傳,同時自動產生 OpenAPI 文件,對前端開發者非常友好。

本篇文章將從 概念說明實作範例常見陷阱 以及 最佳實踐 逐步帶你掌握 FastAPI 的檔案上傳技巧,適合剛入門的初學者,也能為已有經驗的開發者提供實務參考。


核心概念

1. 為什麼使用 UploadFile 而不是 bytes

FastAPI 提供兩種接收檔案的方式:

型別 特色 何時使用
bytes 檔案會一次性全部讀入記憶體 檔案極小(例如 < 1 KB)
UploadFile StarletteUploadFile 包裝,支援 流式 讀取,只有檔案的 metadata 會先載入 大多數實務情境(圖片、PDF、CSV 等)

建議:除非明確知道檔案非常小,否則優先使用 UploadFile,可避免 OOM(記憶體耗盡)問題。

2. 表單 (Form) 與檔案 (File) 的結合

FastAPI 透過 FormFile 兩個依賴注入(dependency)來解析 multipart/form-data 請求。

  • Form(...) 解析一般文字欄位。
  • File(...) 解析檔案欄位,回傳 UploadFile(單檔)或 List[UploadFile](多檔)。
from fastapi import FastAPI, Form, File, UploadFile

3. 單檔 vs 多檔的型別差異

需求 參數型別 範例
單檔上傳 UploadFile file: UploadFile = File(...)
多檔上傳 List[UploadFile] files: List[UploadFile] = File(...)

注意List 必須從 typing 匯入 List,且在 OpenAPI 中會顯示為「檔案陣列」。


程式碼範例

以下示範 5 個常見情境,從最簡單的單檔上傳到結合表單與多檔的完整範例。程式碼均使用 Pythonpython 標記),并加入詳細註解。

範例 1️⃣:最簡單的單檔上傳

# app.py
from fastapi import FastAPI, File, UploadFile
from fastapi.responses import JSONResponse

app = FastAPI()

@app.post("/upload/single")
async def upload_single(file: UploadFile = File(...)):
    """
    接收單一檔案,回傳檔名與大小(bytes)。
    """
    content = await file.read()               # 讀取全部內容(小檔案可這樣用)
    size = len(content)
    await file.close()
    return JSONResponse(content={"filename": file.filename, "size": size})

說明

  • File(...) 表示此欄位是必填的。
  • await file.read() 會一次性讀取檔案,適合小於 1 MB 的情況。

範例 2️⃣:流式讀取大檔案(避免一次載入)

@app.post("/upload/stream")
async def upload_stream(file: UploadFile = File(...)):
    """
    以 chunk 方式讀取檔案,適合上傳大於記憶體容量的檔案。
    """
    chunk_size = 1024 * 1024   # 1 MB
    total_bytes = 0
    async for chunk in file.iter_chunks(chunk_size):
        # 這裡可以直接寫入磁碟、雲端儲存或做即時處理
        total_bytes += len(chunk)
    return {"filename": file.filename, "bytes_received": total_bytes}

說明

  • iter_chunks() 為非同步迭代器,每次回傳指定大小的位元組。
  • 只要不把全部內容一次讀入,就能有效控制記憶體使用量。

範例 3️⃣:多檔上傳 + 表單欄位

from typing import List

@app.post("/upload/multiple")
async def upload_multiple(
    description: str = Form(...),                 # 文字欄位
    files: List[UploadFile] = File(...),          # 多檔陣列
):
    """
    同時接收文字描述與多張圖片,回傳每張檔案的資訊。
    """
    results = []
    for f in files:
        content = await f.read()
        results.append({"filename": f.filename, "size": len(content)})
        await f.close()
    return {"description": description, "files": results}

說明

  • Form(...)File(...) 可以同時使用,FastAPI 會自動解析 multipart/form-data
  • List[UploadFile] 讓前端可以一次選取多個檔案。

範例 4️⃣:將上傳的檔案儲存到本機磁碟

import os
from pathlib import Path

UPLOAD_DIR = Path("./uploaded")
UPLOAD_DIR.mkdir(exist_ok=True)

@app.post("/upload/save")
async def upload_and_save(file: UploadFile = File(...)):
    """
    把檔案寫入本機目錄,使用 async with 以確保非同步寫入。
    """
    destination = UPLOAD_DIR / file.filename
    async with aiofiles.open(destination, "wb") as out_file:
        while chunk := await file.read(1024 * 1024):   # 1 MB
            await out_file.write(chunk)
    await file.close()
    return {"saved_path": str(destination)}

說明

  • 這裡使用 aiofiles(需 pip install aiofiles)進行非同步寫檔。
  • 透過 while chunk := await file.read(...) 逐塊寫入,避免一次讀入過大資料。

範例 5️⃣:結合驗證與回傳檔案 URL(實務常見)

from fastapi import HTTPException, status
from uuid import uuid4

BASE_URL = "https://cdn.example.com/files"

@app.post("/upload/validated")
async def upload_validated(
    file: UploadFile = File(...),
    max_size: int = Form(5 * 1024 * 1024),   # 5 MB 上限(可由前端傳入)
):
    # 1. 檢查檔案大小
    size = 0
    async for chunk in file.iter_chunks(1024 * 1024):
        size += len(chunk)
        if size > max_size:
            raise HTTPException(
                status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
                detail="檔案過大"
            )
    # 2. 產生唯一檔名並儲存(此處僅模擬)
    unique_name = f"{uuid4().hex}_{file.filename}"
    # 假設已上傳至雲端儲存服務
    file_url = f"{BASE_URL}/{unique_name}"
    return {"filename": file.filename, "size": size, "url": file_url}

說明

  • 先以 iter_chunks 逐塊計算大小,若超過上限即拋出 413 錯誤。
  • 使用 uuid4 產生唯一檔名,避免衝突。
  • 回傳的 url 可直接給前端顯示或存入資料庫。

常見陷阱與最佳實踐

陷阱 可能的後果 解決方式
一次性讀入大型檔案 (await file.read()) 記憶體 OOM,服務崩潰 改用 iter_chunksaiofiles 流式寫入
未限制檔案類型 安全風險(上傳惡意程式) 在路由內檢查 file.content_type 或副檔名,必要時使用病毒掃描
未設定 max_length 客戶端可無限制上傳巨檔 Starletteuvicorn 設定 --limit-concurrency / --max-request-size,或於程式內自行驗證
忘記關閉檔案 (await file.close()) 檔案描述符泄漏,長時間運行後耗盡資源 使用 async with(需要自行封裝)或確保在所有分支最後呼叫 close()
直接把上傳檔案寫入根目錄 權限問題、路徑遍歷攻擊 使用 pathlibresolve() 與白名單路徑,避免 ../ 攻擊

最佳實踐

  1. 永遠使用非同步 I/OUploadFile 本身是非同步的,搭配 aiofiles 能最大化效能。
  2. 限制檔案大小與類型:在路由層或中介層(middleware)加入驗證。
  3. 產生安全的檔名:使用 UUID、hash 或時間戳記,避免檔名衝突與路徑注入。
  4. 回傳可用的 URL:上傳後即返回可直接存取的 URL,減少前端再次請求的步驟。
  5. 記錄上傳日誌:包含使用者 ID、檔案大小、MD5/SHA256 雜湊,方便追蹤與除錯。

實際應用場景

場景 需求 FastAPI 解法
使用者頭像 單檔、大小限制 2 MB、只接受 JPEG/PNG UploadFile + content_type 檢查 + 儲存至 CDN
批次匯入 CSV 多檔、每檔最多 10 MB、須即時解析 List[UploadFile] + iter_chunks 逐行讀取 → pandas 讀取
線上表單 + 附件 表單文字 + 多張圖片 + 儲存至 S3 Form + List[UploadFile] + boto3 非同步上傳
大型影片上傳 單檔、上限 2 GB、需要斷點續傳 UploadFile + 前端切片 + 後端接收每片段寫入暫存 → 合併
安全文件審核 多檔、必須走病毒掃描服務 接收後立即將檔案寫入暫存 → 呼叫 ClamAV API → 通過後搬移至正式位置

總結

FastAPI非同步型別安全 的框架,讓 單檔多檔 上傳變得既簡潔又強大。

  • 使用 UploadFile 可以避免記憶體浪費,配合 iter_chunksaiofiles 即可安全處理大檔案。
  • 結合 FormFileList[UploadFile],我們可以同時取得文字欄位與多檔陣列,滿足大多數實務需求。
  • 在實作時務必 限制檔案大小與類型產生安全檔名確保檔案關閉,並記錄相關日誌,以提升系統的 可靠性安全性

掌握以上概念與範例後,你就能在 FastAPI 中快速構建可靠的檔案上傳介面,為前端提供友善的 API,並在後端安全、有效率地處理各式檔案。祝開發順利,玩得開心!