FastAPI 教學:表單與檔案上傳 – HTML Form 資料解析
簡介
在 Web 應用程式中,最常見的資料傳遞方式之一就是 HTML 表單。使用者在瀏覽器上填寫文字、選擇下拉選項、或上傳檔案,最後提交給後端 API。對於使用 FastAPI 建置的服務,正確解析這些表單資料是實作 CRUD、使用者註冊、檔案管理等功能的基礎。
FastAPI 內建對 application/x-www-form-urlencoded、multipart/form-data(包含檔案) 的支援,且結合 Pydantic 的型別驗證,讓開發者只需要宣告參數類型,就能自動完成資料抽取、驗證與錯誤回應。掌握表單資料的解析技巧,不僅能提升開發效率,也能減少安全漏洞(例如未驗證的輸入導致的 SQL Injection)。
本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,帶你一步步了解如何在 FastAPI 中處理 HTML Form 資料,並提供實務應用情境,幫助你快速上手。
核心概念
1. 表單資料的傳遞格式
| 格式 | 說明 | 常見用途 |
|---|---|---|
application/x-www-form-urlencoded |
以 key=value&key2=value2 形式編碼,字元會被 URL 編碼。 |
純文字表單、登入、搜尋等。 |
multipart/form-data |
每個欄位分成獨立的「部件」,支援檔案二進位傳輸。 | 檔案上傳、混合文字+檔案的表單。 |
FastAPI 會根據 Content-Type 來決定使用哪種解析器。
2. Form 與 File 參數
Form:用於接收application/x-www-form-urlencoded或multipart/form-data中的文字欄位。File:專門用來接收檔案(UploadFile物件),只能在multipart/form-data中使用。
兩者都是 依賴注入(Dependency Injection)的形式,必須在路由函式的參數前加上 Form(...) 或 File(...)。
from fastapi import FastAPI, Form, File, UploadFile
app = FastAPI()
3. 型別驗證與預設值
FastAPI 會把 Form/File 的值交給 Pydantic 進行型別驗證。
- 必填欄位:只要不給
default=或...,FastAPI 會自動產生 422 錯誤。 - 預設值:使用
default=或直接在函式參數設定預設值。
@app.post("/login")
async def login(username: str = Form(...), password: str = Form(...)):
# 若缺少任一欄位會回傳 422
return {"msg": f"Welcome, {username}!"}
4. 取得多值欄位(list)
HTML 中的 <select multiple> 或同名的 <input> 可以產生 多個值。只要把參數型別寫成 list[<type>],FastAPI 會自動收集所有值。
@app.post("/tags")
async def tags(selected: list[str] = Form(...)):
return {"tags": selected}
5. UploadFile 物件的特性
UploadFile 提供以下屬性與方法:
| 屬性/方法 | 說明 |
|---|---|
filename |
原始檔名(含副檔名)。 |
content_type |
MIME 類型,例如 image/png。 |
file |
SpooledTemporaryFile 物件,可使用 read(), write(), seek()。 |
read() |
讀取全部二進位資料(建議搭配 await)。 |
write(data) |
寫入資料。 |
close() |
關閉暫存檔。 |
程式碼範例
以下示範 5 個常見情境,從最簡單的文字表單到同時上傳多個檔案。每段程式碼均附上說明註解。
範例 1️⃣:最基礎的文字表單(登入)
from fastapi import FastAPI, Form, HTTPException
app = FastAPI()
@app.post("/login")
async def login(
username: str = Form(...), # 必填欄位,若缺少會回 422
password: str = Form(...), # 必填欄位
):
# 實作簡易驗證(僅示範,不建議明文比對)
if username != "admin" or password != "secret":
raise HTTPException(status_code=401, detail="Invalid credentials")
return {"message": f"Hi, {username}!"}
重點:使用
Form(...)表示「此欄位必須由表單送出」,FastAPI 會自動產生 OpenAPI 文件,前端開發者可直接參考。
範例 2️⃣:帶有預設值的表單欄位(搜尋)
@app.get("/search")
async def search(
query: str = Form(""), # 預設空字串,若未提供則使用空值
page: int = Form(1), # 預設第 1 頁
page_size: int = Form(20) # 預設每頁 20 筆
):
# 假設有一個搜尋函式 search_items(query, offset, limit)
offset = (page - 1) * page_size
results = await search_items(query, offset, page_size)
return {"query": query, "page": page, "items": results}
說明:即使是
GET方法,也可以使用Form取得表單資料(瀏覽器會自行以application/x-www-form-urlencoded方式發送)。
範例 3️⃣:接收多值欄位(標籤)
@app.post("/post")
async def create_post(
title: str = Form(...),
content: str = Form(...),
tags: list[str] = Form([]) # 若未傳送則回傳空 list
):
# 在資料庫中建立貼文,並同時儲存 tags
post_id = await db.create_post(title=title, content=content, tags=tags)
return {"post_id": post_id, "tags": tags}
技巧:HTML 範例
<form action="/post" method="post">
<input type="text" name="title" required>
<textarea name="content"></textarea>
<select name="tags" multiple>
<option value="fastapi">FastAPI</option>
<option value="python">Python</option>
<option value="web">Web</option>
</select>
<button type="submit">送出</button>
</form>
範例 4️⃣:單一檔案上傳(圖片)
from fastapi import UploadFile, File
import aiofiles
@app.post("/upload-image")
async def upload_image(
file: UploadFile = File(...), # 必須使用 multipart/form-data
description: str = Form("")
):
# 限制檔案類型
if not file.content_type.startswith("image/"):
raise HTTPException(status_code=400, detail="Only image files allowed")
# 儲存至本機 (非同步寫入)
async with aiofiles.open(f"uploads/{file.filename}", "wb") as out_file:
while content := await file.read(1024): # 分塊讀取,降低記憶體占用
await out_file.write(content)
await file.close()
return {"filename": file.filename, "description": description}
說明:
UploadFile內部使用 SpooledTemporaryFile,在檔案小於 1 MB 時會保存在記憶體,超過則自動寫入磁碟。
範例 5️⃣:多檔案同時上傳(文件批次)
from typing import List
@app.post("/batch-upload")
async def batch_upload(
files: List[UploadFile] = File(...), # List 代表可接受多個檔案
user_id: int = Form(...)
):
saved = []
for f in files:
# 只接受 PDF 或文字檔
if f.content_type not in ("application/pdf", "text/plain"):
continue
path = f"documents/{user_id}/{f.filename}"
async with aiofiles.open(path, "wb") as out:
while chunk := await f.read(2048):
await out.write(chunk)
await f.close()
saved.append(f.filename)
return {"saved_files": saved, "total": len(saved)}
實務提示:為了避免磁碟寫入過於頻繁,可先在記憶體中驗證檔案類型與大小,再決定是否寫入。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方法 / Best Practice |
|---|---|---|
忘記加 await 讀取檔案 |
UploadFile.read() 是 非同步,直接呼叫會回傳 coroutine,導致錯誤或未實際讀取。 |
使用 await file.read(),或分塊 while chunk := await file.read(size). |
| 未限制檔案大小 | 使用者可能上傳巨大的檔案,導致記憶體或磁碟耗盡。 | 在 Middleware 或路由內檢查 file.size(需要自行取得),或在前端限制 <input type="file" maxsize="...">。 |
直接使用 file.file.read() |
會同步阻塞,失去 FastAPI 的非同步優勢。 | 始終使用 await file.read();若必須同步,考慮 run_in_threadpool. |
| 表單欄位名稱拼寫錯誤 | FastAPI 依賴欄位名稱匹配,錯誤會導致 422。 | 使用 OpenAPI 自動產生的 Swagger UI 測試表單,確保名稱一致。 |
未設定 media_type |
回傳檔案時若未指定 MIME,瀏覽器可能無法正確辨識。 | 使用 FileResponse(path, media_type="application/pdf"). |
| 安全性檢查不足 | 直接將使用者上傳的檔名寫入磁碟,可能導致路徑穿越攻擊 (../../etc/passwd)。 |
使用 uuid4() 產生唯一檔名或 os.path.basename 只保留檔名,並限制存放目錄。 |
最佳實踐清單
- 型別驗證:盡量使用 Pydantic model 進行資料結構驗證,避免手寫大量條件。
- 非同步 I/O:檔案讀寫、資料庫操作皆使用
async/await,提升併發效能。 - 錯誤回應統一:自訂
HTTPException,返回結構化的 JSON(detail,code),方便前端處理。 - 日誌與監控:記錄上傳檔案的大小、類型與使用者 ID,便於追蹤與偵測異常。
- 安全檔名:上傳後使用隨機字串或 hash 作為檔名,避免原始檔名帶入危險字元。
實際應用場景
| 場景 | 需求 | 建議的 FastAPI 實作方式 |
|---|---|---|
| 使用者註冊 | 收集 username, email, password,同時允許上傳頭像。 |
使用 Form 收集文字欄位,UploadFile 處理頭像,並在儲存前壓縮圖片(Pillow)以及加密密碼(bcrypt)。 |
| 商品上架 | 商品資訊 + 多張商品圖片。 | Form 接收商品名稱、描述、價格等;List[UploadFile] 接收多張圖片,儲存至雲端(AWS S3)或本機,回傳圖片 URL。 |
| 文件審核系統 | 批次上傳 PDF、Word,並紀錄上傳者與審核狀態。 | List[UploadFile] + user_id(Form),在路由內先驗證檔案類型與大小,寫入資料庫 Document 表,並觸發背景任務(Celery)進行文字抽取。 |
| 即時聊天附件 | 使用者可上傳圖片或音訊檔案。 | 前端使用 FormData 並以 multipart/form-data 送出,後端使用 UploadFile,儲存後回傳檔案 URL,接著在訊息資料表中保存該 URL。 |
| 多語系表單 | 同一表單在不同語系下有不同欄位名稱。 | 利用 alias 參數在 Form(..., alias="中文欄位名"),讓 API 同時支援多種欄位名稱,保持 OpenAPI 文件的可讀性。 |
總結
- HTML 表單 是前端與後端溝通的核心管道,FastAPI 透過
Form、File、UploadFile三個主要工具,提供簡潔且功能完整的解析機制。 - 只要在路由函式的參數前加上
Form(...)或File(...),FastAPI 會自動完成 資料抽取、型別驗證與錯誤回應,讓開發者專注於業務邏輯。 - 實作時要注意 非同步 I/O、檔案大小限制、安全檔名 等常見陷阱,並遵循 最佳實踐(統一錯誤格式、日誌、驗證),才能打造高效且安全的 API。
- 透過本文的範例與應用情境,你已掌握從單一文字表單到多檔案批次上傳的完整流程,未來可以自由拓展到更複雜的使用情境,如圖像處理、文件審核、即時聊天附件等。
快把這些技巧運用到你的下一個 FastAPI 專案吧!祝開發順利 🚀