FastAPI:(5)表单与请求文件
由于CSDN无法展示「渐构」的「#d,#e,#t,#c,#v,#a」标签,推荐访问我个人网站进行阅读:Hkini
「渐构展示」如下:
#c 概述 文章相关概念关系
graph TD
A[FastAPI 请求体处理] --> B[表单数据]
A --> C[表单模型]
A --> D[请求文件]
%% 表单数据特征
B --> B1[使用Form声明]
B --> B2[Content-Type: application/x-www-form-urlencoded]
B --> B3[适合轻量键值对结构]
%% 表单模型是表单数据的结构化扩展
B --> C
C --> C1[继承 Pydantic.BaseModel]
C --> C2[每个字段仍需Form]
C --> C3[适合多字段表单,如注册表单]
C --> C4[结构清晰,便于复用]
%% 请求文件特征
D --> D1[使用File声明]
D --> D2[Content-Type: multipart/form-data]
D --> D3[适合文件上传]
D --> D4[推荐使用UploadFile类型]
%% 共性:表单和文件都依赖 multipart/form-data
B ---|当包含文件时| D2
C ---|基于表单数据| B
D ---|与表单字段可同时使用| B
%% 文件和表单的联合使用
B & D --> E[组合上传表单字段与文件]
E --> E1[Form + File 混合声明参数]
1.表单数据
#d 表单数据
在 FastAPI 中,表单数据是指通过 HTTP 请求的 Content-Type: application/x-www-form-urlencoded
或 multipart/form-data
方式提交的数据。常见于传统 HTML <form>
表单的 POST 提交场景,用于收集用户输入的数据(如登录表单、注册表单等)。FastAPI 提供 Form
类型来显式声明处理此类数据,与 Body
(处理 JSON 请求体)区别开。
重要特征:
- 特征1:传输方式为编码后的键值对数据
表单数据以键值对形式通过 HTTP 请求体传输,典型格式为application/x-www-form-urlencoded
或包含文件的multipart/form-data
。 - 特征2:主要用于 HTML 表单提交场景
通常由前端<form>
提交产生,广泛用于用户注册、登录、上传等交互行为。 - 特征3:必须显式使用
Form(...)
进行声明
FastAPI 通过Form(...)
告诉框架该字段应从表单数据中提取,而不是 JSON 或路径参数。 - 特征4:适用于轻量数据,适合简单键值对,不含嵌套结构
表单数据天生不支持复杂嵌套结构,适合简单字段传输。
#c 细节 使用表单细节
Form
是直接继承自Body
的类。声明表单体要显式使用
Form
,否则,FastAPI 会把该参数当作查询参数或请求体(JSON)参数。
- 表单数据的「媒体类型」编码一般为
application/x-www-form-urlencoded
。
- 包含文件的表单编码为
multipart/form-data
。
- 要使用表单,需预先安装
python-multipart
可在一个_路径操作_中声明多个
Form
参数,但不能同时声明要接收 JSON 的Body
字段。因为此时请求体的编码是application/x-www-form-urlencoded
,不是application/json
。这不是 FastAPI 的问题,而是 HTTP 协议的规定。与 JSON 不同,HTML 表单(
<form></form>
)向服务器发送数据通常使用「特殊」的编码。FastAPI 要确保从正确的位置读取数据,而不是读取 JSON。
#e 用户登录表单(正例) 表单数据
现象
在一个典型的 Web 应用中,用户通过一个 HTML 登录页面输入用户名和密码,这些信息通过 <form>
提交为表单数据,后端使用 FastAPI 接收处理。
特征对比
特征 | 是否满足 |
---|---|
键值对传输 | ✅ 用户名和密码为键值对 |
HTML 表单提交 | ✅ 来源为 <form> |
使用 Form(...) 声明 | ✅ 后端字段使用 Form |
数据结构简单 | ✅ 只包含两个简单字段 |
from fastapi import FastAPI, Form # 导入Form
app = FastAPI()
@app.post("/login")
async def login(username: str = Form(...), password: str = Form(...)): # 定义Form参数
return {"message": f"User {username} attempted to log in."}
'''
- 表单字段用 `Form(...)` 显式声明;
- 用户通过 HTML `<form>` 提交用户名与密码;
- 请求头应为 `Content-Type: application/x-www-form-urlencoded`。
'''
#d 表单模型
表单模型(Form Model) 是指通过继承 BaseModel
创建的 Pydantic 模型,并结合 Form(...)
实现的字段声明,从而将多个表单字段组织为一个结构化模型,用于解析 HTML 或表单请求数据。它是 FastAPI 中一种对表单数据进行结构化封装和验证的方式,使表单处理更加规范和复用性更强。
重要特征:
- 特征1:模型结构基于
pydantic.BaseModel
使用 Pydantic 来定义字段、类型和验证规则,实现结构化数据管理。 - 特征2:每个字段必须显式声明为
Form(...)
类型
与传统 JSONBaseModel
区分,Form 模型中的字段不能省略Form(...)
,否则 FastAPI 默认认为是 JSON。 - 特征3:适用于表单数据结构较复杂但仍为键值对的情况
例如包含多个字段的注册、反馈、申请表单等。 - 特征4:实现代码复用和清晰结构
多个请求处理函数可共享相同的表单模型定义,增强维护性。
#e 用户注册表单模型
现象:
在一个 Web 应用中,用户提交注册信息(用户名、邮箱、密码、确认密码),后端使用表单模型封装这些字段,统一校验并进行处理。FastAPI 将从请求中的表单数据中提取出每个字段的数据,并提供您定义的 Pydantic 模型。
特征对比
特征 | 是否满足 |
---|---|
使用 Pydantic 定义结构 | ✅ 包含多个字段 |
所有字段使用 Form(…) | ✅ 显式声明 |
数据源为表单 | ✅ 来自 HTML <form> |
数据仍是简单键值对 | ✅ 没有嵌套结构 |
from fastapi import FastAPI, Form
from pydantic import BaseModel
app = FastAPI()
# 表单模型定义
class RegisterForm(BaseModel):
username: str = Form(...)
email: str = Form(...)
password: str = Form(...)
confirm_password: str = Form(...)
# 使用模型接收表单数据
@app.post("/register")
async def register_user(form: Annotated[RegisterForm,Form()):
if form.password != form.confirm_password:
return {"error": "Passwords do not match"}
return {"message": f"User {form.username} registered successfully"}
#e 禁止表单额外字段
在某些特殊使用情况下(可能并不常见),可能希望将表单字段限制为仅在 Pydantic 模型中声明过的字段,并禁止任何额外的字段。
可以使用 Pydantic 的模型配置来禁止(
forbid
)任何额外(extra
)字段:
from typing import Annotated
from fastapi import FastAPI, Form
from pydantic import BaseModel
app = FastAPI()
class FormData(BaseModel):
username: str
password: str
model_config = {"extra": "forbid"}
@app.post("/login/")
async def login(data: Annotated[FormData, Form()]):
return data
如果客户端尝试发送这样的表单字段:
username
:Rick
password
:Portal Gun
extra
:Mr. Poopybutthole
将收到一条错误响应,表明字段extra
是不被允许的:
{
"detail": [
{
"type": "extra_forbidden",
"loc": ["body", "extra"],
"msg": "Extra inputs are not permitted",
"input": "Mr. Poopybutthole"
}
]
}
2.请求文件
#d 请求文件
在 FastAPI 中,请求文件是指客户端通过 HTTP 请求(通常使用 multipart/form-data
)上传的文件数据。FastAPI 提供 File(...)
类型来接收和处理上传的文件内容。该机制支持用户通过表单或 API 客户端上传图片、文档、音频等任意二进制文件,FastAPI 会自动将文件封装为 UploadFile
类型对象,供开发者读取、保存或处理内容。
重要特征:
- 特征1:传输格式必须为
multipart/form-data
与传统表单不同,请求文件必须使用multipart
类型支持文件内容传输。 - 特征2:使用
File(...)
显式声明参数类型
FastAPI 使用File(...)
指示框架此参数应作为文件上传接收,而非表单字段或 JSON。 - 特征3:接收类型为
UploadFile
(推荐)或bytes
推荐使用UploadFile
,可异步处理文件流,避免一次性读取全部内容。 - 特征4:常见于上传图像、文档、语音、模型文件等场景
多用于媒体数据或用户提交的资料、证件等内容。
#e 头像上传接口(正例)
现象:
一个社交平台允许用户上传头像图片,客户端通过表单上传图像文件到服务器,后端通过 File(...)
和 UploadFile
获取文件内容并存储。
特征对比
特征 | 是否满足 |
---|---|
使用 multipart/form-data | ✅ 表单上传图像格式正确 |
使用 File(…) 声明 | ✅ 后端显式使用 File(...) |
使用 UploadFile 类型 | ✅ 使用异步文件句柄 |
适用于文件内容 | ✅ 上传图像符合用途 |
说明:
- 上传格式必须为
multipart/form-data
; UploadFile
提供异步文件操作能力;file.filename
可获取原始文件名;- 使用
shutil.copyfileobj
进行文件保存。
from fastapi import FastAPI, File, UploadFile
import shutil
app = FastAPI()
@app.post("/upload-avatar/")
async def upload_avatar(file: UploadFile = File(...)):
with open(f"uploads/{file.filename}", "wb") as buffer:
shutil.copyfileobj(file.file, buffer)
return {"filename": file.filename, "message": "Upload successful"}
#e JSON请求体base64编码(反例)
现象:
某客户端将图片文件 base64 编码后嵌入 JSON 中提交,如:
{
"filename": "photo.png",
"data": "iVBORw0KGgoAAAANSUhEUgAA..."
}
后端用 BaseModel
读取并解码数据。
特征对比
特征 | 是否满足 |
---|---|
使用 multipart/form-data | ❌ 使用的是 application/json |
使用 File(…) 声明 | ❌ 使用的是 Body 和普通模型 |
文件类型 UploadFile | ❌ 实际为字符串字段 |
适用于原始文件上传 | ❌ 不推荐用 JSON 携带大量文件内容 |
#e bytes与UploadFile
如果把_路径操作函数_参数的类型声明为
bytes
,FastAPI 将以bytes
形式读取和接收文件内容。这种方式把文件的所有内容都存储在内存里,适用于小型文件。
UploadFile
与 bytes
相比有更多优势:
- 使用
spooled
文件:- 存储在内存的文件超出最大上限时,FastAPI 会把文件存入磁盘;
- 这种方式更适于处理图像、视频、二进制文件等大型文件,好处是不会占用所有内存;
- 可获取上传文件的元数据;
- 自带 file-like
async
接口; - 暴露的 Python
SpooledTemporaryFile
对象,可直接传递给其他预期「file-like」对象的库。
from fastapi import FastAPI, File, UploadFile
app = FastAPI()
@app.post("/files/")
async def create_file(file: bytes = File()):
return {"file_size": len(file)}
@app.post("/uploadfile/")
async def create_upload_file(file: UploadFile):
return {"filename": file.filename}
#c 属性 Uploaded的属性
UploadFile
的属性如下:
filename
:上传文件名字符串(str
),例如,myimage.jpg
;content_type
:内容类型(MIME 类型 / 媒体类型)字符串(str
),例如,image/jpeg
;file
:SpooledTemporaryFile
( file-like 对象)。其实就是 Python文件,可直接传递给其他预期file-like
对象的函数或支持库。
UploadFile
支持以下 async
方法,(使用内部 SpooledTemporaryFile
)可调用相应的文件方法:
write(data)
:把data
(str
或bytes
)写入文件;read(size)
:按指定数量的字节或字符(size
(int
))读取文件内容;seek(offset)
:移动至文件offset
(int
)字节处的位置;- 例如,
await myfile.seek(0)
移动到文件开头; - 执行
await myfile.read()
后,需再次读取已读取内容时,这种方法特别好用;
- 例如,
close()
:关闭文件。
因为上述方法都是 async
方法,要搭配「await」使用。例如,在 async
路径操作函数 内,要用以下方式读取文件内容:contents = await myfile.read()
在普通 def
路径操作函数 内,则可以直接访问 UploadFile.file
,例如:contents = myfile.file.read()
#c 细节 技术细节
使用
async
方法时,FastAPI 在线程池中执行文件方法,并await
操作完成。FastAPI 的
UploadFile
直接继承自 Starlette 的UploadFile
,但添加了一些必要功能,使之与 Pydantic 及 FastAPI 的其它部件兼容。不包含文件时,表单数据一般用
application/x-www-form-urlencoded
「媒体类型」编码。
但表单包含文件时,编码为 multipart/form-data
。使用了 File
,FastAPI 就知道要从请求体的正确位置获取文件。
#e 可选文件上传
通过使用标准类型注解并将 None 作为默认值的方式将一个文件参数设为可选:
from fastapi import FastAPI, File, UploadFile
app = FastAPI()
@app.post("/files/")
async def create_file(file: bytes | None = File(default=None)):
if not file:
return {"message": "No file sent"}
else:
return {"file_size": len(file)}
@app.post("/uploadfile/")
async def create_upload_file(file: UploadFile | None = None):
if not file:
return {"message": "No upload file sent"}
else:
return {"filename": file.filename}
#e 设置额外元数据
将 File()
与 UploadFile
一起使用,设置额外的元数据:
from fastapi import FastAPI, File, UploadFile
app = FastAPI()
@app.post("/files/")
async def create_file(file: bytes = File(description="A file read as bytes")):
return {"file_size": len(file)}
@app.post("/uploadfile/")
async def create_upload_file(
file: UploadFile = File(description="A file read as UploadFile"),
):
return {"filename": file.filename}
#e 多文件上传
FastAPI 支持同时上传多个文件。可用同一个「表单字段」发送含多个文件的「表单数据」。上传多个文件时,要声明含 bytes
或 UploadFile
的列表(List
),接收的也是含 bytes
或 UploadFile
的列表(list
)。
from fastapi import FastAPI, File, UploadFile
from fastapi.responses import HTMLResponse
app = FastAPI()
@app.post("/files/")
async def create_files(files: list[bytes] = File()):
return {"file_sizes": [len(file) for file in files]}
@app.post("/uploadfiles/")
async def create_upload_files(files: list[UploadFile]):
return {"filenames": [file.filename for file in files]}
@app.get("/")
async def main():
content = """
<body>
<form action="/files/" enctype="multipart/form-data" method="post">
<input name="files" type="file" multiple>
<input type="submit">
</form>
<form action="/uploadfiles/" enctype="multipart/form-data" method="post">
<input name="files" type="file" multiple>
<input type="submit">
</form>
</body>
"""
return HTMLResponse(content=content)
也可以使用 from starlette.responses import HTMLResponse
。
fastapi.responses
其实与 starlette.responses
相同,只是为了方便开发者调用。实际上,大多数 FastAPI 的响应都直接从 Starlette 调用。
#e 带元数据的多文件上传
可以为 File()
设置额外参数, 即使是 UploadFile
from fastapi import FastAPI, File, UploadFile
from fastapi.responses import HTMLResponse
app = FastAPI()
@app.post("/files/")
async def create_files(
files: list[bytes] = File(description="Multiple files as bytes"),
):
return {"file_sizes": [len(file) for file in files]}
@app.post("/uploadfiles/")
async def create_upload_files(
files: list[UploadFile] = File(description="Multiple files as UploadFile"),
):
return {"filenames": [file.filename for file in files]}
@app.get("/")
async def main():
content = """
<body>
<form action="/files/" enctype="multipart/form-data" method="post">
<input name="files" type="file" multiple>
<input type="submit">
</form>
<form action="/uploadfiles/" enctype="multipart/form-data" method="post">
<input name="files" type="file" multiple>
<input type="submit">
</form>
</body>
"""
return HTMLResponse(content=content)
#e 请求表单与文件
FastAPI 支持同时使用 File
和 Form
定义文件和表单字段,创建文件和表单参数的方式与 Body
和 Query
一样,文件和表单字段作为表单数据上传与接收,声明文件可以使用 bytes
或 UploadFile
。
from fastapi import FastAPI, File, Form, UploadFile # 导入Field与Form
app = FastAPI()
@app.post("/files/")
async def create_file(
file: bytes = File(), fileb: UploadFile = File(), token: str = Form()
): # 定义Field与Form参数
return {
"file_size": len(file),
"token": token,
"fileb_content_type": fileb.content_type,
}