【python】flask框架
部分参考来源
http://www.ruanyifeng.com/blog/2019/09/curl-reference.html
https://www.jianshu.com/p/ee92c9accedd
https://www.cnblogs.com/wang-yaz/p/9237981.html
http://docs.jinkan.org/docs/flask/quickstart.html
https://www.w3cschool.cn/flask/flask_quick_guide.html
https://www.jianshu.com/p/ed1f819a7b58
http://www.pythondoc.com/flask-restful/first.html
https://www.jianshu.com/p/bee9e61aca14
https://www.jianshu.com/p/9bb7e6c683a3
什么是RESTful
Web API(Web应用程序接口)
如果我们想要获取某个电商网站的某个商品,输入http://localhost:9999/products/123,就可以看到id为123的商品页面,但这个结果是HTML页面,它同时混合包含了商品的数据和商品的展示两个部分。对于用户来说,阅读起来没有问题,但是,如果机器读取,就很难从HTML中解析出商品的数据。
如果一个URL返回的不是HTML,而是机器能直接解析的数据,这个URL就可以看成是一个Web API。比如,读取http://localhost:9999/api/products/123,如果能直接返回商品的数据,那么机器就可以直接读取。
REST
REST是一种设计Web API的模式。最常用的数据格式是JSON。
REST,英文Representational state transfer(表述性状态转移),其实就是对资源的表述性状态转移。
(什么是表述性:就是指客户端请求一个资源,服务器拿到的这个资源,就是表述)
资源的地址在web中就是URL(统一资源标识符)
资源是REST系统的核心概念。 所有的设计都是以资源为中心
REST架构的主要原则:
- 网络上的所有事物都被抽象为资源
- 每个资源都有一个唯一的资源标识符
- 同一个资源具有多种表现形式(XML,JSON等)
- 对资源的各种操作不会改变资源标识符
- 所有的操作都是无状态的
六条设计规范定义了一个REST系统的特点:
- 客户端-服务器:客户端和服务器之间隔离,服务器提供服务,客户端进行消费。
- 无状态:从客户端到服务器的每个请求都必须包含理解请求所必需的信息。换句话说,服务器不会存储客户端上一次请求的信息用来给下一次使用。
- 可缓存:服务器必须明示客户端请求能否缓存。
- 分层系统:客户端和服务器之间的通信应该以一种标准的方式。
- 统一的接口:服务器和客户端的通信方法必须是统一的。
- 按需执行代码:服务器可以提供可执行代码或脚本,为客户端在它们的环境中执行。这个约束是唯一一个是可选的。
RESTful
符合REST原则和特点的架构方式即可称为RESTful
RESTful api设计规范
URL的定义,叫做统一资源定位符,也就是说URL是用来表示资源在互联网上的位置的,所以说在URL中不应该包含动词,只能包含名词。对资源的操作应该体现在http method上面
什么是Flask
Flask是一个使用 python 编写的轻量级Web应用框架
安装Flask
安装Flask库
sudo pip3 install Flask
使用Flask
在网页显示"Hello World!"
# -*- coding:utf-8 -*-
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello_world():
return 'Hello World!'
if __name__ == '__main__':
app.run()
运行python脚本后,访问 http://127.0.0.1:5000/,你会看见Hello World问候。
那么,这段代码做了什么?
- 首先,我们导入了 Flask 类。这个类的实例将会是我们的WSGI应用程序。Web服务器网关接口(Python Web Server Gateway Interface,缩写为WSGI)是为Python语言定义的Web服务器和Web应用程序或框架之间的一种简单而通用的接口。
from flask import Flask
- 接下来,我们创建一个该类的实例,第一个参数是应用模块或者包的名称。 如果你使用单一的模块(如本例),你应该使用 __name__,因为模块的名称将会因其作为单独应用启动还是作为模块导入而有不同(也即是 '__main__ ’ 或实际的导入名)。这是必须的,这样 Flask 才知道到哪去找模板、静态文件等等。
app = Flask(__name__)
- 然后,我们使用 route() 装饰器告诉 Flask 什么样的URL 能触发我们的函数。
@app.route('/')
- 这个函数的名字也在生成 URL 时被特定的函数采用,这个函数返回我们想要显示在用户浏览器中的信息。
def hello_world():
return 'Hello World!'
- 最后我们用 run() 函数来让应用运行在本地服务器上。 其中 if __name__ == ‘__main__’: 确保服务器只会在该脚本被 Python 解释器直接执行的时候才会运行,而不是作为模块导入的时候。
if __name__ == '__main__':
app.run()
- 欲关闭服务器,按 Ctrl+C。
使用python和Flask实现RESTful services
在 Flask 中有许多扩展来帮助我们构建RESTful services。
我们web service的客户端需要添加、删除以及修改任务的服务,因此显然我们需要一种方式来存储任务。最直接的方式就是建立一个小型的数据库。
这里我们直接把任务列表存储在内存中,因此这些任务列表只会在web服务器运行中工作,在结束的时候就失效。这种方式只是适用我们自己开发的web服务器,不适用于生产环境的web服务器, 这种情况一个合适的数据库的搭建是必须的。
我们现在来实现web service的第一个入口:
# -*- coding:utf-8 -*-
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('/todo/api/v1.0/tasks', methods=['GET'])
def get_tasks():
return jsonify({'tasks': tasks})
if __name__ == '__main__':
app.run(debug=True)
正如你所见,没有多大的变化。我们创建一个任务的内存数据库,这里无非就是一个字典和数组。数组中的每一个元素都具有上述定义的任务的属性。
创建一个get_tasks的函数,访问的URI为 /todo/api/v1.0/tasks,并且只允许 GET 的 HTTP 方法。
这个函数的响应不是文本,我们使用JSON数据格式来响应,Flask的jsonify函数从我们的数据结构中生成。
使用网页浏览器来测试我们的web service不是一个最好的注意,因为网页浏览器上不能轻易地模拟所有的HTTP请求的方法。相反,我们会使用curl:
curl -i http://localhost:5000/todo/api/v1.0/tasks
返回如下:
我们已经成功地调用我们的RESTful service的一个函数!
现在我们开始编写GET方法请求我们的任务资源的第二个版本。这是一个用来返回单独一个任务的函数:
# -*- coding:utf-8 -*-
from flask import Flask, jsonify, abort
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('/todo/api/v1.0/tasks/<int:task_id>', methods=['GET'])
def get_task(task_id):
task = list(filter(lambda t: t['id'] == task_id, tasks))
if len(task) == 0:
abort(404)
return jsonify({'task': task[0]})
if __name__ == '__main__':
app.run(debug=True)
第二个函数有些意思。这里我们得到了URL中任务的id,接着 Flask 把它转换成函数中的task_id的参数。
我们用这个参数来搜索我们的任务数组。如果我们的数据库中不存在搜索的id,我们将会返回一个类似404的错误,根据HTTP规范的意思是 “资源未找到”。
如果我们找到相应的任务,那么我们只需将它用jsonify打包成 JSON 格式并将其发送作为响应,就像我们以前那样处理整个任务集合。
调用curl请求的结果如下:
curl -i http://localhost:5000/todo/api/v1.0/tasks/2
curl -i http://localhost:5000/todo/api/v1.0/tasks/3
当我们请求id #2的资源时候,我们获取到了,但是当我们请求#3的时候返回了404错误。有关错误奇怪的是返回的是HTML信息而不是JSON,这是因为Flask按照默认方式生成404响应。由于这是一个Web service客户端希望我们总是以JSON格式回应,所以我们需要改善我们的404错误处理程序:
# -*- coding:utf-8 -*-
from flask import Flask, jsonify, abort, make_response
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.errorhandler(404)
def not_found(error):
return make_response(jsonify({'error': 'Not found'}), 404)
@app.route('/todo/api/v1.0/tasks/<int:task_id>', methods=['GET'])
def get_task(task_id):
task = list(filter(lambda t: t['id'] == task_id, tasks))
if len(task) == 0:
abort(404)
return jsonify({'task': task[0]})
if __name__ == '__main__':
app.run(debug=True)
我们会得到一个友好的错误提示:
接下来就是POST方法,我们用来在我们的任务数据库中插入一个新的任务:
# -*- coding:utf-8 -*-
from flask import Flask, jsonify, abort, make_response, request
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('/todo/api/v1.0/tasks', methods=['GET'])
def get_tasks():
return jsonify({'tasks': tasks})
@app.errorhandler(404)
def not_found(error):
return make_response(jsonify({'error': 'Not found'}), 404)
@app.route('/todo/api/v1.0/tasks/<int:task_id>', methods=['GET'])
def get_task(task_id):
task = list(filter(lambda t: t['id'] == task_id, tasks))
if len(task) == 0:
abort(404)
return jsonify({'task': task[0]})
@app.route('/todo/api/v1.0/tasks', methods=['POST'])
def create_task():
if not request.json or not 'title' in request.json:
abort(400)
task = {
'id': tasks[-1]['id'] + 1,
'title': request.json['title'],
'description': request.json.get('description', ""),
'done': False
}
tasks.append(task)
return jsonify({'task': task}), 201
if __name__ == '__main__':
app.run(debug=True)
添加一个新的任务也是相当容易。只有当请求以JSON格式形式,request.json才会有请求的数据。如果没有数据,或者存在数据但是缺少title项,我们将会返回 400,这是表示请求无效。
接着我们会创建一个新的任务字典,使用最后一个任务的id + 1作为该任务的 id。我们允许description字段缺失,并且假设done字段设置成False。
我们把新的任务添加到我们的任务数组中,并且把新添加的任务和状态201响应给客户端。
使用如下的 curl 命令来测试这个新的函数:
curl -i -H "Content-Type: application/json" -X POST -d '{"title":"Read a book"}' http://localhost:5000/todo/api/v1.0/tasks
curl -i http://localhost:5000/todo/api/v1.0/tasks/3
当然在完成这个请求后,我们可以得到任务的更新列表:
curl -i http://localhost:5000/todo/api/v1.0/tasks
剩下的两个函数如下所示:
# -*- coding:utf-8 -*-
from flask import Flask, jsonify, abort, make_response, request
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('/todo/api/v1.0/tasks', methods=['GET'])
def get_tasks():
return jsonify({'tasks': tasks})
@app.errorhandler(404)
def not_found(error):
return make_response(jsonify({'error': 'Not found'}), 404)
@app.route('/todo/api/v1.0/tasks/<int:task_id>', methods=['GET'])
def get_task(task_id):
task = list(filter(lambda t: t['id'] == task_id, tasks))
if len(task) == 0:
abort(404)
return jsonify({'task': task[0]})
@app.route('/todo/api/v1.0/tasks', methods=['POST'])
def create_task():
if not request.json or not 'title' in request.json:
abort(400)
task = {
'id': tasks[-1]['id'] + 1,
'title': request.json['title'],
'description': request.json.get('description', ""),
'done': False
}
tasks.append(task)
return jsonify({'task': task}), 201
@app.route('/todo/api/v1.0/tasks/<int:task_id>', methods=['PUT'])
def update_task(task_id):
task = list(filter(lambda t: t['id'] == task_id, tasks))
if len(task) == 0:
abort(404)
if not request.json:
abort(400)
if 'title' in request.json and type(request.json['title']) != unicode:
abort(400)
if 'description' in request.json and type(request.json['description']) is not unicode:
abort(400)
if 'done' in request.json and type(request.json['done']) is not bool:
abort(400)
task[0]['title'] = request.json.get('title', task[0]['title'])
task[0]['description'] = request.json.get('description', task[0]['description'])
task[0]['done'] = request.json.get('done', task[0]['done'])
return jsonify({'task': task[0]})
@app.route('/todo/api/v1.0/tasks/<int:task_id>', 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])
return jsonify({'result': True})
if __name__ == '__main__':
app.run(debug=True)
delete_task函数没有什么特别的。对于update_task函数,我们需要严格地检查输入的参数以防止可能的问题。我们需要确保在我们把它更新到数据库之前,任何客户端提供我们的是预期的格式。
更新任务#2的函数调用如下所示:
curl -i -H "Content-Type: application/json" -X PUT -d '{"done":true}' http://localhost:5000/todo/api/v1.0/tasks/2
优化web service接口
目前API的设计的问题就是迫使客户端在任务标识返回后去构造URI。这对于服务器是十分简单的,但是间接地迫使客户端知道这些URI是如何构造的,这将会阻碍我们以后变更这些URI。
不直接返回任务的id,我们直接返回控制这些任务的完整的URI,以便客户端可以随时使用这些 URI。为此,我们可以写一个小的辅助函数生成一个 “公共” 版本任务发送到客户端:
# -*- coding:utf-8 -*-
from flask import Flask, jsonify, abort, make_response, request, url_for
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.errorhandler(404)
def not_found(error):
return make_response(jsonify({'error': 'Not found'}), 404)
@app.route('/todo/api/v1.0/tasks/<int:task_id>', methods=['GET'])
def get_task(task_id):
task = list(filter(lambda t: t['id'] == task_id, tasks))
if len(task) == 0:
abort(404)
return jsonify({'task': task[0]})
@app.route('/todo/api/v1.0/tasks', methods=['POST'])
def create_task():
if not request.json or not 'title' in request.json:
abort(400)
task = {
'id': tasks[-1]['id'] + 1,
'title': request.json['title'],
'description': request.json.get('description', ""),
'done': False
}
tasks.append(task)
return jsonify({'task': task}), 201
@app.route('/todo/api/v1.0/tasks/<int:task_id>', methods=['PUT'])
def update_task(task_id):
task = list(filter(lambda t: t['id'] == task_id, tasks))
if len(task) == 0:
abort(404)
if not request.json:
abort(400)
if 'title' in request.json and type(request.json['title']) != unicode:
abort(400)
if 'description' in request.json and type(request.json['description']) is not unicode:
abort(400)
if 'done' in request.json and type(request.json['done']) is not bool:
abort(400)
task[0]['title'] = request.json.get('title', task[0]['title'])
task[0]['description'] = request.json.get('description', task[0]['description'])
task[0]['done'] = request.json.get('done', task[0]['done'])
return jsonify({'task': task[0]})
@app.route('/todo/api/v1.0/tasks/<int:task_id>', 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])
return jsonify({'result': True})
def make_public_task(task):
new_task = {}
for field in task:
if field == 'id':
new_task['uri'] = url_for('get_task', task_id=task['id'], _external=True)
else:
new_task[field] = task[field]
return new_task
@app.route('/todo/api/v1.0/tasks', methods=['GET'])
def get_tasks():
return jsonify({'tasks': list(map(make_public_task, tasks))})
if __name__ == '__main__':
app.run(debug=True)
# 局域网下其他设备访问设置方法
# app.run(host='0.0.0.0', port=5000, debug=True)
我们将会把上述的方式应用到其它所有的函数上以确保客户端一直看到URI而不是id。
curl -i http://localhost:5000/todo/api/v1.0/tasks
Flask实现下载功能
# -*- coding:utf-8 -*-
from flask import Flask, send_from_directory
app = Flask(__name__)
@app.route("/download/<string:file>", methods=["GET"])
def download_file(file):
return send_from_directory("/home/zhangchen", filename=file, as_attachment=True)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True)
结语
如果您有修改意见或问题,欢迎留言或者通过邮箱和我联系。
手打很辛苦,如果我的文章对您有帮助,转载请注明出处。