本文 AI 產出,尚未審核

FastAPI 教學:表單與檔案上傳 – HTML Form 資料解析


簡介

在 Web 應用程式中,最常見的資料傳遞方式之一就是 HTML 表單。使用者在瀏覽器上填寫文字、選擇下拉選項、或上傳檔案,最後提交給後端 API。對於使用 FastAPI 建置的服務,正確解析這些表單資料是實作 CRUD、使用者註冊、檔案管理等功能的基礎。

FastAPI 內建對 application/x-www-form-urlencodedmultipart/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. FormFile 參數

  • Form:用於接收 application/x-www-form-urlencodedmultipart/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 只保留檔名,並限制存放目錄。

最佳實踐清單

  1. 型別驗證:盡量使用 Pydantic model 進行資料結構驗證,避免手寫大量條件。
  2. 非同步 I/O:檔案讀寫、資料庫操作皆使用 async/await,提升併發效能。
  3. 錯誤回應統一:自訂 HTTPException,返回結構化的 JSON(detail, code),方便前端處理。
  4. 日誌與監控:記錄上傳檔案的大小、類型與使用者 ID,便於追蹤與偵測異常。
  5. 安全檔名:上傳後使用隨機字串或 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 透過 FormFileUploadFile 三個主要工具,提供簡潔且功能完整的解析機制。
  • 只要在路由函式的參數前加上 Form(...)File(...),FastAPI 會自動完成 資料抽取、型別驗證與錯誤回應,讓開發者專注於業務邏輯。
  • 實作時要注意 非同步 I/O檔案大小限制安全檔名 等常見陷阱,並遵循 最佳實踐(統一錯誤格式、日誌、驗證),才能打造高效且安全的 API。
  • 透過本文的範例與應用情境,你已掌握從單一文字表單到多檔案批次上傳的完整流程,未來可以自由拓展到更複雜的使用情境,如圖像處理、文件審核、即時聊天附件等。

快把這些技巧運用到你的下一個 FastAPI 專案吧!祝開發順利 🚀