$ ls ~yifei/notes/

FastAPI 传统表单和上传文件

Posted on:

Last modified:

FastAPI 中默认的数据格式是 JSON,但是有时还是需要使用传统的 Web 表单,比如上传文件。让我们 来简单看下吧。

表单

from fastapi import Form

@app.post("/login")
async def login(username: str=Form(...), password: str=Form(...)):
    auth(username, password)
    return username

其中参数的名字要和 html 中 input 标签的 name 对应上。

上传文件

如果要在 HTML 页面中上传文件,那么文件只能通过表单的形式进行传送。在 FastAPI 中,使用 File 对象来表示一个文件。

from fastapi import File, UploadFile

# 这里的 file 是 bytes 类型,会直接进内存,适合临时文件
@app.route("/files")
def create_file(file: bytes = File(...)):
    ...

# 注意 file 的类型区别,这种方式会混存到硬盘,还可以读取文件名等更多属性
@app.route("/files2")
def create_file2(file: UploadFile = File(...)):
    ...

其中参数的名字也要和 <input type="file" name="xxx"> 中的名字对应上。

如果文件类型声明为 bytes,那么文件会直接缓存到内存中,可以直接用 io.BytesIO 打开读取。 如果文件类型定义为 UploadFile 类型,那么当文件过大的时候 FastAPI 会在硬盘中缓存文件。 使用 bytes 当然方便,但是如果每个用户都在上传 2G 大小的文件,可能你的内存一会儿就爆了。

特别注意, FastAPI 0.44.1 引入了一个 bug,导致使用 bytes 参数必须得放到第一个位置才行。 不过使用 UploadFile 则没有影响。

UploadFile 的属性和方法有:

file.filename  # 用户上传的原始文件名,注意一定要做过滤,不能相信任何用户数据
file.content_type  # 文件类型,比如 image/jpeg
file.file: SpooledTemporaryFile  # 真正打开的文件对象 

def get_valid_filename(s):
    """过滤文件名的一个方法,来自 django"""
    s = str(s).strip().replace(' ', '_')
    return re.sub(r'(?u)[^-\w.]', '', s)

# 一些异步读写方法
await file.write(data)
await file.read(size)
await file.seek(offset)
await file.close()

# 如果是在一个同步的 handler 中,可以直接使用对应的 file 属性的方法,不需要 await
data = file.file.read()

如果你使用的不是 async 函数的话,可以使用 UploadFile.file 属性对应的方法访问, SpooledTemporaryFile 是标准库中的一个类型。

Form 和 File 可以一起使用,因为他们都使用相同的编码方式一起发送给服务器,但是不能和 Body 或者 BaseModel 共用,因为编码方式不兼容。一个是 form,一个是 json.

参考

  1. https://docs.python.org/3/library/tempfile.html#tempfile.SpooledTemporaryFile
  2. https://stackoverflow.com/questions/65504438/how-to-add-both-file-and-json-body-in-a-fastapi-post-request
  3. https://github.com/tiangolo/fastapi/issues/1964

© 2016-2022 Yifei Kong. Powered by ynotes

All contents are under the CC-BY-NC-SA license, if not otherwise specified.

Opinions expressed here are solely my own and do not express the views or opinions of my employer.