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}
和 Query、Path 一个套路,只是换成 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 def和await 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 错误处理