本文 AI 產出,尚未審核
FastAPI – 表單與檔案上傳(Form & File Upload)
單檔上傳與多檔上傳
簡介
在 Web 應用程式中,檔案上傳是最常見的需求之一:使用者可能需要上傳頭像、簡歷、報表或多張圖片。
FastAPI 以 非同步、型別安全 為核心設計,讓我們能以最少的程式碼即完成單檔與多檔上傳,同時自動產生 OpenAPI 文件,對前端開發者非常友好。
本篇文章將從 概念說明、實作範例、常見陷阱 以及 最佳實踐 逐步帶你掌握 FastAPI 的檔案上傳技巧,適合剛入門的初學者,也能為已有經驗的開發者提供實務參考。
核心概念
1. 為什麼使用 UploadFile 而不是 bytes
FastAPI 提供兩種接收檔案的方式:
| 型別 | 特色 | 何時使用 |
|---|---|---|
bytes |
檔案會一次性全部讀入記憶體 | 檔案極小(例如 < 1 KB) |
UploadFile |
以 Starlette 的 UploadFile 包裝,支援 流式 讀取,只有檔案的 metadata 會先載入 |
大多數實務情境(圖片、PDF、CSV 等) |
建議:除非明確知道檔案非常小,否則優先使用
UploadFile,可避免 OOM(記憶體耗盡)問題。
2. 表單 (Form) 與檔案 (File) 的結合
FastAPI 透過 Form、File 兩個依賴注入(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 個常見情境,從最簡單的單檔上傳到結合表單與多檔的完整範例。程式碼均使用 Python(python 標記),并加入詳細註解。
範例 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_chunks 或 aiofiles 流式寫入 |
| 未限制檔案類型 | 安全風險(上傳惡意程式) | 在路由內檢查 file.content_type 或副檔名,必要時使用病毒掃描 |
未設定 max_length |
客戶端可無限制上傳巨檔 | 在 Starlette 或 uvicorn 設定 --limit-concurrency / --max-request-size,或於程式內自行驗證 |
忘記關閉檔案 (await file.close()) |
檔案描述符泄漏,長時間運行後耗盡資源 | 使用 async with(需要自行封裝)或確保在所有分支最後呼叫 close() |
| 直接把上傳檔案寫入根目錄 | 權限問題、路徑遍歷攻擊 | 使用 pathlib 的 resolve() 與白名單路徑,避免 ../ 攻擊 |
最佳實踐
- 永遠使用非同步 I/O:
UploadFile本身是非同步的,搭配aiofiles能最大化效能。 - 限制檔案大小與類型:在路由層或中介層(middleware)加入驗證。
- 產生安全的檔名:使用 UUID、hash 或時間戳記,避免檔名衝突與路徑注入。
- 回傳可用的 URL:上傳後即返回可直接存取的 URL,減少前端再次請求的步驟。
- 記錄上傳日誌:包含使用者 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_chunks或aiofiles即可安全處理大檔案。 - 結合
Form、File、List[UploadFile],我們可以同時取得文字欄位與多檔陣列,滿足大多數實務需求。 - 在實作時務必 限制檔案大小與類型、產生安全檔名、確保檔案關閉,並記錄相關日誌,以提升系統的 可靠性 與 安全性。
掌握以上概念與範例後,你就能在 FastAPI 中快速構建可靠的檔案上傳介面,為前端提供友善的 API,並在後端安全、有效率地處理各式檔案。祝開發順利,玩得開心!