首页 / FastAPI 入门教程 / 表单数据与文件上传

FastAPI 入门教程

表单数据与文件上传

本教程共 50 篇 · 第 13 篇 · 更新于 2026-08-12 · 约 8 分钟阅读

FastAPIFastAPI 入门教程表单Form文件上传UploadFile

本节目标:分清表单数据和 JSON 请求体的区别,学会用 Form 接收表单字段,用 File 和 UploadFile 处理文件上传,并理解多文件与 multipart 编码。

前面的请求体都是 JSON。但还有一种常见场景:网页上的登录框、文件选择按钮,它们提交的数据不是 JSON,而是「表单」。FastAPI 用 FormFile 来接这些数据。

13-1 表单数据和 JSON 的区别

HTML 表单提交数据时,编码方式和 JSON 完全不同。这一点决定了你不能用 Pydantic 模型去接表单。

对比项JSON 请求体表单数据
Content-Typeapplication/jsonapplication/x-www-form-urlencoded
声明方式Pydantic BaseModelForm()
数据结构支持嵌套对象和数组扁平的键值对
常见场景前后端分离的接口网页表单提交、文件上传

表单数据是一组扁平的键值对,不支持嵌套。所以通常用 Form() 逐字段声明。新版 FastAPI(0.113 及以上)也支持把一个扁平的 Pydantic 模型传给 Form(),框架会把模型字段平铺成表单键值对;但表单本质仍是扁平结构,不支持嵌套对象。

Warning

表单字段和 JSON 请求体不能在同一接口里混用。因为两者的编码方式不同,一个请求体要么是表单,要么是 JSON,不可能同时是两种。这是 HTTP 协议的规定,不是 FastAPI 的限制。

13-2 先装 python-multipart

用表单和文件之前,必须装一个依赖:python-multipart。FastAPI 靠它来解析表单编码的数据。

python -m pip install python-multipart

装好之后,才能正常使用 FormFile。否则运行时会报错提示缺少这个包。

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}

这段代码声明了两个必填表单字段:usernamepassword。客户端提交表单时,FastAPI 会从中取出对应的值。

Form(...) 表示必填,Form(None) 表示可选。和之前学的 QueryField 用法一致。

打开 /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_lengthmax_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 表单与文件混合

在同一次提交里,既能传文件,也能传普通表单字段。只需把 FormFile 写在一起。

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 的方法要 awaitread()seek()close()
  • 多文件用 list[UploadFile];文件和表单字段可一起用 multipart/form-data
  • 客户端上传文件时,表单要设 enctype="multipart/form-data"

到这里,请求相关的核心数据形式——路径参数、查询参数、请求体、表单、文件——你都已经掌握。