13-flask博客项目之restful api详解1-概念
英文:https://flask-restful.readthedocs.io/en/latest/
中文:http://www.pythondoc.com/Flask-RESTful/quickstart.html
一个英文一个中文
使用 pip
安装 Flask-RESTful:
pip install flask-restful
开发的版本可以从 GitHub 上的页面 下载
git clone https://github.com/twilio/flask-restful.git
cd flask-restful
python setup.py develop
Flask-RESTful 有如下的依赖包(如果你使用 pip
,依赖包会自动地安装):
Flask-RESTful 要求 Python 版本为 2.6, 2.7, 或者 3.3。
我这里装的0.3.8
调试程序需要用到postman
postman使用:https://blog.csdn.net/fxbin123/article/details/80428216/
一、Postman背景介绍
用户在开发或者调试网络程序或者是网页B/S模式的程序的时候是需要一些方法来跟踪网页请求的,用户可以使用一些网络的监视工具比如著名的Firebug等网页调试工具。今天给大家介绍的这款网页调试工具不仅可以调试简单的css、html、脚本等简单的网页基本信息,它还可以发送几乎所有类型的HTTP请求!Postman在发送网络HTTP请求方面可以说是Chrome插件类产品中的代表产品之一。
1> 、postman下载地址:
https://www.getpostman.com/apps
使用它访问一下百度
我们看下面的请求地址,请求方式
请求结果
用post发送geti请求,对这个url
上面请求200,我们还可以看请求头
保存是将请求结果保存到一个文件中
我们随便填个信息登录,可以看到是发送了ajax的请求,类型是xhr。当滑动验证的时候,也是发送了verify的ajax请求
我们可以看他登录的请求地址和类型。我们可以看到,手机上提交的form表单都是post请求
我们可以看到表单数据携带邮箱密码
我们这里用的是英文的,导入的模块路径不同,应该是版本不同吧
from flask import Flask
from flask_restful import Resource, Api
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)
我们需要将restful这个第三方组件加入到我们的项目中,它是跟db加进来是一样的
实例化api对象
api对象初始化app
定义api蓝图,并添加api前缀
注册蓝图
创建并修改数据库。其它不用变
app改成这个样子的
我们导入资源
导入之后,我们点击resource进入查看,它就有一个dispatch_request方法
Resource类继承了MethodView类,我们点进去看下这个类
MethodView里面也有dispatch_request这个方法。它有继承了使用元类。我们可以看到下面写着要定义get post等方法
from flask import Blueprint
from flask_restful import Resource, marshal_with, fields
from exts import api
user_bp = Blueprint('user', __name__, url_prefix='/api')
from apps.user.model import User
user_fields = {
'id': fields.Integer,
'username': fields.String,
'password': fields.String,
'udatetime': fields.DateTime
}
class UserResource(Resource):
# get 请求的处理
@marshal_with(user_fields)
def get(self):
users = User.query.all()
# userList = []
# for user in users:
# userList.append(user.__dict__)
return users
# post
def post(self):
return {'msg': '------>post'}
# put
def put(self):
return {'msg': '------>put'}
# delete
def delete(self):
return {'msg': '------>delete'}
api.add_resource(UserResource, '/user')
api示例
我们在蓝图中如下操作:
从组件中导入资源,定义视图类,然后让它继承资源类,在自己定义的视图类下面定义了get ,post,put,delete四个方法,分别对于查增改删四个功能。
给我们的视图类添加路由,通过调用之前在exts目录init文件中从组件导入的api类生成的api对象,然后api对象点增加资源方法 。将我们定义的视图类加进去,后面是路由字符串。这样就给我们定义的视图类增添了路由映射了。
from flask import Blueprint
from flask_restful import Resource
from exts import api
user_bp = Blueprint('user', __name__, url_prefix='/api')
class UserResource(Resource):
# get 请求的处理
def get(self):
return {'msg': '------>get'}
# post
def post(self):
return {'msg': '------>post'}
# put
def put(self):
return {'msg': '------>put'}
# delete
def delete(self):
return {'msg': '------>delete'}
api.add_resource(UserResource, '/user')
启动程序,我们从浏览器访问一下,当我们get请求路由的时候,找的这个视图类,然后找到get请求,执行get方法,响应这个字典。前端接收到并在浏览器页面显示粗来。这里添加了api前缀,但是这里不需要添加api前缀,就能访问到这里的视图类,如果添加上了反而无法访问到,可能是因为没有其它蓝图中有相同的路由的原因吧,所以用不上
我们在postman中也是可以访问
我们四个请求方式都使用了,这里目前都是可以请求到对应方式的t数据
当请求一个未在视图类中定义的请求方法时,报错
我们先定义一个模型,然后迁移数据
添加两个用户,
我们在用户查询方法里面查出这个用户对象列表,然后返回这个用户对象列表。再看postman请求
此时,不能直接返回用户对象列表。报错没有json序列化
我们改成这样还是不行
这样也不行
官网是这样做的
当我们添加marshal_with装饰器,装饰器传参用户字典,用户字段字典里面定义用户表表中有的字段,每个字段的类型是什么,这样再遇到执行视图装饰器装饰下的get方法时,返回用户对象,就能将用户对象的信息像用户字段字典定义的格式查出来并响应给客户端。这里只响应一个用户对象,返回的是个字典。
当我去掉所以,返回用户对象列表后,响应结果是每一个用户对象一个字典,所有用户对象字典组成一个用户对象列表。每个用户对象返回的信息都是安装用户字段设置的格式返回的字段数据,从表中查出数据。这里字段名字和表字段名字保持一致的。字段类型也需要设置。从数据库中查询出的数据,会渲染到这些相同字段名称下。也就是。用户字段前面自己定义,后面应该必须是_fields命名。然后需要返回数据库对象的方法前面添加marshal_with装饰器,并把要响应的格式用户字段传进去,这样装饰器中会帮我们把用户对象列表中的每个用户对象按照user_fiels来生成响应数据。从而生成响应数据列表。只有一个用户对象,请求结果是一个字典,有多个用户时就是多个字典在一个列表里面
from flask import Blueprint
from flask_restful import Resource, marshal_with, fields
from exts import api
user_bp = Blueprint('user', __name__, url_prefix='/api')
from apps.user.model import User
user_fields = {
'id': fields.Integer,
'username': fields.String,
'password': fields.String,
'udatetime': fields.DateTime
}
class UserResource(Resource):
# get 请求的处理
@marshal_with(user_fields)
def get(self):
users = User.query.all()
# userList = []
# for user in users:
# userList.append(user.__dict__)
return users
# post
def post(self):
return {'msg': '------>post'}
# put
def put(self):
return {'msg': '------>put'}
# delete
def delete(self):
return {'msg': '------>delete'}
api.add_resource(UserResource, '/user')
如果在字段里面只定义了一个,那么get获取的数据也只有这一个。不会因为你响应的数据的字段多而变多。我们的数据库字段是比较多的,当我们想要只返回一两个字段时,那么只需要在xxfields里面定义指定要返回的字段就可以。
什么是RESTful架构:
(1)每一个URI代表一种资源;
(2)客户端和服务器之间,传递这种资源的某种表现层;
(3)客户端通过四个HTTP动词(GET,POST,PUT,DELETE,[PATCH]),对服务器端资源进行操作,实现"表现层状态转化"。
Postman
前后端分离:
前端: app,小程序,pc页面
后端: 没有页面,mtv: 模型模板视图 去掉了t模板。
mv:模型 视图
模型的使用:跟原来的用法相同
视图: api构建视图
步骤:
1. pip3 install flask-restful
2.创建api对象
api = Api(app=app)
api = Api(app=蓝图对象)
3.
定义类视图:
from flask\_restful import Resource
class xxxApi(Resource):
def get(self):
pass
def post(self):
pass
def put(self):
pass
def delete(self):
pass
4. 绑定
api.add\_resource(xxxApi,'/user')
中文的可以用来学习,可能包路径是不一样的,跟中文的
上面写了一个接口,这个接口返回的就是一个json数据,提供前端请求使用,
一个视图函数,按照继承的类是资源,我们可以称之为一个视图函数就是一个资源吧,而视图函数的请求路径的字符串映射,也就是路由,按官网翻译就是资源路由
jquery使用地址(ajax使用地址) :https://jquery.cuishifeng.cn/
我们在前面的基础上再添加一个类视图。上面哪个是对所有用户的操作,下面这个是对单个用户的操作,对单个用户的操作需要指定用户的id。也就是说当是同一张表时,我们或许可以根据前端需求,给同一个表添加多个不同的类视图,以返回不同的响应数据。这里就是所有用户和单个用户是分开的,也就是还要单独添加路由,这个路由是需要传参的。至于是否可以合并 ,看情况考虑。
我们需要添加单个用户的类视图,添加单个用户的增删改查四个功能,添加单个用户的类视图的访问路由。需要将单个 用户资源类视图作为添加路由的参数,然后后面填写映射的路径字符串。因为单个用户是需要传递用户id参数的,这里可以使用这种方式传参,但是类视图中每个方法需要定义接收这个传参的形参的
我们根据传进的用户id,将用户对象查出来然后响应回去,但是我们需要的响应的数据需要是json格式的。
因此我们需要给它添加marshal,将用户对象转成一个序列号对象,以之前定义的用户字段格式来响应。
然后 我们测试。可以发现,当我们不接参数的时候,访问的是上面返回所有用户的
当我们接上参数的时候,返回的是单个用户的
from flask import Blueprint
from flask_restful import Resource, marshal_with, fields
from exts import api
user_bp = Blueprint('user', __name__, url_prefix='/api')
from apps.user.model import User
user_fields = {
'id': fields.Integer,
'username': fields.String,
'password': fields.String,
'udatetime': fields.DateTime
}
class UserResource(Resource):
# get 请求的处理
@marshal_with(user_fields)
def get(self):
users = User.query.all()
# userList = []
# for user in users:
# userList.append(user.__dict__)
return users
# post
def post(self):
return {'msg': '------>post'}
# put
def put(self):
return {'msg': '------>put'}
# delete
def delete(self):
return {'msg': '------>delete'}
class UserSimpleResource(Resource):
@marshal_with(user_fields) # user转成一个序列化对象,
def get(self, id):
user = User.query.get(id)
return user # 不是str,list,int,。。。
def put(self, id):
pass
def delete(self, id):
pass
api.add_resource(UserResource, '/user')
api.add_resource(UserSimpleResource, '/user/
一个网址,后端是同样的,但是前端可以有不同的,如pc端,小程序,手机app等等。前端需要什么数据,后端需要前端传什么参数,需要前后端开发人员协商好
我们除了上面那种传参,还可以用问号方式传参
我们在这里打印一下url_map,就能打印出路由的信息
当我们请求一条路由的时候,就打印出路由信息
当我们不添加endpoint时,它用的时视图类名称小写做的endpoint,
当我们添加了endpoint之后,就叫我们修改后的那个名称,它指代的就是那条路由,可以使用endpoint来做反向解析
我们在单个用户的视图类put方法下打印一下all_user这个endpoint的反向解析,
postman请求正常
然后我们可以看到通过endpoint名称,可以解析出它对应的路由。也就是在别的地方,我们需要使用这个路由,就可以使用endpoint去做反向解析出它的路由来了
from flask import Blueprint, url_for
from flask_restful import Resource, marshal_with, fields,reqparse
from exts import api
user_bp = Blueprint('user', __name__, url_prefix='/api')
from apps.user.model import User
from exts import db
user_fields = {
'id': fields.Integer,
'username': fields.String,
'password': fields.String,
'udatetime': fields.DateTime
}
parser = reqparse.RequestParser() # 解析对象
parser.add_argument('username', type=str, required=True, help='必须输入用户名')
parser.add_argument('password', type=str , required=True, help='必须输入密码',
location=['form'])
parser.add_argument('phone', type=str)
class UserResource(Resource):
# get 请求的处理
@marshal_with(user_fields)
def get(self):
users = User.query.all()
# userList = []
# for user in users:
# userList.append(user.__dict__)
return users
@marshal\_with(user\_fields)
def post(self):
# 获取数据
args = parser.parse\_args()
username = args.get('username')
password = args.get('password')
phone = args.get('phone')
# 创建user对象
user = User()
user.username = username
user.password = password
if phone:
user.phone = phone
db.session.add(user)
db.session.commit()
return user
# put
def put(self):
return {'msg': '------>put'}
# delete
def delete(self):
return {'msg': '------>delete'}
class UserSimpleResource(Resource):
@marshal_with(user_fields) # user转成一个序列化对象,
def get(self, id):
user = User.query.get(id)
return user # 不是str,list,int,。。。
def put(self, id):
print('endpoint的使用:', url\_for('all\_user'))
return {'msg': 'ok'}
def delete(self, id):
pass
api.add_resource(UserResource, '/user',endpoint='all_user')
api.add_resource(UserSimpleResource, '/user/
我们想要请求这个路径的时候这个方式去携带很多个值
我们提交数据时带着数据
需要用到
我们在用户字段下面创建解析对象,然后添加参数。我们可以对前端的传参进行解析,进行校验。相当于form类的作用
我们可以看到,添加参数是请求解析类中的一个方法。
我们看下请求解析这个类的init方法,里面有些参数,我们没有设置的时候用的是这些默认参数。trim就是默认不做空字符的去除,bundle_errors就是,如果enabled,当第一个错误出来的时候,不会中止。继续往下校验所有的字段
给解析器添加参数
我们按照下面添加参数,如果前端有电话,后端没有电话参数,是不可以的,是不能多给的
添加参数,限制传参的类型,指明传参类型必须是整型。默认是字符串类型
比如我们的分页,必须是整型,这时就需要前端传递过来的必须是整型,所以我们可以像上面那样添加数据类型必须是整型才能成功提交,才能校验通过
我们给模型添加一个电话字段,迁移数据
然后添加参数,指定参数名称,类型,必填,帮助信息,就是报错信息
我们写好之后,前端就可以传数据了。后端视图函数中需要取值,就从parse_args里面取
我们导入reqpares和数据库
我们实例化解析对象,添加三个参数
当前端发送post请求,将三个数据传递到后端,我们在post中从解析器里面取数据
需要先调用解析参数方法,然后从这个对象中获取请求过来的form数据,将他们保存到数据库,再将这个对象返回给前端,因为需要返回给前端,所以需要是json序列化过的,需要添加上marshal_with装饰器。指定按照用户字段这个格式返回数据
我们在postman中添加数据,如果是文本的就用这个就行,如果是有文件的,选择form-data。这里是post请求,我们应该在body里面添加数据,而不是在params里面添加数据。这里添加键值对,就相当于你在form表单里面写入了数据。
当我们输入键值对,点击发送post请求之后。我们从解析器中获取到用户传过来的键值对,然后保存到数据库中,数据库中已经增加了这条数据了。post方法里面返回这个添加的用户,添加装饰器和指定返回的字段格式后,我们在postman中就接收到了保存下来的数据信息。
我们在params里面添加键值对,这样来发送post请求。也是可以接收到数据的,从而往下执行保存进入数据库。毕竟有的地方form表单post请求就是可以接问号拼接传参的,只要取得键值对对应上就行
我们在location参数里面添加form时,这样就限制了,表示form提交的数据里必须有这些字段,而我们是从params里面写的,那么相当于每天在body里面填写form数据,所以请求到达解析器就对数据做了校验,相当于没有填写username,就把错误信息返回给请求客户端了。这样前端可以根据这个判断,如果有错误信息就在前端渲染,否则就是用用户信息做啥渲染的。或者其它操作等等
我们导入input,里面使用正则校验,这样没有通过校验的字段,就会将帮助信息,也就是未校验通过的错误信息响应给客户端。其它字段,包括密码我们都可以设置校验。除了正则校验还可以使用其它校验方法。密码的正则校验,可以通过正则限制个数,这里是6到12位,我们提示信息也修改完整一点,提示是需要6到12位的数字
我们将手机号让它通过校验,我们可以看到能成功提交数据。
我们再get请求5号用户,将body键值对取消勾选,然后点击发送。我们就能得到5号用户的数据
复选框,我们需要添加action 是append
在post方法中我们get这个爱好。打印一下,这里没有添加数据库这个字段,只是看一下请求时这里接收的数据是怎样的
我们发送post请求,添加上多个值,键是一样的。
我们解析器定义接收的字段行为是追加,所以我们从参数解析器里面取的这个字段,是postman里面添加的多个值组成的一个列表。
换名字
添加头像字段,迁移数据
location有多种类型,
如果是文件上传,类型必须得填文件存储,
他们就对应我们从不同里面取值是一样的。像下面电话里面,是可以填写多个location的,这样支持多种数据提交
看下文件存储类,它里面也是用了之前我们使用的存储验证码图片用的字节io对象。
因为我们需要上传文件了,所以需要将数据修改位formdata。我们点击后面bulk edit
复制粘贴键值对到form-data中
再点击一下,粘贴到formdata里面键值对编辑。
这样就将键值对复制过来了
选中所有,点击块编辑
可以看到,没有双斜线//了,这说明没有选中使用的键值对是//这种注释,没有注释//的就是被选中需要使用的键值对。每个键值对都是冒号隔开,多个键值对换行分隔,这样我们就能批量添加,修改和删除键值对了。所以这个地方叫块编辑。而返回到之前的状态就是键值对编辑。描述信息在这里是怎么定义的一会看,我试了一下,键值对描述信息在块编辑里面不显示
我们添加文件存储参数,
from werkzeug.datastructures import FileStorage
parser.add_argument('icon', type=FileStorage, location=['files'])
post请求里面再获取头像文件
我们在后端post请求逻辑中,根据头像字段名称是从参数解析器里面获取头像文件,这是个文件存储对象。如果或者到头像文件对象,那么就保存在服务器上,然后将图片的相对路径写入到数据库中。
而前端是用postman,使用form data方式添加字段,发送post请求。将key修改为file类型。然后点击value里就可以将文件加进来,点击发送,就会请求到后端。最终走到post请求这个方法里,然后保存文件,保存文件路径到数据库,并返回这个用户对象的信息,按照之前定好的用户对象字段格式。
因为是按照这里定义的格式返回的数据,所以并没有返回图片字段的信息
参数继承
from flask_restful import reqparse
parser = reqparse.RequestParser()
parser.add_argument('foo', type=int)
parser_copy = parser.copy()
parser_copy.add_argument('bar', type=int)
parser_copy.replace_argument('foo', required=True, location='json')
parser_copy.remove_argument('foo')
如果添加bundle_errors=True,那么响应数据,会将所有字段都校验完,将校验结果返回给客户端。而如果没有添加的话,那么只会返回第一个校验失败的消息,其它错误消息不会返回,甚至可能都没有往下进行校验
from flask_restful import reqparse
parser = reqparse.RequestParser(bundle_errors=True)
parser.add_argument('foo', type=int, required=True)
parser.add_argument('bar', type=int, required=True)
{
"message": {
"foo": "foo error message",
"bar": "bar error message"
}
}
parser = RequestParser()
parser.add_argument('foo', type=int, required=True)
parser.add_argument('bar', type=int, required=True)
{
"message": {
"foo": "foo error message"
}
}
from flask_restful import reqparse
parser = reqparse.RequestParser()
parser.add_argument(
'foo',
choices=('one', 'two'),
help='Bad choice: {error_msg}'
)
{
"message": {
"foo": "Bad choice: three is not a valid choice",
}
}
输出字段可以设置复杂结构
Flask-RESTful 提供了一个简单的方式来控制在你的响应中实际呈现什么数据。使用 fields
模块,你可以使用在你的资源里的任意对象(ORM 模型、定制的类等等)并且 fields
让你格式化和过滤响应,因此您不必担心暴露内部数据结构。
当查询你的代码的时候,哪些数据会被呈现以及它们如何被格式化是很清楚的。
也就是我在这里减少一个字段,那么用户就看不到这个字段,如果添加上那么用户就能看到这个字段
你可以定义一个字典或者 fields
的 OrderedDict 类型,OrderedDict 类型是指键名是要呈现的对象的属性或键的名称,键值是一个类,该类格式化和返回的该字段的值。这个例子有三个字段,两个是字符串(Strings)以及一个是日期时间(DateTime),格式为 RFC 822 日期字符串(同样也支持 ISO 8601)
from flask_restful import Resource, fields, marshal_with
resource_fields = {
'name': fields.String,
'address': fields.String,
'date_updated': fields.DateTime(dt_format='rfc822'),
}
class Todo(Resource):
@marshal_with(resource_fields, envelope='resource')
def get(self, **kwargs):
return db_get_todo() # Some function that queries the db
这个例子假设你有一个自定义的数据库对象(todo
),它具有属性:name
, address
, 以及 date_updated
。该对象上任何其它的属性可以被认为是私有的不会在输出中呈现出来。一个可选的 envelope
关键字参数被指定为封装结果输出。
装饰器 marshal_with
是真正接受你的数据对象并且过滤字段。marshal_with
能够在单个对象,字典,或者列表对象上工作。
注意:marshal_with 是一个很便捷的装饰器,在功能上等效于如下的 return marshal(db_get_todo(), resource_fields), 200
。这个明确的表达式能用于返回 200 以及其它的 HTTP 状态码作为成功响应(错误响应见 abort
)。
给字段多加个括号,里面加上东西,看着没区别啊
很多时候你面向公众的字段名称是不同于内部的属性名。使用 attribute
可以配置这种映射。
fields = {
'name': fields.String(attribute='private_name'),
'address': fields.String,
}
没添加之前
添加之后
点进去
string和integer这些继承Raw
raw里面添加了attribute了,还有个默认,那么我们添加一个默认
这样实现了字段值的隐匿性。也实现了重命名字段值
我们也可以修改前端展示的字段名字。上面是没有修改,字段值跟模型的字段值名字是一样,所以前端展示的也是跟模型的字段值一样是username;下面我们就让前端看到的和模型字段名字不同,这样客户端请求后就不能获取到我们数据库真实的字段名称了,数据库也相对更安全,做法如下:用户字段这里修改为前端能看到的字段名称private_name,然后在字符串对象里面添加attribute属性,属性值使用这个字段对应的模型中字段的名称,这样将二者关联起来,后面数据就会使用这个字段的数据库的值了,后面还有个默认值,也就是没有值的话,就会用到这个默认值。
我们看了两个字段类型都是继承Raw这个类,没看过是否所有都继承Raw,不过猜测大部分都是继承Raw的,那么继承了的就会都可以添加attribute 和default两个参数,那么它们就都可以修改给前端的展示字段,隐匿数据库字段名,给请求客户端展示的都是我这里定义的字段值,而非数据库字段值。也就是前端看到的不一定就是数据库字段名
如下,如果不适应attribute来修改前端显示字段名,那么字段名默认是药和数据库字段名是保持一致的。不然是找不到数据的,找不到就显示null了
总结:
1.需要定义字典,字典的格式就是给客户端看的格式
user_fields = {
'id': fields.Integer,
'username': fields.String(default='匿名'),
'pwd': fields.String(attribute='password'),
'udatetime': fields.DateTime(dt_format='rfc822')
}
客户端能看到的是: id,username,pwd,udatetime这四个key
默认key的名字是跟model中的模型属性名一致,如果不想让前端看到命名,则可以修改
但是必须结合attribute='模型的字段名'
lambda 也能在 attribute
中使用
fields = {
'name': fields.String(attribute=lambda x: x._private_name),
'address': fields.String,
}
也可以使用属性访问嵌套属性
fields = {
'name': fields.String(attribute='people_list.0.person_dictionary.name'),
'address': fields.String,
}
如果由于某种原因你的数据对象中并没有你定义的字段列表中的属性,你可以指定一个默认值而不是返回 None
。
fields = {
'name': fields.String(default='Anonymous User'),
'address': fields.String,
}
有时候你有你自己定义格式的需求。你可以继承 fields.Raw
类并且实现格式化函数。当一个属性存储多条信息的时候是特别有用的。例如,一个位域(bit-field)各位代表不同的值。你可以使用 fields
复用一个单一的属性到多个输出值(一个属性在不同情况下输出不同的结果)。
这个例子假设在 flags
属性的第一位标志着一个“正常”或者“迫切”项,第二位标志着“读”与“未读”。这些项可能很容易存储在一个位字段,但是可读性不高。转换它们使得具有良好的可读性是很容易的。
自定义字段输出,需要继承Raw,然后重写format方法
class UrgentItem(fields.Raw):
def format(self, value):
return "Urgent" if value & 0x01 else "Normal"
class UnreadItem(fields.Raw):
def format(self, value):
return "Unread" if value & 0x02 else "Read"
fields = {
'name': fields.String,
'priority': UrgentItem(attribute='flags'),
'status': UnreadItem(attribute='flags'),
}
我们在模型里面添加一个布尔类型的数据
我们给1 5 7 设置为已经被删除的,其它默认就是未删除就是0状态
我们添加一个查询时将这个字段展示给前端。我们可以看到数据库中存储的是布尔值类型的,是0和1数据。客户端请求时时显示true和false。这里给前端展示的是用大写的D。我们想要让前端展示的内容根据数据库值不同显示其它的可读信息。0展示未删除,1展示已删除。这样我们需要添加一个判断
我们如官网样例,添加判断。
所以我们可以自己定义输出字段 。我们前面用的string和integer这些字段都是继承Raw类的,我们自己创建一个自定义输出字段,也需要继承Raw类,然后重写format方法,里面放一个value形参。然后在输出字段字典里添加一个展示给客户的字段,也是让它关联上isdelete模型字段。但是用的不是组件里面的输出字段类,而是我们自己写的isDelete类,因为它继承Raw类,所以我们也可以传参属性attribute,让我们定义的能跟isdelete模型字段关联起来。
再看看我们定义的输出字段类,里面的value就是模型中关联字段的值,存入的是0 1 布尔类型,所以打印出来是true和false,我们做了个判断,如果true返回什么字符串,否则返回什么字符串,这样就将输出修改掉了。数据库中的值是固定的两个,这里就能根据值来返回我们想要给前端展示的可读性好的一个字符串值。返回内容定制化
Flask-RESTful 包含一个特别的字段,fields.Url
,即为所请求的资源合成一个 uri。这也是一个好示例,它展示了如何添加并不真正在你的数据对象中存在的数据到你的响应中。
class RandomNumber(fields.Raw):
def output(self, key, obj):
return random.random()
fields = {
'name': fields.String,
# todo_resource is the endpoint name when you called api.add_resource()
'uri': fields.Url('todo_resource'),
'random': RandomNumber,
}
默认情况下,fields.Url
返回一个相对的 uri。为了生成包含协议(scheme),主机名以及端口的绝对 uri,需要在字段声明的时候传入 absolute=True
。传入 scheme
关键字参数可以覆盖默认的协议(scheme):
fields = {
'uri': fields.Url('todo_resource', absolute=True)
'https_uri': fields.Url('todo_resource', absolute=True, scheme='https')
}
就是生成一个访问地址,点击一下就能够访问它
from flask import Blueprint, url_for
from flask_restful import Resource, marshal_with, fields,reqparse,inputs
from exts import api
from werkzeug.datastructures import FileStorage
from settings import Config
import os
user_bp = Blueprint('user', __name__, url_prefix='/api')
from apps.user.model import User
from exts import db
class IsDelete(fields.Raw):
def format(self, value):
print('------------------>', value)
return '删除' if value else '未删除'
user_fields_1 = {
'id': fields.Integer,
'username': fields.String(default='匿名'),
'uri': fields.Url('single_user', absolute=True)
}
user_fields = {
'id': fields.Integer,
'private_name': fields.String(attribute='username',default='匿名'),
'password': fields.String,
'isDelete': fields.Boolean(attribute='isdelete'),
'isDelete1': IsDelete(attribute='isdelete'),
'udatetime': fields.DateTime(dt_format='rfc822')
}
parser = reqparse.RequestParser(bundle_errors=True) # 解析对象
parser.add_argument('username', type=str, required=True, help='必须输入用户名', location=['form'])
parser.add_argument('password', type=inputs.regex(r'^\d{6,12}$'), required=True, help='必须输入6~12位数字密码',
location=['form'])
parser.add_argument('phone', type=inputs.regex(r'^1[356789]\d{9}$'), location=['form'], help='手机号码格式错误')
parser.add_argument('hobby', action='append') # ['篮球', '游戏', '旅游']
parser.add_argument('icon', type=FileStorage, location=['files'])
class UserResource(Resource):
# get 请求的处理
@marshal_with(user_fields_1)
def get(self):
users = User.query.all()
# userList = []
# for user in users:
# userList.append(user.__dict__)
return users
@marshal\_with(user\_fields)
def post(self):
# 获取数据
args = parser.parse\_args()
username = args.get('username')
password = args.get('password')
phone = args.get('phone')
bobby = args.get('hobby')
print(bobby)
icon = args.get('icon')
print(icon)
# 创建user对象
user = User()
user.username = username
user.password = password
if icon:
upload\_path = os.path.join(Config.UPLOAD\_ICON\_DIR, icon.filename)
icon.save(upload\_path)
# 保存路径个
user.icon = os.path.join('upload/icon', icon.filename)
if phone:
user.phone = phone
db.session.add(user)
db.session.commit()
return user
# put
def put(self):
return {'msg': '------>put'}
# delete
def delete(self):
return {'msg': '------>delete'}
class UserSimpleResource(Resource):
@marshal_with(user_fields) # user转成一个序列化对象,
def get(self, id):
user = User.query.get(id)
return user # 不是str,list,int,。。。
def put(self, id):
print('endpoint的使用:', url\_for('all\_user'))
return {'msg': 'ok'}
def delete(self, id):
pass
api.add_resource(UserResource, '/user',endpoint='all_user')
api.add_resource(UserSimpleResource, '/user/
蓝图中所以代码
我们添加一个展示字段的字典,前面的是展示所有用户的,但是不展示用户所有字段,指包含用户名和每个用户详情访问地址,点击用户访问地址就能看到该用户的详情,所有字段;后面的给前端展示单个用户的,也就是用户详情,
查看所有用户和单个用户的视图类的get,要和对应字段响应格式绑定上。
用户详情的要传用户id。传参字段名要和模型中用户id字段名保持一致,之前使用uid但是报错了,名称对不上和模型中,这个id也可能是响应字段字典中对应的字段名,也就是给前端看的那个字段名,后面测试。传参进去,才能查出并返回单个用户对象
Url输出字段,第一个参数填字符串,是放用户详情的endpoint。这里使用absolute
网页测试:
访问所有用户,响应所有用户的,生成单个用户的访问地址。这个地址如何生成的呢?根据反向解析找的路径,然后根据传参id,在这里的id还是在模型里的id添加上的数字
点击id是1的地址打开一个postman请求标签页,就是用户1的访问地址,点击发送
成为走单个用户,走响应详情的字段格式。这样的话,像首页博客列表,每个博客详情页需要地址 ;商品列表,每个商品详情页需要地址;产品列表,每个产品详情页需要地址等等类似的,都可以这样来做
当我们将字段改为uid时
fields.Url根据endpoint反向解析路径,会找不到uid,因此路由添加的用户id传参和视图名称要和model里面用户id名称要一致才行。model里面是id,这里传参也是用id才能根据名字相同,在fields.Url反向解析时,将用户id拼接到url访问地址上,成为用户详情访问地址
你可以有一个扁平的结构,marshal_with 将会把它转变为一个嵌套结构
>>> from flask_restful import fields, marshal
import json
resource_fields = {'name': fields.String}
resource_fields['address'] = {}
resource_fields['address']['line 1'] = fields.String(attribute='addr1')
resource_fields['address']['line 2'] = fields.String(attribute='addr2')
resource_fields['address']['city'] = fields.String
resource_fields['address']['state'] = fields.String
resource_fields['address']['zip'] = fields.String
data = {'name': 'bob', 'addr1': '123 fake street', 'addr2': '', 'city': 'New York', 'state': 'NY', 'zip': '10468'}
json.dumps(marshal(data, resource_fields))
'{"name": "bob", "address": {"line 1": "123 fake street", "line 2": "", "state": "NY", "zip": "10468", "city": "New York"}}'
注意:address 字段并不真正地存在于数据对象中,但是任何一个子字段(sub-fields)可以直接地访问对象的属性,就像没有嵌套一样。
我们根据用户表构建一个朋友表,用户表是第一张表,第二张表还是用户表,这个朋友表相当于第三张表。 因为朋友也是用户,朋友的信息也是在用户表里面,只是朋友表里面存放的使用某个用户和其它用户的关系映射,是同一张表不同行之间的关系映射。这个应该是可以做成一对一和一对多 多对多的关系的,只要设定好下面那张表的外键uid和Fid是否是唯一键就可以实现。
在原有用户表上,添加朋友表和关系映射对象,迁移数据
我们给朋友表添加数据
我们添加用户朋友表的路由和视图类,这里只是查询,就只写get就好了
我们再看下用户朋友视图类,我们要查用户朋友,需要知道是查哪个用户的朋友,所以路由上需要添加上用户的id。视图类方法中需要接收这个用户id参数,因为用户的朋友还是用户,所以我们根据用户id从用户表中查到该用户的朋友列表。然后构造数据结构响应给请求客户端。需要响应的数据结构如下:要显示是哪个用户,有多少个朋友,然后是朋友列表里面每个朋友的信息,每个朋友就是一个用户详情的信息 。然后将这个数据结构返回给客户端。那么restful api中想要响应这种复杂的数据结构,是需要如何实现呢
那么朋友列表需要怎么做呢?friends是我们根据传进来的用户id在朋友表中查询来的该用户对应的所有朋友的id。但是我们需要给客户端传朋友的详情而不是朋友的id,所以需要根据所有朋友的id在用户表中找的所有朋友的用户对象。这里是遍历朋友id列表,然后在用户表中查出每个朋友对象并追加到朋友列表中。这样我们就有了朋友对象列表了,而返回客户端详情信息,我们一般都是通过传返回对象,然后根据输出字段格式将对象的所有字段显示给客户端的
这时我们请求一下试试,我们可以看到报错外键错误
在后端报错 foreign_keys,需要个外键参数
File "D:\softwareinstall\python3\lib\site-packages\sqlalchemy\util\compat.py", line 208, in raise_
raise exception
sqlalchemy.exc.AmbiguousForeignKeysError: Could not determine join condition between parent/child tables on relationship User.friends - there are multiple foreign key paths linking the tables. Specify th
e 'foreign_keys' argument, providing a list of those columns which should be counted as containing a foreign key reference to the parent table.
我们添加到这里试试
还是会报错
我们把朋友列表先注释试试
还是会报错
将关系映射字段先注释掉
get这里返回的是个字典,没有model对象。没有model对象的时候没有使用装饰器也是可以正常请求到的。如果有model对象返回,是需要添加装饰器然后绑定输出字段格式,那么这里需要怎么做呢,这里其它的已经正常输出给客户端了,我们需要将朋友对象列表也返回给客户端
如果我们直接返回朋友对象列表,是没有序列化的。
我们可以使用marshal加工一下。将model对象列表放到marshl中,然后指定列表中每个对象需要按照哪个输出字段格式usre_fields展示给客户端。这样就会将包含model对象的数据按指定格式去展示给客户端了。如果这里有多个model对象展示,那么我们可以构造含有多个model对象的数据结构,将每个model对象或对象列表都用一下marshal,并且定义好对象的返回格式。这里不能用marshal_with这个装饰器,因为marshal_with虽然能序列化model对象或者对象列表,但是不能格式化其它数据,这里的数据还包含了username,nums等等。要使用装饰器的方式需要另外做处理,后面讲
from flask_restful import marshal
'friends': marshal(friend_list,user_fields)
我们用页面访问一下。正是我们需要的样式
你也可以把字段解组(unmarshal)成列表
>>> from flask_restful import fields, marshal
import json
resource_fields = {'name': fields.String, 'first_names': fields.List(fields.String)}
data = {'name': 'Bougnazal', 'first_names' : ['Emile', 'Raoul']}
json.dumps(marshal(data, resource_fields))
'{"first_names": ["Emile", "Raoul"], "name": "Bougnazal"}'
尽管使用字典套入字段能够使得一个扁平的数据对象变成一个嵌套的响应,你可以使用 Nested
解组(unmarshal)嵌套数据结构并且合适地呈现它们。
>>> from flask_restful import fields, marshal
import json
address_fields = {}
address_fields['line 1'] = fields.String(attribute='addr1')
address_fields['line 2'] = fields.String(attribute='addr2')
address_fields['city'] = fields.String(attribute='city')
address_fields['state'] = fields.String(attribute='state')
address_fields['zip'] = fields.String(attribute='zip')resource_fields = {}
resource_fields['name'] = fields.String
resource_fields['billing_address'] = fields.Nested(address_fields)
resource_fields['shipping_address'] = fields.Nested(address_fields)
address1 = {'addr1': '123 fake street', 'city': 'New York', 'state': 'NY', 'zip': '10468'}
address2 = {'addr1': '555 nowhere', 'city': 'New York', 'state': 'NY', 'zip': '10468'}
data = { 'name': 'bob', 'billing_address': address1, 'shipping_address': address2}json.dumps(marshal_with(data, resource_fields))
'{"billing_address": {"line 1": "123 fake street", "line 2": null, "state": "NY", "zip": "10468", "city": "New York"}, "name": "bob", "shipping_address": {"line 1": "555 nowhere", "line 2": null, "state": "NY", "zip": "10468", "city": "New York"}}'
此示例使用两个嵌套字段。Nested
构造函数把字段的字典作为子字段(sub-fields)来呈现。使用 Nested
和之前例子中的嵌套字典之间的重要区别就是属性的上下文。在本例中 “billing_address” 是一个具有自己字段的复杂的对象,传递给嵌套字段的上下文是子对象(sub-object),而不是原来的“数据”对象。换句话说,data.billing_address.addr1
是在这里的范围(译者:这里是直译),然而在之前例子中的 data.addr1
是位置属性。记住:嵌套和列表对象创建一个新属性的范围。
如下,我们这里使用嵌套字段实现上面那个案例
user_fields = {
'id': fields.Integer(attribute='id'),
'private_name': fields.String(attribute='username',default='匿名'),
'password': fields.String,
'isDelete': fields.Boolean(attribute='isdelete'),
'isDelete1': IsDelete(attribute='isdelete'),
'udatetime': fields.DateTime(dt_format='rfc822')
}
user_friend_fields = {
'username': fields.String,
'nums': fields.Integer,
'friends': fields.List(fields.Nested(user_fields))
}
class UserFriendResource(Resource):
@marshal_with(user_friend_fields)
def get(self, id):
friends = Friend.query.filter(Friend.uid == id).all()
user = User.query.get(id)
friend\_list = \[\]
for friend in friends:
u = User.query.get(friend.fid)
friend\_list.append(u)
data = {
'username': user.username,
'nums': len(friends),
'friends': friend\_list # \[user,user,user\]
}
return data
如下,我们不用marshal了,用marshal_with。我们的输出数据还是data,data里面有friend_list这个model对象列表。把data返回去,那么需要将它交给marsh_with这个装饰器处理,这个装饰器里面需要指定user_friend_fields这个我们定义的输出字段格式,这个输出字段格式和我们在get方法里面定义的数据结构是一样的,但是有model对象的地方它是做了处理的。处理的方式就是使用fields.List(),将朋友对象列表,解组成朋友信息列表,然后指定user_fields这个输出字段格式来输出给客户端。而user_fields是之前定义的用户详情输出字段格式。
如果返回的数据中有多个model对象或者对象列表,那么应该也是可以这样做的,然后在外面定义一个同样数据结构的字段格式。将这个字段格式有model对象或对象列表的地方再使用下面的方式来嵌套其它输出字段格式。这就完成了复杂的数据结构输出。如果friends_list里面还要更复杂的嵌套那就一层层往里面嵌套,有时间去试试
什么是RESTful架构:
(1)每一个URI代表一种资源;
(2)客户端和服务器之间,传递这种资源的某种表现层;
(3)客户端通过四个HTTP动词(GET,POST,PUT,DELETE,[PATCH]),对服务器端资源进行操作,实现"表现层状态转化"。
Postman
前后端分离:
前端: app,小程序,pc页面
后端: 没有页面,mtv: 模型模板视图 去掉了t模板。
mv:模型 视图
模型的使用:跟原来的用法相同
视图: api构建视图
步骤:
1. pip3 install flask-restful
2.创建api对象
api = Api(app=app)
api = Api(app=蓝图对象)
3.
定义类视图:
from flask\_restful import Resource
class xxxApi(Resource):
def get(self):
pass
def post(self):
pass
def put(self):
pass
def delete(self):
pass
4. 绑定
api.add\_resource(xxxApi,'/user')
参照:http://www.pythondoc.com/Flask-RESTful/quickstart.html
https://flask-restful.readthedocs.io/en/latest/
路由:
@app.route('/user')
def user(): -------》视图函数
…..
return response对象
增加 修改 删除 查询 按钮动作
http://127.0.0.1:5000/user?id=1
http://127.0.0.1:5000/user/1
restful: ---->api ----> 接口 ---->资源 ----> url
class xxx(Resource): -------> 类视图
def get(self):
pass
….
http://127.0.0.1:5000/user
get
post
put
delete
增加 修改 删除 查询 是通过请求方式完成的
路径产生:
api.add_resource(Resource的子类,'/user')
api.add_resource(Resource的子类,'/goods')
api.add_resource(Resource的子类,'/order')
endpoint:
http://127.0.0.1:5000/user/1
http://127.0.0.1:5000/goods?type=xxx&page=1&sorted=price ----》get
----------------进:请求参数传入-------------------
步骤:
1。创建RequestParser对象:
parser = reqparse.RequestParser(bundle_errors=True) # 解析对象
2。给解析器添加参数:
通过parser.add_argument('名字',type=类型,required=是否必须填写,help=错误的提示信息,location=表明获取的位置form就是post表单提交)
注意在type的位置可以添加一些正则的验证等。
例如:
parser.add_argument('username', type=str, required=True, help='必须输入用户名', location=['form'])
parser.add_argument('password', type=inputs.regex(r'^\d{6,12}$'), required=True, help='必须输入6~12位数字密码',
location=['form'])
parser.add_argument('phone', type=inputs.regex(r'^1[356789]\d{9}$'), location=['form'], help='手机号码格式错误')
parser.add_argument('hobby', action='append') # ['篮球', '游戏', '旅游']
parser.add_argument('icon', type=FileStorage, location=['files'])
只要添加上面的内容,就可以控制客户端的提交,以及提交的格式。
3。在请求的函数中获取数据:
可以在get,post,put等中获取数据,通过parser对象.parse_args()
# 获取数据
args = parser.parse_args()
args是一个字典底层的结构中,因此我们获取具体的数据时可以通过get
username = args.get('username')
password = args.get('password')
------------输出-----------------
1.需要定义字典,字典的格式就是给客户端看的格式
user_fields = {
'id': fields.Integer,
'username': fields.String(default='匿名'),
'pwd': fields.String(attribute='password'),
'udatetime': fields.DateTime(dt_format='rfc822')
}
客户端能看到的是: id,username,pwd,udatetime这四个key
默认key的名字是跟model中的模型属性名一致,如果不想让前端看到命名,则可以修改
但是必须结合attribute='模型的字段名'
自定义fields
1。必须继承Raw
2。重写方法:
def format(self):
return 结果
class IsDelete(fields.Raw):
def format(self, value):
print('------------------>', value)
return '删除' if value else '未删除'
user_fields = {
。。。
'isDelete1': IsDelete(attribute='isdelete'),
。。。
}
URI:
xxxlist ----->点击具体的一个获取详情 ------> 详情
定义两个user_fields,
1.用于获取用户的列表信息结构的fields:
user_fields_1 = {
'id': fields.Integer,
'username': fields.String(default='匿名'),
'uri': fields.Url('single_user', absolute=True) ----》参数使用的就是endpoint的值
}
2。具体用户信息展示的fields
user_fields = {
'id': fields.Integer,
'username': fields.String(default='匿名'),
'pwd': fields.String(attribute='password'),
'isDelete': fields.Boolean(attribute='isdelete'),
'isDelete1': IsDelete(attribute='isdelete'),
'udatetime': fields.DateTime(dt_format='rfc822')
}
涉及endpoint的定义:
api.add_resource(UserSimpleResource, '/user/
出:
return data
注意:data必须是符合json格式
{
'aa':10,
'bb':[
{
'id':1,
'xxxs':[
{},{}
]
},
{
}
]
}
如果直接返回不能有自定义的对象User,Friend,。。。。
如果有这种对象,需要:marchal(),marchal_with()帮助进行转换。
1。marchal(对象,对象的fields格式) # 对象的fields格式是指字典的输出格式
marchal([对象,对象],对象的fields格式)
2。marchal_with() 作为装饰器修饰请求方法
@marshal\_with(user\_friend\_fields)
def get(self, id):
。。。。
return data
函数需要参数,参数就是最终数据输出的格式
参数: user_friend_fields,类型是:dict类型
例如:
user_friend_fields = {
'username': fields.String,
'nums': fields.Integer,
'friends': fields.List(fields.Nested(user_fields))
}
fields.Nested(fields.String) ----> ['aaa','bbb','bbbc']
fields.Nested(user_fields) -----> user_fields是一个字典结构,将里面的每一个对象转成user_fields
-----》[user,user,user]
import os
from flask import Blueprint, url_for
from flask_restful import Resource, marshal_with, fields, reqparse, inputs, marshal
from werkzeug.datastructures import FileStorage
from apps.user.model import User, Friend
from exts import api, db
from settings import Config
user_bp = Blueprint('user', __name__, url_prefix='/api')
class IsDelete(fields.Raw):
def format(self, value):
# print('------------------>', value)
return '删除' if value else '未删除'
user_fields_1 = {
'id': fields.Integer,
'username': fields.String(default='匿名'),
'uri': fields.Url('single_user', absolute=True)
}
user_fields = {
'id': fields.Integer,
'username': fields.String(default='匿名'),
'pwd': fields.String(attribute='password'),
'isDelete': fields.Boolean(attribute='isdelete'),
'isDelete1': IsDelete(attribute='isdelete'),
'udatetime': fields.DateTime(dt_format='rfc822')
}
parser = reqparse.RequestParser(bundle_errors=True) # 解析对象
parser.add_argument('username', type=str, required=True, help='必须输入用户名', location=['form'])
parser.add_argument('password', type=inputs.regex(r'^\d{6,12}$'), required=True, help='必须输入6~12位数字密码',
location=['form'])
parser.add_argument('phone', type=inputs.regex(r'^1[356789]\d{9}$'), location=['form'], help='手机号码格式错误')
parser.add_argument('hobby', action='append') # ['篮球', '游戏', '旅游']
parser.add_argument('icon', type=FileStorage, location=['files'])
class UserResource(Resource):
# get 请求的处理
@marshal_with(user_fields_1)
def get(self):
users = User.query.all()
# userList = []
# for user in users:
# userList.append(user.__dict__)
return users
# post
@marshal\_with(user\_fields)
def post(self):
# 获取数据
args = parser.parse\_args()
username = args.get('username')
password = args.get('password')
phone = args.get('phone')
bobby = args.get('hobby')
print(bobby)
icon = args.get('icon')
print(icon)
# 创建user对象
user = User()
user.username = username
user.password = password
if icon:
upload\_path = os.path.join(Config.UPLOAD\_ICON\_DIR, icon.filename)
icon.save(upload\_path)
# 保存路径个
user.icon = os.path.join('upload/icon', icon.filename)
if phone:
user.phone = phone
db.session.add(user)
db.session.commit()
return user
# put
def put(self):
return {'msg': '------>put'}
# delete
def delete(self):
return {'msg': '------>delete'}
class UserSimpleResource(Resource):
@marshal_with(user_fields) # user转成一个序列化对象,
def get(self, id):
user = User.query.get(id)
return user # 不是str,list,int,。。。
def put(self, id):
print('endpoint的使用:', url\_for('all\_user'))
return {'msg': 'ok'}
def delete(self, id):
pass
user_friend_fields = {
'username': fields.String,
'nums': fields.Integer,
'friends': fields.List(fields.Nested(user_fields))
}
class UserFriendResource(Resource):
@marshal\_with(user\_friend\_fields)
def get(self, id):
friends = Friend.query.filter(Friend.uid == id).all()
user = User.query.get(id)
friend\_list = \[\]
for friend in friends:
u = User.query.get(friend.fid)
friend\_list.append(u)
data = {
'username': user.username,
'nums': len(friends),
'friends': friend\_list # \[user,user,user\]
}
return data
api.add_resource(UserResource, '/user', endpoint='all_user')
api.add_resource(UserSimpleResource, '/user/
api.add_resource(UserFriendResource, '/friend/
视图函数
import datetime
from exts import db
class Friend(db.Model):
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
uid = db.Column(db.Integer, db.ForeignKey('user.id'))
fid = db.Column(db.Integer, db.ForeignKey('user.id'))
class User(db.Model):
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
username = db.Column(db.String(15), nullable=False)
password = db.Column(db.String(12), nullable=False)
phone = db.Column(db.String(11))
icon = db.Column(db.String(150))
isdelete = db.Column(db.Boolean())
email = db.Column(db.String(100))
udatetime = db.Column(db.DateTime, default=datetime.datetime.now)
friends = db.relationship('Friend', backref='user', foreign\_keys=Friend.uid)
def \_\_str\_\_(self):
return self.username
model
手机扫一扫
移动阅读更方便
你可能感兴趣的文章