编辑
2024-07-15
技术
00
请注意,本文编写于 191 天前,最后修改于 170 天前,其中某些信息可能已经过时。

目录

环境
快速入门
一个最小的API
资源丰富的路由请求
参数携带在路由的url中
不同的请求方式
同一个方法配置多个路由
请求解析
参数解析
校验入参格式
检验入参格式的继承写法
检验入参格式实践
场景1 不做任何变动
场景2 增加required判定为True
场景3 增加action='append'
场景4 增加dest='public_name'
场景5 增加location
自定义格式校验
输出字段
基本用法
补充属性
重命名 attribute
默认值 default
自定义格式
复杂结构
将扁平结构转换为嵌套结构
展示列表结构
将嵌套结构展示为嵌套结构
扩展flask_restful
返回格式限制
资源方法装饰器
自定义错误
错误处理器
错误消息
结尾

最近工作上没什么事,打算梳理并且加深一下自己对于部分内容的理解
开发均基于vscode进行开发,打算从pycharm转入vscode了
该文章基于 http://www.pythondoc.com/Flask-RESTful/ 进行学习

环境

python
Python 3.8.14 Flask 3.0.3 Flask-RESTful 0.3.10

快速入门

一个最小的API

python
from flask import Flask # from flask.ext.restful import Api, Resource 旧版本适用 from flask_restful import Api, Resource app = Flask(__name__) api = Api(app) class HelloWorld(Resource): def get(self): return {'hello': 'world'} api.add_resource(HelloWorld, '/') if __name__ == '__main__': app.run(debug=True)

资源丰富的路由请求

参数携带在路由的url中

python
class TodoSimple(Resource): def get(self, todo_id): return todo_id api.add_resource(TodoSimple, '/<string:todo_id>')

该方式直接以ip:port/todo1的url进行访问,则TodoSimple.get方法接收到的todo_id为string格式的todo1

不同的请求方式

python
class HelloWorld(Resource): def get(self): return {'hello': 'world'} def post(self): return {'hello': 'world'} def put(self): return {'hello': 'world'} def delete(self): return {'hello': 'world'} api.add_resource(HelloWorld, '/')

可以调用ip:port/的get/post/put/delete四种请求方式

同一个方法配置多个路由

python
class HelloWorld(Resource): def get(self): return {'hello': 'world'} api.add_resource(HelloWorld, '/a', '/b')

此时既可以通过ip:port/a访问,也可以通过ip:port/b访问

请求解析

参数解析

python
request.method 返回请求方法 request.args 返回url中的请求参数数据(key-value),主要是GET请求,请求参数在url中.也即请求链接中?后⾯的所有参数 #获取get请求的数据 request.args.get('a') 返回url中参数a的值,数据来源是url地址 # 获取get请求指定的值 request.form 返回form表单中的数据(key-value),原理跟request.args差不多,只是request.args数据来源是url,request.form的数据来源是表单 # 获取post请求的数据 request.form.get('username') 返回表单中key为username的值,也即获取在html页面中定义的元素的name值对应的输入值 # 获取post请求指定的值 request.cookies 返回cookies信息 request.cookies.get('') #返回某个具体的cookie值 request.headres 返回请求头信息 request.data 如果处理不了的数据就变成字符串儿存在data里面 request.files 返回上传或下载的文件信息 request.path 返回请求文件路径:/myapplication/page.html request.base_url 返回域名与请求文件路径:http://www.baidu.com/myapplication/page.html request.url 返回全部url:http://www.baidu.com/myapplication/page.html?id=1&edit=edit # 包括get参数部分 request.url_root 返回域名:http://www.baidu.com/ # 包括端口部分

校验入参格式

python
from flask_restful import reqparse parser = reqparse.RequestParser() parser.add_argument('name', type=str, help='name length mush more than 2') args = parser.parse_args() # type=str 校验数据格式 str int werkzeug.datastructures.FileStorage # help显示错误信息 # required=True 是否是必要参数 # action='append' 该参数是否有多个值 # dest='public_name' 修改存储的key值 # location='form' 指定该参数的来源 from args headers cookies files 如果多个来源则配置为 location=['headers', 'values']

检验入参格式的继承写法

python
parser = RequestParser() parser.add_argument('foo', type=int) parser_copy = parser.copy() parser_copy.add_argument('bar', type=int) # parser_copy has both 'foo' and 'bar' parser_copy.replace_argument('foo', type=str, required=True, location='json') # 'foo' is now a required str located in json, not an int as defined # by original parser parser_copy.remove_argument('foo') # parser_copy no longer has 'foo' argument

检验入参格式实践

样例代码

python
from flask import Flask, request from flask_restful import Api, Resource, reqparse app = Flask(__name__) api = Api(app) class NameChekc(Resource): def post(self): parser = reqparse.RequestParser() parser.add_argument('name', type=str, help='name length mush more than 2') args = parser.parse_args() print(args) return args api.add_resource(NameChekc, '/namecheck') if __name__ == '__main__': app.run(debug=True)

场景1 不做任何变动

shell
curl "127.0.0.1:5000/namecheck?name=args" -X post -d "name=form" { "message": "Did not attempt to load JSON data because the request Content-Type was not 'application/json'." }

以json格式获取数据失败,我们在请求代码上加入对应的content-type

shell
curl "127.0.0.1:5000/namecheck?name=args" -X post -d "name=form" -H "Content-Type:application/json" { "message": "Failed to decode JSON object: Expecting value: line 1 column 1 (char 0)" }

那就将-d的数据修改成json格式

shell
curl "127.0.0.1:5000/namecheck?name=args" -X post -d '{"name":"json"}' -H "Content-Type:application/json" { "name": "json" }

注意,我们参数中包含了get形式的name参数,但是并没有获取到(或者说被覆盖了),只显示了json方式的结果

shell
curl "127.0.0.1:5000/namecheck?name=args" -X post -d '{}' -H "Content-Type:application/json" { "name": "args" }

去掉json部分数据之后发现是可以获取到args的,也就是其内部获取存在某种顺序,后者会覆盖前者

shell
curl "127.0.0.1:5000/namecheck" -X post -d '{}' -H "Content-Type:application/json" { "name": null }

什么参数都不传入则显示null

场景2 增加required判定为True

python
parser.add_argument('name', type=str, help='name length mush more than 2', required=True)

之前尝试之前的空参数形式

shell
curl "127.0.0.1:5000/namecheck" -X post -d '{}' -H "Content-Type:application/json" { "message": { "name": "name length mush more than 2" } }

如预期中的显示了help部分的文字内容

场景3 增加action='append'

python
parser.add_argument('name', type=str, help='name length mush more than 2', action='append')

该场景主要为了校验单值有多个的情况,我们设置两个args参数和两个json参数

shell
curl "127.0.0.1:5000/namecheck?name=args1&name=args2" -X post -d '{"name": "json1", "name": "json2"}' -H "Content-Type:application/json" { "name": [ "json2", "args1", "args2" ] }

显然json中的数据会直接根据key去重,而args中的两个数据都会保留

场景4 增加dest='public_name'

python
parser.add_argument('name', type=str, help='name length mush more than 2', dest='public_name')

同上的多数据场景

shell
curl "127.0.0.1:5000/namecheck?name=args1&name=args2" -X post -d '{"name": "json1", "name": "json2"}' -H "Content-Type:application/json" { "public_name": "json2" }

输出的key发生了改变

场景5 增加location

python
parser.add_argument('name', type=str, help='name length mush more than 2', location='args') curl "127.0.0.1:5000/namecheck?name=args1&name=args2" -X post { "name": "args1" }

由于限制了location的原因,没有再去从json进行数据获取,所以终于可以去掉对应的headers了,再进行多location的尝试

python
parser.add_argument('name', type=str, help='name length mush more than 2', location=['args', 'form']) curl "127.0.0.1:5000/namecheck?name=args1&name=args2" -X post -d "name=form" { "name": "args1" }

location中args居前,因此获取到了args数据,这里有两种逻辑可能性,我个人更倾向于第一种

  1. 从前向后,先获取到了则不再向后获取
  2. 从后向前,先获取到的会被后获取到的覆盖掉

加入json获取的方式进行进一步排查

python
parser.add_argument('name', type=str, help='name length mush more than 2', location=['args', 'form', 'json']) curl "127.0.0.1:5000/namecheck?name=args1&name=args2" -X post -d "name=form" { "message": "Did not attempt to load JSON data because the request Content-Type was not 'application/json'." }

所以可以完全排除第一种可能性,至少说会显示第一个获取到的,但是每一个都会去尝试获取

总结一下 使用参数校验的时候尽量设置好location,不然可能因为会尝试解析json部分的参数而出现错误

自定义格式校验

上文在说关于格式校验方面列举了str,int,werkzeug.datastructures.FileStorage三种格式,实际上还可以通过自定义数据格式的方式来进行数据校验,参考代码如下

python
from flask import Flask from flask_restful import Api, Resource, reqparse app = Flask(__name__) api = Api(app) def long_str(value, name): # 设置的自定义校验方法 print(value, name) if len(value) > 10: raise ValueError("the length of {0} must <= 10, but len('{1}') > 10".format(name, value)) return value class Todo(Resource): def get(self): parser = reqparse.RequestParser() parser.add_argument("name", type=long_str, location="args") args = parser.parse_args() return args api.add_resource(Todo, '/todo') if __name__ == '__main__': app.run(debug=True)

我们对args中的参数name进行了长度校验,如果超过10则提示错误,否则正常验证
具体验证效果如下

shell
curl "127.0.0.1:5000/todo?name=123123123123" { "message": { "name": "the length of name must <= 10, but len('123123123123') > 10" } } ##################################### curl "127.0.0.1:5000/todo?name=123" { "name": "123" }

输出字段

核心目的是对返回的输出结果进行格式化过滤

基本用法

python
import datetime from flask import Flask from flask_restful import Api, Resource, fields, marshal_with app = Flask(__name__) api = Api(app) resource_fields = { 'name': fields.String, 'address': fields.String, 'date_updated': fields.DateTime(dt_format='iso8601'), } class Obj: def __init__(self, name, address, sex): self.name = name self.address = address self.sex = sex self.date_updated = datetime.datetime.now() class Todo(Resource): @marshal_with(resource_fields, envelope='data') def get(self): return Obj("jack", "shanghai", "male") api.add_resource(Todo, '/todo') if __name__ == '__main__': app.run(debug=True)

执行并且请求一下,返回的结果是

shell
curl 127.0.0.1:5000/todo { "data": { "name": "jack", "address": "shanghai", "date_updated": "2024-07-15T17:56:09.064912" } }

其中格式内的data是由配置在marshal_with中的envelope参数决定的
另外,返回格式也支持dict形式的,以及list形式的
对应的get方法返回值和请求的返回值如下

python
# 返回dict格式的数据 class Todo(Resource): @marshal_with(resource_fields, envelope='data') def get(self): return dict(name='jack', address='shanghai', sex='male') # 调用之后的返回值如下,date_updated为null是因为未传入数据 { "data": { "name": "jack", "address": "shanghai", "date_updated": null } } # 返回list格式的数据 class Todo(Resource): @marshal_with(resource_fields, envelope='data') def get(self): return [Obj("jack", "shanghai", "male"), Obj("mary", "beijing", "female")] # 调用之后的返回值如下,自动转换成了list格式 { "data": [ { "name": "jack", "address": "shanghai", "date_updated": "2024-07-15T18:03:23.118613" }, { "name": "mary", "address": "beijing", "date_updated": "2024-07-15T18:03:23.118674" } ] }

补充属性

重命名 attribute

resource_fields的设置改为如下,则取数的时候会用name进行取数,返回的结果显示还是firstname形式

python
resource_fields = { 'firstname': fields.String(attribute='name'), 'address': fields.String, 'date_updated': fields.DateTime(dt_format='iso8601'), } ########请求示例####### curl 127.0.0.1:5000/todo { "data": { "firstname": "jack", "address": "shanghai", "date_updated": "2024-07-16T09:20:03.965606" } }

默认值 default

当想要获取到的值未获取到的情况下,则显示默认值 将resource_fields的设置改为如下

python
resource_fields = { 'name': fields.String, 'address': fields.String, 'date_updated': fields.DateTime(dt_format='iso8601'), 'age': fields.String(default='18') } ########请求示例####### curl 127.0.0.1:5000/todo { "data": { "name": "jack", "address": "shanghai", "date_updated": "2024-07-16T09:22:43.567608", "age": "18" } }

自定义格式

数据中实际存储的是该用户的当前年龄,而显示需要显示该用户的出生年份,因此需要对存储的数据进行一定的计算并且输出
这种情况下我们需要自定义一个格式化函数来继承fields.Raw类

python
# 自定义格式化函数 class BirthYearItem(fields.Raw): def format(self, value): return 2024 - int(value) # 这里假定存储的是string格式的年龄 # 设置fields resource_fields = { 'name': fields.String, 'address': fields.String, 'date_updated': fields.DateTime(dt_format='iso8601'), 'birthyear': BirthYearItem(default='1900', attribute='age') # 从key=age中获取值,如果没有的话则默认为'1900', 也就是说这里的default针对的是最终返回的结果数据而不是获取到的原始数据 } # 设置 Obj对象 class Obj: def __init__(self, name, address, sex, age): self.name = name self.address = address self.sex = sex self.date_updated = datetime.datetime.now() self.age = age # 路由方法返回数据 class Todo(Resource): @marshal_with(resource_fields, envelope='data') def get(self): # return Obj("jack", "shanghai", "male") return [Obj("jack", "shanghai", "male", '24'), Obj("mary", "shanghai", "male", None)] ########请求示例####### curl 127.0.0.1:5000/todo { "data": [ { "name": "jack", "address": "shanghai", "date_updated": "2024-07-16T09:32:39.154740", "birthyear": 2000 }, { "name": "mary", "address": "shanghai", "date_updated": "2024-07-16T09:32:39.154786", "birthyear": "1990" } ] }

这里我们额外参考一下fields.Raw的源码

python
class Raw(object): """Raw provides a base field class from which others should extend. It applies no formatting by default, and should only be used in cases where data does not need to be formatted before being serialized. Fields should throw a :class:`MarshallingException` in case of parsing problem. :param default: The default value for the field, if no value is specified. :param attribute: If the public facing value differs from the internal value, use this to retrieve a different attribute from the response than the publicly named value. """ def __init__(self, default=None, attribute=None): self.attribute = attribute self.default = default def format(self, value): """Formats a field's value. No-op by default - field classes that modify how the value of existing object keys should be presented should override this and apply the appropriate formatting. :param value: The value to format :exception MarshallingException: In case of formatting problem Ex:: class TitleCase(Raw): def format(self, value): return unicode(value).title() """ return value def output(self, key, obj): """Pulls the value for the given key from the object, applies the field's formatting and returns the result. If the key is not found in the object, returns the default value. Field classes that create values which do not require the existence of the key in the object should override this and return the desired value. :exception MarshallingException: In case of formatting problem """ value = get_value(key if self.attribute is None else self.attribute, obj) if value is None: return self.default return self.format(value)

可以看出来如下几点

  1. 本质上format函数是被output函数进行调用的,所以直接重写output函数更为精确,并且可以进行一些复合运算,比如将firstname和lastname合并成一个字符串等
  2. default是作为直接return的结果,因此也确实如上所说default针对的是最终返回的结果数据而不是获取到的原始数据
  3. key的本质是去获取value,如果在特定的场合想可以不去使用key,而是直接操作obj获取数据并且进行组合,不过这可能一种源码建议的书写方式

并且在源码文件中我们也看到了几个写好的记成了Raw类的子类
Nested,List,String,Integer,Boolean,FormattedString,Url,Float,Arbitrary,DateTime,Fixed
同时也有DateTime类中所支持的日期格式化标准rfc822和iso8601
当然不建议修改源码的形式来修改日期格式化结果,更建议使用自定义Raw子类的方式
这里不对这些子类做展开说明(下一小节会说明一下Nested和List),有需要的可以自行去查看源码

复杂结构

将扁平结构转换为嵌套结构

python
# 原生数据结构 class Obj: def __init__(self, name, address, father, mother): self.name = name self.address = address self.father = father self.mother = mother # fields设置结构 resource_fields = { 'name': fields.String, 'address': fields.String, 'relation': { 'father': fields.String, 'mother': fields.String } } # 返回数据结构 class Todo(Resource): @marshal_with(resource_fields, envelope='data') def get(self): return Obj('jack', 'shanghai', 'jack father', 'jack mother') ########请求示例####### curl 127.0.0.1:5000/todo { "data": { "name": "jack", "address": "shanghai", "relation": { "father": "jack father", "mother": "jack mother" } } }

展示列表结构

python
# 原生数据结构 class Obj: def __init__(self, name, address, father, mother): self.name = name self.address = address self.relation = [father, mother] # fields设置结构 resource_fields = { 'name': fields.String, 'address': fields.String, 'relation': fields.List(fields.String) } # 返回数据结构 class Todo(Resource): @marshal_with(resource_fields, envelope='data') def get(self): return Obj('jack', 'shanghai', 'jack father', 'jack mother') ########请求示例####### curl 127.0.0.1:5000/todo { "data": { "name": "jack", "address": "shanghai", "relation": [ "jack father", "jack mother" ] } }

将嵌套结构展示为嵌套结构

python
# 原生数据结构 class Obj: def __init__(self, name, address, father, mother): self.name = name self.address = address self.relation=dict(father=father, mother=mother) # fields设置结构 resource_fields = { 'name': fields.String, 'address': fields.String, 'relation': fields.Nested({ 'father': fields.String, 'mother': fields.String }) } # 返回数据结构 class Todo(Resource): @marshal_with(resource_fields, envelope='data') def get(self): return Obj('jack', 'shanghai', 'jack father', 'jack mother') ########请求示例####### curl 127.0.0.1:5000/todo { "data": { "name": "jack", "address": "shanghai", "relation": { "father": "jack father", "mother": "jack mother" } } }

!!!重点说明!!!
扁平结构/嵌套结构转换为嵌套结构,区别是取数方式的问题,扁平结构取数直接根据key去取即可,不需要考虑层级问题;而嵌套结构取数需要多个key进行迭代取数,因此需要增加Nested类进行嵌套。

扩展flask_restful

返回格式限制

限制格式必须为json形式的flask的response

python
from flask import Flask, make_response from flask_restful import Api app = Flask(__name__) api = restful.Api(app) @api.representation('application/json') def output_json(data, code, headers=None): resp = make_response(json.dumps(data), code) resp.headers.extend(headers or {}) return resp

资源方法装饰器

利用Resource的method_decorators属性,我们可以对Resource中的所有method添加装饰器,可以用在一些认证以及日志输出上,我的样例代码是这个样子的

python
from flask import Flask from flask_restful import Api, Resource, wraps app = Flask(__name__) api = Api(app) def decorator1(func): @wraps(func) def wrapper(*args, **kwargs): print("1", func, args, kwargs) return func(*args, **kwargs) return wrapper def decorator2(func): @wraps(func) def wrapper(*args, **kwargs): print("2", func, args, kwargs) return func(*args, **kwargs) return wrapper class Todo(Resource): method_decorators = [decorator1, decorator2] def get(self, todo_id): return todo_id def post(self, todo_id): return todo_id api.add_resource(Todo, '/todo/<todo_id>') if __name__ == '__main__': app.run(debug=True)

在实际请求进行调用的时候,结果如下

shell
curl "127.0.0.1:5000/todo/123?name=123" "123" #######vscode控制台输出如下====== 2 <function Todo.get at 0x10aa72700> () {'todo_id': '123'} 1 <bound method Todo.get of <__main__.Todo object at 0x10aa96550>> () {'todo_id': '123'} curl "127.0.0.1:5000/todo/123?name=123" -X post "123" #######vscode控制台输出如下====== 2 <function Todo.post at 0x10aa72820> () {'todo_id': '123'} 1 <bound method Todo.post of <__main__.Todo object at 0x10aa966d0>> () {'todo_id': '123'}

显然当同时配置了多个装饰器的时候,调用顺序是自后向前的,并且通过控制台的输出我们会发现,func第一次在装饰器中被输出是function,结果第二次被输出已经变成了bound method,这是由于其已经被实例化了的原因,后续有时间可以专门再写一篇博客说明
这里的设置是使得method_decorators中的装饰器对所有的method方法均生效,也可以设置单独对某些method生效的格式,如下所示

python
# 将method_decorators部分的配置修改成如下形式 method_decorators = {"get": [decorator1], "post": [decorator2]} # 即 decorator1对get方法生效,而decorator2对post方法生效 #######get请求的控制台输出====== 1 <bound method Todo.get of <__main__.Todo object at 0x10aa736a0>> () {'todo_id': '123'} #######post请求的控制台输出====== 2 <bound method Todo.post of <__main__.Todo object at 0x10aa73520>> () {'todo_id': '123'}

可以发现,这里的func的输出都是bound method,实际不管list里面有几个装饰器,总是第一个装饰器(即最后执行的一个)才会显示为bound method,其余的均为function

自定义错误

待完成

错误处理器

错误消息

结尾

flask_restful只是基于flask扩展的一小部分功能,通过对该部分的整理梳理发现自己对于flask还有很多不了解的地方,准备开一个关于flask的专栏,深入理解一下该框架,最终深入源码进行阅读
专栏地址为 https://doc.kangen.fun:7894/web/#/12/0 ,并且已将其放置在博客顶部的专栏菜单里

本文作者:康恩

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 Copyright © 2024 KangEn 许可协议。转载请注明出处!