表单数据与文件上传
本教程共 50 篇 · 第 13 篇 · 更新于 2026-08-12 · 约 8 分钟阅读
本节目标:分清表单数据和 JSON 请求体的区别,学会用 Form 接收表单字段,用 File 和 UploadFile 处理文件上传,并理解多文件与 multipart 编码。
前面的请求体都是 JSON。但还有一种常见场景:网页上的登录框、文件选择按钮,它们提交的数据不是 JSON,而是「表单」。FastAPI 用 Form 和 File 来接这些数据。
13-1 表单数据和 JSON 的区别
HTML 表单提交数据时,编码方式和 JSON 完全不同。这一点决定了你不能用 Pydantic 模型去接表单。
| 对比项 | JSON 请求体 | 表单数据 |
|---|---|---|
| Content-Type | application/json | application/x-www-form-urlencoded |
| 声明方式 | Pydantic BaseModel | Form() |
| 数据结构 | 支持嵌套对象和数组 | 扁平的键值对 |
| 常见场景 | 前后端分离的接口 | 网页表单提交、文件上传 |
表单数据是一组扁平的键值对,不支持嵌套。所以通常用 Form() 逐字段声明。新版 FastAPI(0.113 及以上)也支持把一个扁平的 Pydantic 模型传给 Form(),框架会把模型字段平铺成表单键值对;但表单本质仍是扁平结构,不支持嵌套对象。
Warning表单字段和 JSON 请求体不能在同一接口里混用。因为两者的编码方式不同,一个请求体要么是表单,要么是 JSON,不可能同时是两种。这是 HTTP 协议的规定,不是 FastAPI 的限制。
13-2 先装 python-multipart
用表单和文件之前,必须装一个依赖:python-multipart。FastAPI 靠它来解析表单编码的数据。
python -m pip install python-multipart
装好之后,才能正常使用 Form 和 File。否则运行时会报错提示缺少这个包。
13-3 用 Form 接收表单字段
从 fastapi 导入 Form,把它当默认值写在参数上,参数名要和表单字段名一致。
from fastapi import FastAPI, Form
app = FastAPI()
@app.post("/login/")
async def login(username: str = Form(...), password: str = Form(...)):
return {"username": username}
这段代码声明了两个必填表单字段:username 和 password。客户端提交表单时,FastAPI 会从中取出对应的值。
Form(...) 表示必填,Form(None) 表示可选。和之前学的 Query、Field 用法一致。
打开 /docs 测试这个接口,你会看到它的内容类型被标成 application/x-www-form-urlencoded,这说明它确实是表单接口,不是 JSON。
13-4 可选表单字段
和查询参数一样,给表单字段一个默认值,它就变成可选的。
from fastapi import FastAPI, Form
app = FastAPI()
@app.post("/items/")
async def create_item(
name: str = Form(...),
description: str | None = Form(None),
):
return {"name": name, "description": description}
这里 description 带了 Form(None),客户端不传也不会报错。表单字段同样支持 min_length、max_length 等校验参数,写法和 Field 类似。
13-5 用 File 接收单个文件
接收上传的文件,要从 fastapi 导入 File。最简单的方式是把参数类型写成 bytes,FastAPI 会把整个文件读进内存。
from fastapi import FastAPI, File
app = FastAPI()
@app.post("/files/")
async def create_file(file: bytes = File(...)):
return {"file_size": len(file)}
这种方式适合小文件。文件内容会完整存放在内存里,len(file) 就是字节数。文件一大,内存就吃不消,所以不推荐大文件用 bytes。
13-6 UploadFile 的优势
更推荐的方式是 UploadFile。它比 bytes 强在好几个地方。
from fastapi import FastAPI, File, UploadFile
app = FastAPI()
@app.post("/uploadfile/")
async def create_upload_file(file: UploadFile = File(...)):
contents = await file.read()
return {
"filename": file.filename,
"content_type": file.content_type,
"size": len(contents),
}
UploadFile 的优势有这些:
- 不必在默认值写
File(),类型标注UploadFile就够了。 - 它用「内存+磁盘」的临时文件:小文件放内存,超过阈值自动落盘,大文件不撑爆内存。
- 能拿到文件的元数据:原始文件名、MIME 类型等。
- 提供类文件的异步接口,能直接交给别的库使用。
Tip处理图片、视频、压缩包这类大文件,一律优先用
UploadFile。它既省内存,又能拿到文件名和类型,排查问题方便得多。
13-7 UploadFile 的属性与方法
UploadFile 提供三个常用属性:
filename:上传时的原始文件名,比如photo.jpg。content_type:文件的 MIME 类型,比如image/jpeg。file:底层的类文件对象,可直接交给需要文件句柄的库。
它还有几个异步方法,调用时记得加 await:
await file.read():读取内容,返回bytes。await file.write(data):写入数据。await file.seek(offset):移动读取位置,比如seek(0)回到开头。await file.close():关闭文件。
contents = await file.read()
# 读完后想再读一次,先回到开头
await file.seek(0)
在普通的 def 函数里(非异步),可以直接用 file.file.read() 同步读取,不必 await。但本教程默认用 async def,所以统一用 await 写法。
13-8 多文件上传
要一次上传好几个文件,把参数声明成 list[UploadFile] 即可。
from fastapi import FastAPI, File, UploadFile
app = FastAPI()
@app.post("/uploadfiles/")
async def create_upload_files(files: list[UploadFile] = File(...)):
return {"filenames": [f.filename for f in files]}
客户端把多个文件绑在同一个表单字段名下发送,FastAPI 会收成一个列表。你可以用循环逐个处理,比如依次读取、保存。
13-9 表单与文件混合
在同一次提交里,既能传文件,也能传普通表单字段。只需把 Form 和 File 写在一起。
from fastapi import FastAPI, File, Form, UploadFile
app = FastAPI()
@app.post("/files/")
async def create_file(
file: UploadFile = File(...),
token: str = Form(...),
):
return {"filename": file.filename, "token": token}
这个接口同时要求一个文件和一个名为 token 的表单字段。它用到的编码是 multipart/form-data,下面会解释。
Note表单字段和文件可以在同一接口混用(都是 multipart)。但 JSON 请求体和表单字段不能混用,别搞混这两条规则。
13-10 客户端为什么要 multipart
当表单只含普通字段时,编码是 application/x-www-form-urlencoded。一旦表单里出现文件,编码就变成 multipart/form-data。
multipart 的意思是「多部分」:它会把请求体切成若干块,一块放普通字段,一块放文件内容。这样二进制文件才能被正确传输,不会被当成普通文本损坏。
FastAPI 看到你用了 File,就自动按 multipart/form-data 去解析,从正确那一块里取文件。你不用手动设置编码,框架帮你处理。
前端用 HTML 表单上传时,只要写对这两点就能对接:
<form action="http://localhost:8000/uploadfile/" method="POST" enctype="multipart/form-data">
<input type="file" name="file" />
<input type="submit" />
</form>
enctype="multipart/form-data" 和 input type="file" 是关键,缺了就传不了文件。
13-11 把上传文件保存到磁盘
实际项目里,拿到文件后通常要存到服务器。用 UploadFile 配合异步读取,再写进本地文件即可。
from fastapi import FastAPI, File, UploadFile
app = FastAPI()
@app.post("/upload/")
async def upload(file: UploadFile = File(...)):
with open(f"uploads/{file.filename}", "wb") as buffer:
content = await file.read()
buffer.write(content)
return {"filename": file.filename}
这段代码把文件内容读出来,以原文件名写进 uploads/ 目录。生产环境还要注意三件事:校验文件类型和大小;用安全的文件名避免路径穿越;限制目录权限。这些属于安全范畴,这里先记住思路。
13-12 小结
表单和文件是 Web 开发里的常见需求,记住这些要点:
- 表单数据用
Form()接收,不能用 Pydantic 模型,且不能和 JSON 请求体混用。 - 用表单和文件前,先装
python-multipart。 - 小文件可用
bytes = File(...);大文件优先用UploadFile,省内存还能拿元数据。 UploadFile的方法要await:read()、seek()、close()。- 多文件用
list[UploadFile];文件和表单字段可一起用multipart/form-data。 - 客户端上传文件时,表单要设
enctype="multipart/form-data"。
到这里,请求相关的核心数据形式——路径参数、查询参数、请求体、表单、文件——你都已经掌握。