目前Web应用这块,restufl API用得非常普遍,因为,你手上的前端设备五花八门,各种系统的手机,pad等等,而且网站和手机APP经常会有需要资源共享的时候。
如果网站做个app,手机端再独立一个app,要保持2者同步,估计做开发的要做死了。
所以,RESTFUL API提出了一个概念,就是资源为上,通俗地讲:就是,我有一个资源A,可以作为接口来提供出来,那么,前端设备的B,C,D 都可以通过申请这个API接口的方式,来进行获取资源,而最后显示画面如何渲染,让客户看到是怎么样的一个样子,那是前端的事情了。
学习API之前,有两个前置技能需要去了解下,一个是JSON数据格式,另外一个是curl或者HTTPie这样的http调试工具,我这里用的是HTTPie
先来看看通过Python Flask做一个非常简单的Web API接口范例
from flask import Flask,jsonify
app = Flask(__name__)
tasks =[
{
'id':1,
'title': u'Buy groceries',
'description': u'Milk, Cheese, Pizza, Fruit, Tylenol',
'done': False
},
{
'id': 2,
'title': u'Learn Python',
'description': u'Need to find a good Python tutorial on the web',
'done': False
}]
@app.route('/')
def index():
return 'Hello,world!'
@app.route('/todo/api/v1.0/tasks',methods=['GET'])
def get_tasks():
return jsonify({'tasks':tasks})
如上面代码,当你请求 /todo/api/v1.0/tasks 的时候,通过路由get_tasks,就能返回当前所有的数据信息
他是通过jsonify来进行返回的,这里需要注意的是,有2种方法可以返回json数据,一个是jsonify,还有一个是json.dumps()
那么2者有什么区别呢?可以看下图
jsonify直接返回的是Content-type:application/json的响应对象(Response对象)
json.dumps返回的,则是Content-type:text/html,charset=utf-8的HTML格式
我们可以看下jsonfiy的官方解释
来看下效果图及两者的区别
刚才是获取了所有的任务,那如果我需要其中某一个数据的话,如何做呢?
@app.route('/todo/api/v1.0/tasks/',methods=['GET'])
def get_task(task_id):
task = list(filter(lambda t:t['id']==task_id,tasks)) #检查tasks内部元素,是否有元素的id的值和参数id相匹配
if len(task)==0: #有的话,就返回列表形式包裹的这个元素,如果没有,则报错404
abort(404)
return jsonify({'tasks':task[0]}) #否则,将这个task以JSON的响应形式返回
比如下面,他就获取了id为2的这个数据的内容,当然,如果你在URL后面添加的是3,那他就会报错了,因为暂时还没有id为3的这个数据
上面的报错画面有些难看,而且,这样的报错格式,不太利于别人接口的使用。
一般像正规的接口,他都会以JSON格式返回error内容或者代码,所以,我们也要优化一下这样的错误信息。
@app.errorhandler(404)
def not_found(error):
return jsonify({'error':'Not found'}),404
这样,返回的格式,也是以JSON的格式,那样,如果别人调用你的接口,如果发生错误,别人就可以通过error信息,来进行渲染了。
接下来,既然有数据,那肯定会涉及到添加数据,那我们就要用到POST方法了,就和上传FORM表单的性质是一样的
HTTPie的好处是,他默认的POST格式是JSON的,所以,你不必特意指定格式,只要写一个POST,就ok,如下
@app.route('/todo/api/v1.0/tasks',methods=['POST'])
def create_task():
if not request.json or not 'title' in request.json: #如果请求里面没有JSON数据,或者在JSON数据里面,title的内容是空的
abort(404) #返回404报错
task = {
'id':tasks[-1]['id']+1, #取末尾task的id号,并加一作为新的数据的id号
'title':request.json['title'], #title必须要设置,不能为空
'description':request.json.get('description',""), #描述可以添加,但是也可以不写,默认为空
'done':False
}
tasks.append(task) #完了以后,添加这个task进tasks列表
return jsonify({'task':task}),201 #并返回这个添加的task内容,和状态码
后面加入的信息,你写title=test或者title="test"都可以
既然已经有了添加的功能,那么修改和删除的也必不可少,如下
@app.route('/todo/api/v1.0/tasks/',methods=['PUT'])
def update_task(task_id):
task = list(filter(lambda t:t['id']==task_id,tasks)) #检查是否有这个id的数据
if len(task)==0:
abort(404)
if not request.json: #如果请求中没有附带json数据,则报错400
abort(400)
if 'title' in request.json and not isinstance(request.json['title'],str): #如果title对应的值,不是字符串类型,则报错400
abort(400)
if 'description' in request.json and not isinstance(request.json['description'],str): #同上,检查description对应的值是否为字符串
abort(400)
if 'done' in request.json and not isinstance(request.json['done'],bool): #检查done对应的值是否是布尔值
abort(400)
task[0]['title'] = request.json.get('title',task[0]['title']) #如果上述条件全部通过的话,更新title的值,同时要设置默认值
task[0]['description'] = request.json.get('description', task[0]['description']) #修改description值
task[0]['done'] = request.json.get('done', task[0]['done']) #修改done的值
return jsonify({'task':task[0]}) #最后,返回修改后的数据
@app.route('/todo/api/v1.0/tasks/',methods=['DELETE'])
def delete_task(task_id):
task = list(filter(lambda t: t['id']==task_id,tasks)) #检查是否有这个数据
if len(task) ==0:
abort(404)
tasks.remove(task[0]) #从tasks列表中删除这个值
return jsonify({'result': True}) #返回结果状态,自定义的result
这样做接口系统,功能是达到了,但是你不见得让用户去背你的URL吧?这样不现实,像我们平时用的话,最好是可以自动生成URL
所以,这里我们需要写一个辅助函数,来返回一个完整的URL,这样可以就可以直接拿着URL用了
def make_public_task(task):
new_task={} #新建一个对象,字典类型
for key in task: #遍历字典内部的KEY
if key == 'id': #当遍历到id的时候,为新对象增加uri的key,对应的值为完整的uri
new_task['uri'] = url_for('get_task',task_id=task['id'],_external=True)
else:
new_task[key] = task[key] #其他的key,分别一一对应加入新对象
return new_task #最后返回新对象数据
@app.route('/todo/api/v1.0/tasks',methods=['GET'])
def get_tasks():
return jsonify({'tasks': list(map(make_public_task,tasks))}) #使用map函数,罗列出所有的数据,返回的数据信息,是经过辅助函数处理的
也就是说,经过辅助函数处理,你就可以直接获取整体URI了,而不是单个id号
接口的应用功能上,已经做得差不多了,但是现在的接口外圈是对外开放的,如果有人恶意破坏,数据就有可能有危险
所以,这里引入了认证机制,其实简单来说就是要求客户端的用户有使用使用权限,比如有账号和密码。
这里引入HTTPBasicAuth
auth=HTTPBasicAuth()
@auth.get_password
def get_password(username):
if username == "allen":
return "python"
return None
@auth.error_handler
def unauthorized():
return make_response(jsonify({'error':'Unauthorized access'}),401)
通过客户端发送的用户名和密码,匹配get_password函数内部这个用户名和密码是否匹配
如果没有用户名密码,或者说是错误的,则返回401错误
于是,像查看资料啊这些的路由,都要给他挂一个login_required的装饰器了
@app.route('/todo/api/v1.0/tasks',methods=['GET'])
@auth.login_required
def get_tasks():
return jsonify({'tasks': list(map(make_public_task,tasks))})
如果你没有附带用户名密码,那么,就会禁止访问了
附带了用户名和密码的话,就可以有权限进行操作