跳到主要内容

2.5 表单和文件上传

本章目标:学会处理 HTML 表单数据和文件上传。这一章使用频率不如前几章高,可以快速过一遍,需要时再回来查。

0. 准备工作

表单和文件功能依赖 python-multipart 库:

pip install python-multipart

(如果你安装的是 fastapi[standard],它已经附带了。)

1. 表单数据:Form

大部分现代前后端交互都用 JSON,但两种场景会用表单Content-Type: application/x-www-form-urlencoded):

  • 传统 HTML <form> 提交
  • OAuth2 登录规范要求用户名密码用表单传(第五部分 JWT 章节会遇到)
from typing import Annotated
from fastapi import FastAPI, Form

app = FastAPI()


@app.post("/login")
def login(
username: Annotated[str, Form()],
password: Annotated[str, Form()],
):
return {"username": username}

QueryPath 一个套路,只是换成 Form()。在 /docs 里测试时它会显示为表单输入框而不是 JSON 编辑器。

注意:一个接口不能同时声明 Form 和 JSON 请求体,二选一。

2. 文件上传:UploadFile

from fastapi import FastAPI, UploadFile

app = FastAPI()


@app.post("/upload")
async def upload_file(file: UploadFile):
content = await file.read() # 读取文件内容(bytes)
return {
"filename": file.filename, # 原始文件名
"content_type": file.content_type, # 如 image/png
"size": len(content), # 字节数
}

说明:

  • 参数类型写 UploadFile,FastAPI 自动识别为文件上传
  • 这里用了 async defawait file.read()UploadFile 的读取方法是异步的,所以配合 async def 使用最自然(关于 async 的说明见本章第 5 节)
  • /docs 里会出现一个文件选择按钮,可直接测试

把上传的文件保存到磁盘

from pathlib import Path
from fastapi import FastAPI, UploadFile, HTTPException

app = FastAPI()

UPLOAD_DIR = Path("uploads")
UPLOAD_DIR.mkdir(exist_ok=True) # 确保目录存在

ALLOWED_TYPES = {"image/jpeg", "image/png", "image/gif"}
MAX_SIZE = 5 * 1024 * 1024 # 5MB


@app.post("/upload-image")
async def upload_image(file: UploadFile):
# 1. 校验类型
if file.content_type not in ALLOWED_TYPES:
raise HTTPException(status_code=400, detail="只允许上传 jpg/png/gif 图片")

# 2. 读取并校验大小
content = await file.read()
if len(content) > MAX_SIZE:
raise HTTPException(status_code=400, detail="文件不能超过 5MB")

# 3. 保存(注意:实际项目不要直接用原始文件名,防止路径攻击/重名,
# 应该生成唯一文件名,如 uuid)
import uuid
ext = Path(file.filename or "").suffix
save_name = f"{uuid.uuid4().hex}{ext}"
(UPLOAD_DIR / save_name).write_bytes(content)

return {"saved_as": save_name}

这段代码包含了文件上传的三个必做安全检查:限类型、限大小、重命名

3. 多文件上传

@app.post("/upload-multi")
async def upload_multi(files: list[UploadFile]):
return {"filenames": [f.filename for f in files]}

4. 文件 + 表单字段一起传

上传头像时同时带一个说明文字:

from typing import Annotated
from fastapi import FastAPI, Form, UploadFile

app = FastAPI()


@app.post("/avatar")
async def upload_avatar(
file: UploadFile,
description: Annotated[str, Form()] = "",
):
return {"filename": file.filename, "description": description}

文件和 Form 可以共存(它们都属于 multipart 表单),但不能和 JSON 请求体共存。

5. 插播:def 还是 async def?

你可能已经注意到本章用了 async def。简单说明一下,新手阶段记住这个决策规则就够:

  • 函数里需要 await 某个异步操作(如 await file.read())→ 用 async def
  • 函数里用的是同步库(比如本教程后面用的同步版 SQLAlchemy)→ 用普通 def

FastAPI 两种都支持,普通 def 不会让你的接口"变慢"——FastAPI 会把同步函数放到线程池里跑,不阻塞服务。新手最容易犯的错误反而是:在 async def 里调用耗时的同步操作(如同步数据库查询),这会卡住整个服务。

本教程主线使用同步写法def + 同步 SQLAlchemy),因为它更简单、心智负担小、完全够用。等你熟练后再去学异步全家桶(async SQLAlchemy + asyncpg)也不迟。

本章小结

  • Form() 接收表单字段(登录接口会用到)
  • UploadFile 接收上传文件,await file.read() 读内容
  • 文件上传三件套:限类型、限大小、用 uuid 重命名
  • 教程主线用同步 def,有 await 需求时才用 async def

下一章:2.6 错误处理