1+1=10

记记笔记,放松一下...

Python FastAPI入门笔记(三)

前面了解如何从URL中获取路径参数和查询参数,接下来看看,如何获取客户端(浏览器)发送给服务端的数据。这个数据称为请求体或请求正文(Request Body)。

请求体在 POST(常用)、PUT、DELETE或PATCH 方法下使用。GET方法没有请求体。

内容小记:

  • JSON请求体处理,与路径参数和查询参数的区分
  • JSON多个请求体处理,含多请求体中单一值处理
  • Annotated注解 Path、Query、Body、Field
  • GET方式获取表单数据
  • POST方式获取表单数据,单文件上传,多文件上传

注:本文代码在python3.11下验证

JSON格式请求体

FastAPI 对 JSON 看的很重,手册中请求体部分上来就是JSON。

使用 Pydantic

解读JSON请求体之前,

  • 先用pydantic的BaseModel定义一个对应的模型
  • 而后和路径参数、查询参数一样,直接加在路径处理函数中。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from fastapi import FastAPI
from pydantic import BaseModel

class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float = 0.13

app = FastAPI()

@app.post("/items/")
async def create_item(item: Item):
    return item

测试1

POST方法,不好直接用浏览器进行测试。写一个客户端代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import requests

url = 'http://127.0.0.1:8001/items/'

headers = {
    'accept': 'application/json',
    'Content-Type': 'application/json'
}

data = {
    "name": "string",
    "description": "string",
    "price": 10
}

response = requests.post(url, headers=headers, json=data)

print(response.json())

结果正常。注意我们忽略了有默认值的tax参数。另外,由于description是可选参数,也可以省略:

1
2
3
4
data = {
    "name": "string",
    "price": 10
}

测试2

不手写客户端,直接使用FastAPI的docs界面会更简单,访问

  • 127.0.0.1:8001/docs

image-20231227145708894

点击执行,它后台执行curl指令:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
curl -X 'POST' \
  'http://127.0.0.1:8001/items/' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "name": "string",
  "description": "string",
  "price": 0,
  "tax": 0.13
}'

并显示结果:

image-20231227145946504

嵌入式请求体

前面例子中,请求体的数据是这样的:

1
2
3
4
5
{
    "name": "string",
    "description": "string",
    "price": 10
}

FastAPI还支持这样的写法(嵌入一层):

1
2
3
4
5
6
7
{
    "item": {
        "name": "string",
        "description": "string",
        "price": 10
    }
}

要使得这个可以工作,需要使用Body进行注解(设置其embed参数):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from fastapi import FastAPI, Body
from pydantic import BaseModel
from typing import Annotated

class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float = 0.13

app = FastAPI()

@app.post("/items/")
async def create_item(item: Annotated[Item, Body(embed=True)]):
    return item

这个和前面遇到的:路径参数使用Path注解,查询参数使用Query注解。概念是一样的。

变量处理逻辑

如果都放置到一块,FastAPI如何区分,路径参数、请求参数 与 请求体??

  • 如果在路径中也声明了该参数,它将被用作路径参数。
  • 如果参数属于单一类型(比如 intfloatstrbool 等)它将被解释为查询参数。
  • 如果参数的类型被声明为一个 Pydantic 模型,它将被解释为请求体

一个混用的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from fastapi import FastAPI
from pydantic import BaseModel

class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float = 0.13

app = FastAPI()

@app.post("/items/{item_id}")
async def create_item(item_id: int, item: Item, q: str|None=None):
    return item
  • item_id:路径参数
  • q:查询参数,可省略
  • item:请求体

添加额外信息 Field

类似于Path、Query、Body,使用Field可以给pydantic BaseModel的属性添加更多信息

1
2
3
4
5
6
7
class Item(BaseModel):
    name: str
    description: str | None = Field(
        default=None, title="The description of the item", max_length=300
    )
    price: float = Field(gt=0, description="The price must be greater than zero")
    tax: float | None = None

这简化数据校验工作

JSON多请求体

前面遇到都是单个请求体:

不管是:

1
2
3
4
5
{
    "name": "string",
    "description": "string",
    "price": 10
}

还是嵌入方式:

1
2
3
4
5
6
7
{
    "item": {
        "name": "string",
        "description": "string",
        "price": 10
    }
}

与嵌入方式有点像,FastAPI支持多个请求体。

多个请求体例子

假定我们请求体格式如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{
    "item": {
        "name": "item1",
        "price": 10.0,
        "tax": 0.13
    },
    "user": {
        "username": "debao",
        "full_name": "1+1=10"
    }
}

程序代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from fastapi import FastAPI
from pydantic import BaseModel

class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float = 0.13

class User(BaseModel):
    username: str
    full_name: str | None = None

app = FastAPI()

@app.post("/items/")
async def create_item(item: Item, user: User ):
    return item

单一值

如果请求体不是一个对象,而是一个单一值,也就是不是pydantic模型时,如何处理??

和前面嵌入式单体一样,需要使用Body进行注解。

程序:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from fastapi import FastAPI, Body
from pydantic import BaseModel
from typing import Annotated

class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float = 0.13

app = FastAPI()

@app.post("/items/")
async def create_item(item: Item, important: Annotated[bool, Body()]):
    return item

可以处理的json文件:

1
2
3
4
5
6
7
8
9
{
  "item": {
    "name": "string",
    "description": "string",
    "price": 0,
    "tax": 0.13
  },
  "important": true
}

表单数据GET

使用GET方法发送表单数据比较简单。直接通过查询参数Query进行获取即可。

一个完整例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
from fastapi import FastAPI
from fastapi.responses import HTMLResponse

app = FastAPI()

body = '''
      <form action = "/login" method = "get">
         <p>Enter Name:</p>
         <p><input type = "text" name = "user" /></p>
         <p><input type = "submit" value = "submit" /></p>
      </form>
      '''

@app.get("/", response_class=HTMLResponse)
async def index():
    return body

@app.get("/login")
async def login(user: str):
    return {"username": user}

表单数据POST

HTML表单向服务器发送数据通常使用特殊编码,而不是JSON。

使用HTTP 的 POST时,enctype属性可以设置:

  • application/x-www-form-urlencoded :默认。所有字符发送前进行编解码
  • multipart/form-data:如要上传文件,需使用
  • text/plain:不建议

需要安装multipart:pip install python-multipart

例子1-表单

首先需要告诉FastAPI,需要处理的是表单数据。

和Query、Path、Body等注解参数类似,此处使用Form参数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
from fastapi import FastAPI, Form
from fastapi.responses import HTMLResponse
from typing import Annotated

app = FastAPI()

body = '''
      <form action = "/login" method = "post">
         <p>Enter Name:</p>
         <p><input type = "text" name = "user" /></p>
         <p><input type = "submit" value = "submit" /></p>
      </form>
      '''

@app.get("/", response_class=HTMLResponse)
async def index():
    return body

@app.post("/login")
async def login(user: Annotated[str, Form()]):
    return {"username": user}

注:

  • 变量名和表单中的字段名要对应
  • 标注用的 Form继承自 Body
  • 老版本Python,没有Annotated支持,使用user: str = Form() 写法,注意区分

例子2-文件上传

上传文件需要通过表单进行。

FastAPI提供的File用于变量类型注解,它将文件内容存入bytes类型的变量中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
from fastapi import FastAPI, File
from fastapi.responses import HTMLResponse
from typing import Annotated

app = FastAPI()

body = '''
          <form action = "/files" method = "post" enctype='multipart/form-data' >
             <p>Enter File Name:</p>
             <p><input type = "file" name = "file1" /></p>
             <p><input type = "submit" value = "submit" /></p>
          </form>
        '''

@app.get("/", response_class=HTMLResponse)
async def index():
    return body

@app.post("/files")
async def create_files(file1: Annotated[bytes, File()]):
    return {"file size": len(file1)}

另外,也可以使用UploadFile,功能更强一些:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
from fastapi import FastAPI, UploadFile
from fastapi.responses import HTMLResponse

app = FastAPI()

body = '''
          <form action = "/files" method = "post" enctype='multipart/form-data' >
             <p>Enter File Name:</p>
             <p><input type = "file" name = "file1" /></p>
             <p><input type = "submit" value = "submit" /></p>
          </form>
        '''

@app.get("/", response_class=HTMLResponse)
async def index():
    return body

@app.post("/files")
async def create_files(file1: UploadFile):
    return {"file size": file1.size, "file name": file1.filename, "file type": file1.content_type}

UploadFile

UploadFile 的属性如下:

  • filename:上传文件名字符串(str),例如, myimage.jpg
  • content_type:内容类型(MIME 类型 / 媒体类型)字符串(str),例如,image/jpeg
  • file: 就是 Python文件,可直接传递给其他预期 file-like 对象的函数或支持库。

UploadFile 支持以下 async 方法,可调用相应的文件方法。

  • write(data):把 datastrbytes)写入文件;
  • read(size):按指定数量的字节或字符(size (int))读取文件内容;
  • seek(offset) :移动至文件 offset 字节处的位置;
  • close():关闭文件。

注:

  • 在async路径操作函数内,上述async方法要配置await使用。
  • 在def路径操作函数内,可以直接访问Upload.file 对象

例子3-多文件上传

与单个文件文件,只需要指定注解类型时bytes或UploadFile的list即可

比如,下面例子中使用 files: list[UploadFile]

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
from fastapi import FastAPI, UploadFile
from fastapi.responses import HTMLResponse

app = FastAPI()

body = '''
          <form action = "/files" method = "post" enctype='multipart/form-data' >
             <p>Enter File Name:</p>
             <p><input type = "file" name = "files" multiple/></p>
             <p><input type = "submit" value = "submit" /></p>
          </form>
        '''

@app.get("/", response_class=HTMLResponse)
async def index():
    return body

@app.post("/files")
async def create_files(files: list[UploadFile]):
    return {"filenames": [file.filename for file in files]}

例子4-表单与文件

一个表单内可以同时由文件和其他参数。

同时在路径操作函数中使用File、UploadFile、Form 进行注解即可。

1
2
3
@app.post("/files")
async def create_files(file1: UploadFile, file2: Annotated[bytes, File()], user: Annotated[str, Form()]):
    return {"Hello": user}

请求参数

可用于Annotated的特殊函数

  • Query()
  • Path()
  • Body()
  • Form()
  • File()
  • Cookie()
  • Header()

可以直接从fastapi中导入:

1
from fastapi import Body, Cookie, File, Form, Header, Path, Query

参考

  • https://fastapi.tiangolo.com
  • https://docs.pydantic.dev/latest/
  • https://fastapi.tiangolo.com/reference/parameters/

Python web