首页 > 技术文章 > FasAPI教程

duoba 2021-01-04 15:04 原文

为什么要有API:

如果我们想要获取一篇博客,输入http://localhost:9000/blog/123,就可以看到id为123的博客页面,但这个结果是HTML页面
它同时混合包含了两个部分的数据:博文的内容数据该页面的展示数据(html、js、css)
如下图所示:

对于用户来说,看上去是没有问题的。但是对于开发人员来说,是有麻烦的。很难从HTML中解析出博文的内容数据

WEB API示例

http://localhost:8000/api/blogs/123
如下图所示:

该URL返回的不是HTML,而是博文数据,这个URL就可以看成是一个Web API。

简单的说就是: api 是可以直接提取干货的!

REST就是一种设计API的模式。最常用的返回数据格式是JSON。由于JSON能直接被JavaScript读取,所以,以JSON格式编写的REST风格的API具有简单、易读、易用的特点。

编写API有什么好处呢?由于API就是把Web App的功能全部封装了,所以,通过API操作数据,可以极大地把前端和后端的代码隔离,使得后端代码易于测试,前端代码编写更简单。

一个API也是一个URL的处理函数,我们希望能直接通过一个@api来把函数变成JSON格式的REST API。

python怎么写api? --FastAPI

FastApi是PythonWeb框架的新晋干将!
它内部使用了 Python 的 Async 异步,因此速度很快, 也要求必须是 Py3.6+
Async 不同于之前自己实现的 gevent ,Async是官方写的, 听说Py4将会大量使用来提高效率

FastApi官网展示了FastApi的特点
官方文档地址:https://fastapi.tiangolo.com/

  • 快速:非常高的性能,看齐的NodeJS和Go(感谢Starlette和Pydantic)。现有最快的Python框架之一。
  • 快速编码:将功能开发速度提高约200%至300%。
  • 更少的错误:减少约40%的人为错误(开发人员)。
  • 直观:强大的编辑器支持。完成无处不在。调试时间更少。
  • 简易:旨在易于使用和学习。减少阅读文档的时间。
  • 短:最小化代码重复。每个参数声明中的多个功能。更少的错误。
  • 健壮:获取可用于生产的代码。具有自动交互式文档。
  • 基于标准:基于(并完全兼容)API的开放标准:OpenAPI(以前称为Swagger)和JSON Schema。

安装

先安装最基础的组件
安装fastapi
pip install fastapi
安装应用服务器
pip install uvicorn

官方推荐了可以与 FastApi 搭配的几个模块, 基本上涵盖了几个大的使用场景

由Pydantic使用:
ujson-用于更快的JSON “解析”。
email_validator -用于电子邮件验证。

由Starlette使用:
requests-如果要使用,则为必填TestClient。
aiofiles-如果要使用FileResponse或,则为必填StaticFiles。
jinja2 -如果要使用默认模板配置,则为必需。
python-multipart-如果要使用来支持表单“解析”,则为必填request.form()。
itsdangerous-需要SessionMiddleware支持。
pyyaml-Starlette SchemaGenerator支持所必需的(FastAPI 可能不需要它)。
graphene-需要GraphQLApp支持。
ujson-如果要使用,则为必需UJSONResponse。

由FastAPI / Starlette使用:
uvicorn -用于加载和服务您的应用程序的服务器。
orjson-如果要使用,则为必需ORJSONResponse。

您可以一次安装所有 这些
pip install fastapi[all]

写第一个demo

from fastapi import FastAPI
app = FastAPI()
@app.get("/")  # 监听GET请求
def read_root():
    return {"Hello": "World"}

@app.get("/items/{item_id}")
def read_item(item_id: int, q: str = None):
    return {"item_id": item_id, "q": q}

或者

from fastapi import FastAPI
app = FastAPI()

@app.get("/")
async def read_root():
    return {"Hello": "World"}

@app.get("/items/{item_id}")
async def read_item(item_id: int, q: str = None):
    return {"item_id": item_id, "q": q}

运行

uvicorn main:app --reload
看到如下提示,证明运行成功

main: 表示app所在文件名
app:文件内创建的对象 app = FastAPI() 
reload:debug模式,可以自动重启

试着请求http://127.0.0.1:8000/items/5?q=somequery,会看到如下返回

里面的内容就是一个JSON格式的返回数据。

远程服务器部署 需要调整ip及端口号

ip要改为0.0.0.0,端口自定义
uvicorn的指令需要添加host参数和port参数:
uvicorn main:app --host '0.0.0.0' --port 8080 --reload
端口一定要是在云服务器安全组策略中开启的。

安装screen

要用screen来挂服务,以免终端关闭后服务终止
yum install screen

新建screen

screen -S fastapi

执行指令

uvicorn main:app --host '0.0.0.0'--port 8082 --reload

后台挂起screen

按CTRL + A + D组合键挂起。

然后关掉终端程序后依旧执行。

恢复screen

screen -ls # 查看screen列表

screen -r [screen会话号]

可以使用 postman 等调试工具测试接口,也可以直接用浏览器打开。

自动生成接口文档

FastApi 会自己给你生成接口文档, 真正的解放你的双手

FastApi 默认提供了两种接口文档, 其实内容一样, 只是使用了两个开源的文档框架

swagger

这得益于 swagger 的帮助, swagger的更多使用这里不再赘述

http://127.0.0.1:8000/docs 使用浏览器打开即可


在你更新代码时接口文档也会同步更新

redoc

得益于 redoc 的帮助, redoc的更多使用这里不再赘述

默认的文档位置在 http://127.0.0.1:8000/redoc 使用浏览器打开即可

在你更新代码时接口文档也会同步更新

更新代码

如果我们对代码进行了修改,保存之后服务器会自动重新启动服务

get传参

在 get 请求时,传递参数通常使用 url 拼接参数和 query 的方式传参

url传参

在 get 请求时,传递参数通常使用 url 拼接参数和 query 的方式传参

url传参

比如 http://127.0.0.1:8000/phone/123456

在 main.py 中增加函数

@app.get("/phone/{phone}")
async def get_phone(phone: int):
    return {"phone": phone}

这里强调一下, phone: int 是声明参数 phone 是 int 类型,这是 python 3.5+ 有的语法, 目的是增加对开发人员的友好度与IDE的提示
因我们在启动时设置了 --reload 所以我们在保存时项目会自己更新, 同时 docs 也会更新

我们访问url查看结果

{
    "phone": 123456
}

query传参

如: http://127.0.0.1:8000/user?user_id=12

在函数中是

@app.get("/user")
async def get_user(user_id: int):
    return {"user_id": user_id}

复合传参

如果一个请求中有两种方式
http://127.0.0.1:8000/user/12?mod=login

对应的是

@app.get("/user/{user_id}")
async def user_mod(user_id: int, mod: str = None):  # str = None 代表mod参数可以为空
    return {
        "user_id": user_id,
        "mod": mod
    }

返回

{
    "user_id": 12,
    "mod": "login"
}

body传参

针对Body传参的情况, 其实也是以函数传参的形式, 但是考虑到传统的 form-data 传参方式字段很多, 我们可以采用 application/json 的方式, 并且定义一个参数类来管控参数

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class Item(BaseModel):  # 定义一个类用作参数
    name: str
    price: float
    is_offer: bool = None  # 该字段可为空

@app.put("/{item_id}")
async def update_item(item_id: int, item: Item):  # item需要与Item对象定义保持一致
    return {
        "item_name": item.name,
        "item_id": item_id
    }

这里接受 URL 参数与 Body 的 json 格式的参数

举个例子, 我们使用 postman 调试, Body里设置为 json 传递

{
	"name": "t1",
	"price": 12.8
}

这里注意我们没有传递 is_offer 因为可以为空

url为 http://127.0.0.1:8000/12 , 请求方式为 put, 结果为

{
    "item_name": "t1",
    "item_id": 12
}

与类型声明结合

如果你试过在以上几种接口中传递错误的参数或者少传递参数, 你会发现 FastApi 会自己返回错误给客户端,

比如在参数中定义了 id 为 int 的时候传递了 str

没错, FastApi 将其与 类型声明 结合在了一起, 这便是其另一个强大之处。

使用枚举来限定参数

可以使用枚举的方式来限定参数为某几个值之内才通过

from fastapi import FastAPI
from enum import Enum

class ModelName(str, Enum):
    alexnet = "alexnet"
    resnet = "resnet"
    lenet = "lenet"
app = FastAPI()

@app.get("/model/{model_name}")
async def get_model(model_name: ModelName):  # 限定参数必须是ModelName枚举中的
    if model_name == ModelName.alexnet:  # 枚举判断方法1
        return {
            "model_name": model_name
        }
    if model_name.value == "lenet":  # 枚举判断方法2
        return {
            "model_name": model_name
        }
    return {
        "model_name": model_name
    }

Fast错误返回

以上面为例, 如果你传递了不在枚举中的参数, Fast 会返回错误给客户端, 例如

{
    "detail": [
        {
            "loc": [
                "path",  # 出错的地方
                "model_name"  # 具体的参数
            ],
            "msg": "value is not a valid enumeration member; permitted: 'alexnet', 'resnet', 'lenet'",  # 出错信息
            "type": "type_error.enum",  # 出错type
            "ctx": {
                "enum_values": [
                    "alexnet",
                    "resnet",
                    "lenet"
                ]
            }
        }
    ]
}

URL传递路径参数

有时候需要传递路径参数给后端, 如果使用灵活的方式

@app.get("/files/{file_path:path}")
async def read_file(file_path: str):
    return {
        "file_path": file_path
    }

设置参数为 path 即可,注意 : 后不能加空格否则会404

因为我们匹配的 URL files本身后面加了 /, 如果我们想在传递时读取到的参数本身前面就带有 /, 写 // 即可, 例如: http://127.0.0.1:8000/files//home/johndoe/myfile.txt 读取到的就是 /home/johndoe/myfile.txt

参数默认值

对于需要分页的接口来说,通常需要传递 page 和 page_size , 而通常, 为了提高代码健壮性, 我们需要给 page 和 page_size 指定一个默认值, 在(一)中我们介绍了如何设置某个字段为可传, 现在我们在FastApi 中指定参数默认值的方法如下

@app.get("/log")
async def get_logs(page: int = 1, page_size: int = 20):
    return {
        "page": page,
        "page_size": page_size
    }

测试不带参数 http://127.0.0.1:8000/log

返回默认值

{
    "page": 1,
    "page_size": 20
}

测试带参数 http://127.0.0.1:8000/log?page=2

返回

{
    "page": 2,
    "page_size": 20
}

post传参

FastAPI 定义请求体,需要 Pydantic 模型。你需要从pydantic中导入BaseModel。

from pydantic import BaseModel

创建数据类型然后,声明你的数据模型为一个类,且该类继承 BaseModel.

# 创建数据模型
class Item(BaseModel):
    name: str
    description: str = None
    price: float
    tax: float = None

服务端完整demo:

# -*- coding: utf-8 -*-
# author:laidefa

# 载入包
import uvicorn
from fastapi import FastAPI
from pydantic import BaseModel

# 创建数据模型
class Item(BaseModel):
    name: str
    description: str = None
    price: float
    tax: float = None

app = FastAPI()

@app.get("/")
async  def root():
    return 'Hello World!'

@app.post("/sys/sinoma/v1/optimization/predict/fcao")
async def fcao_predict(item: Item):
    item_dict = item.dict()
    if item.tax:
        price_with_tax = item.price + item.tax
        item_dict.update({"price_with_tax": price_with_tax})
        return item_dict


if __name__ == '__main__':
    uvicorn.run(app)

客户端demo:

# -*- coding: utf-8 -*-
# author:laidefa

# 载入包
import requests
import json
import time

params={
    "name": "Foo",
    "description": "An optional description",
    "price": 45.2,
    "tax": 3.5
}


url='http://127.0.0.1:8000/sys/sinoma/v1/optimization/predict/fcao'

time1=time.time()
html = requests.post(url, json.dumps(params))
print('发送post数据请求成功!')
print('返回post结果如下:')
print(html.text)

time2=time.time()
print('总共耗时:' + str(time2 - time1) + 's')

Form表单传参

我们之前讲了从 body 的 JSON 里获取参数, 这里说一下从传统的 Form 表单里获取数据(application/x-www-form-urlencoded/multipart/form-data)
想要从 Form 中获取数据,需要导入 Form 模块, 并且安装 python-multipart 包

from fastapi import FastAPI, Form
app = FastAPI()

@app.post("/login")
async def login(*, name: str = Form(...), pwd: str = Form(...)):  # 函数的接受参数第一个是 * 代表此函数只接受关键字传参
    # Form(...) 必写
    return {
        "name": name,
        "pwd": pwd
    }

运行结果如下:
发送post数据请求成功!
返回post结果如下:
{"name":"Foo","description":"An optional description","price":45.2,"tax":3.5,"price_with_tax":48.7}
总共耗时:0.004986763000488281s

本文参考文献:
https://www.cnblogs.com/sanduzxcvbnm/p/12195385.html
https://www.cnblogs.com/chnmig/p/12603895.html
https://blog.csdn.net/u013421629/article/details/104892975
感谢上述博主提供的内容!

推荐阅读