使用 Flask 设计实现一套 REST API【成绩管理系统】
0X00 什么是REST风格的API
众所周知http协议有GET/PUT/POST/PATCH/DELETE等众多方法,还能在提交请求和发送响应的时候携带数据。REST风格的API就是使用了这些HTTP特性的API。针对一个URL可以有多种动词(方法)来表示不同的操作。
更多详细的内容可以点击查看阮一峰的博客:理解RESTful架构
0X01 怎么选用HTTP动词
常见的动词有这五种,可以对应自己的需求选用
| 动词 | 类似的SQL关键字 | 功能 | | ---- |: ---- :|: ---- :| | GET | SELECT | 获取资源 | | POST | CREATE | 创建资源 | | PUT | UPDATE | 更新资源(需要提供改变后的完整资源) | | PATCH | UPDATE | 更新资源(需要提供改变的属性) | | DELETE | DELETE | 删除资源 |
0X02 设计URL
REST风格的API因为可以用HTTP的动词,所以URL中是不带有动词的,如果我要获取某个学生的信息应该是[GET] http://api.example.com/student/id=12345678900。HTTP动词理论上是能满足各种情况下的需求的,所以URL中只应该出现名词而不应该出现动词。这里用阮一峰举的例子来说明一下
| 动词 | 路径 | 功能 | | —|--- | | GET | /zoos | 列出所有动物园 | | POST | /zoos | 新建一个动物园 | | GET | /zoos/ID | 获取某个指定动物园的信息 | | PUT | /zoos/ID | 更新某个指定动物园的信息(提供该动物园的全部信息) | | PATCH | /zoos/ID | 更新某个指定动物园的信息(提供该动物园的部分信息) | | DELETE | /zoos/ID | 删除某个动物园 | | GET | /zoos/ID/animals | 列出某个指定动物园的所有动物 | | DELETE | /zoos/ID/animals/ID | 删除某个指定动物园的指定动物 |
0X03 状态码
状态码是HTTP中的一大优势,一个响应可以只靠状态码来判请求结果。这些是常见的状态码,自己设计API的时候要严格按照规范来设计状态码,可以提高代码和API的可读性和可理解性。
| 状态码 | 信息 | 请求类型 | 含义 |
|---|---|---|---|
| 200 | OK | [GET] | 服务器成功返回用户请求的数据,该操作是幂等的(Idempotent)。 |
| 201 | CREATED | [POST/PUT/PATCH] | 用户新建或修改数据成功。 |
| 202 | Accepted | [*] | 表示一个请求已经进入后台排队(异步任务)。 |
| 204 | NO CONTENT | [DELETE] | 用户删除数据成功。 |
| 400 | INVALID REQUEST | [POST/PUT/PATCH] | 用户发出的请求有错误,服务器没有进行新建或修改数据的操作,该操作是幂等的。 |
| 401 | Unauthorized | [*] | 表示用户没有权限(令牌、用户名、密码错误)。 |
| 403 | Forbidden | [*] | 表示用户得到授权(与401错误相对),但是访问是被禁止的。 |
| 404 | NOT FOUND | [*] | 用户发出的请求针对的是不存在的记录,服务器没有进行操作,该操作是幂等的。 |
| 406 | Not Acceptable | [GET] | 用户请求的格式不可得(比如用户请求JSON格式,但是只有XML格式)。 |
| 410 | Gone | [GET] | 用户请求的资源被永久删除,且不会再得到的。 |
| 422 | Unprocesable entity | [POST/PUT/PATCH] | 当创建一个对象时,发生一个验证错误。 |
| 500 | INTERNAL SERVER ERROR | [*] | 服务器发生错误,用户将无法判断发出的请求是否成功。 |
0X04 如何用Python实现
Python有大量第三方库可以实现REST风格的API,我这里选用的是相对轻量化的一个 Flask。安装这个库最简单的方式还是用pip,根据环境变量的不同可能具体命令有所不同,在我的Linux上是用pip3 install flask就可以直接安装好的。
安装好后进入Python的交互式界面输入import flask如果没有出现Import Error就是安装好了。
0X05创建数据库和表
现在可以开始设计API了。既然是成绩管理系统,那么首先就要创建一个数据库,我这里的数据库是用的MariaDB。
| 列名 | 类型 | 含义 | 键 |
|---|---|---|---|
| id | int | 编号 | 主键 |
| name | varchar(10) | 学生姓名 | |
| number | char(11) | 学号 | |
| python | float | Py成绩 | |
| cpp | float | c++成绩 | |
| os | float | 操作系统成绩 | |
| network | float | 计算机网络成绩 | |
| total | float | 总分 | |
| ave | float | 平均分 |
0X06 创建Py脚本
1from flask import Flask, request
2
3app = Flask(__name__)
4
5# 从这里指定路径、方法、返回数据
6@app.route('/', methods=['GET'])
7def index():
8 return '<h1>hello,world</h1>'
9
10
11with app.test_request_context():
12 app.run()这段代码写好之后运行起来会在本地监听5000端口(默认的),然后当你用浏览器访问http://localhost:5000/的时候就像你返回<h1>hello,world</h1>,在浏览器页面下看到的就是一行大号的hello,world。因为在浏览器的地址栏输入URL按回车之后就是向那个URL发送了GET请求,也就正好调用了index()方法。
这里先将与API无关的代码填好,下面开始正式完成各项功能。其实也就是连接了数据库而已
1from flask import Flask, request
2import json
3import pymysql
4
5app = Flask(__name__)
6database = pymysql.connect("db_host", "db_username", "db_password", "db_name")
7cursor = database.cursor()
8
9@app.route('/', methods=['GET'])
10def index():
11 return '<h1>hello,world</h1>'
12
13
14with app.test_request_context():
15 app.run()0X07 实现一个构造返回Json数据的方法
首先我们选择使用Json来作为数据传输格式,因为Json相对XML来说更轻量一点,现在也更流行。规定客户端每次请求会后服务器都会返回下面这样类型的Json数据
1{
2 "time": "unix_time",
3 “e_msg": "error_message",
4 "search_list": {
5 "item0": {
6 "name": "name",
7 "number": "number",
8 "python": "marks",
9 "os": "marks",
10 "network": "marks",
11 "cpp": "marks",
12 "total": "marks",
13 "ave": "ave"
14 },"item1" : {
15 "name": "name",
16 "number": "number",
17 "python": "marks",
18 "os": "marks",
19 "network": "marks",
20 "cpp": "marks",
21 "total": "marks",
22 "ave": "ave"
23 }
24 }
25}API提供增删查改功能,增删改只通过状态码就可以判断执行结果,只有查询的时候才会需要从响应中获取数据。
0X08 增加一条新的数据
添加一条新数据按照标准应该使用动词POST,根据URL中只有名词不用动词只有名词的标准,隧将URL设计成http://localhost/student,再依据标准添加版本号上去,变成http://localhost/v1/student。
具体功能代码实现如下,
1@app.route('/v1/student', methods=['POST']) # 路径为/v1/student,方法为POST
2def add_student():
3 data = request.get_data().decode('utf-8') # 将客户端传来的数据解码
4 json_data = json.loads(data) # 将数据转为Json
5
6 # 从Json中获取数据
7 name = json_data['name']
8 number = str(json_data['number'])
9 python = json_data['python']
10 cpp = json_data['cpp']
11 os = json_data['os']
12 network = json_data['network']
13
14 # 计算总分平均分
15 total = python + cpp + os + network
16 ave = total / 4
17
18 # 查询数据库中是否有该学生的信息
19 sql = "SELECT COUNT(*) FROM student.marks WHERE number=\"%s\"" % number
20 cursor.execute(sql)
21 count = cursor.fetchall()[0][0]
22 database.commit()
23 if count >= 1: # 该学生信息已经存在,返回400错误
24 return build_json(e_msg="student early exist"), 400
25
26 # 向数据库中插入数据
27 sql = "INSERT INTO student.marks (name, number, python, os, network, cpp, total, ave)" \
28 "VALUES (\"%s\", \"%s\", %s, %s, %s, %s, %s, %s)" % (name, number, python, os,
29 network, cpp, total, ave)
30 cursor.execute(sql)
31 database.commit()
32
33 # 请求成功,返回201状态码
34 return build_json(), 2010X09 删除已经存在的数据
根据标准,将API设计成DELETE方法,URL为http://localhost/v1/student/number=<number>
第一行的number=<number>可以将url中符合这种规范的匹配出来,配合方法定义时的参数,可以直接将url参数传入到方法体中。
1@app.route('/v1/student/number=<number>', methods=['DELETE']) # 路径为/v1/student, 方法为DELETE
2def delete_student(number):
3 if not number.isdigit(): # 判断number是否合法
4 return build_json(e_msg="number should be digit"), 403
5 sql = "DELETE FROM student.marks WHERE number=\"%s\"" % number
6 cursor.execute(sql)
7 database.commit()
8 return build_json(), 2040X0A 查询学生成绩
根据标准,将API设计成GET方法,URL为http://localhost/v1/student/sort_by=<sort_by>。提供了python/cpp/network/os/total/ave排序方式,(其实是数据库实现的)。
1@app.route('/v1/student/sort_by=<sort_by>', methods=['GET'])
2def show_student(sort_by):
3 # 判断排序的key是否正确
4 if sort_by not in ['python', 'cpp', 'os', 'network', 'total', 'ave']:
5 return build_json(e_msg="sort_by key not found"), 404
6
7 # 构建查询SQL
8 sql = "SELECT name, number, python, cpp, os, network, total, ave FROM student.marks ORDER BY %s DESC" % sort_by
9 cursor.execute(sql)
10 database.commit()
11 res = cursor.fetchall() # 获取查询结果
12
13 # 构建查询结果Json
14 res_list = {}
15 count = 0
16 for i in res:
17 res_list['item' + str(count)] = {
18 'name': i[0],
19 'number': i[1],
20 'python': i[2],
21 'cpp': i[3],
22 'os': i[4],
23 'network': i[5],
24 'total': i[6],
25 'ave': [i[7]]
26 }
27 count += 1
28 return build_json(search_list=res_list), 2000X0B 修改学生成绩
修改学生成绩和添加成绩几乎是一样的操作,只有这么几点是不太一样的。添加信息时如果学号已经存在了那就不能再添加了,而修改的时候是如果学号不存在才错误;添加和修改的SQL不同。就没有别的区别了。
1@app.route('/v1/student', methods=['PUT'])
2def modify_student():
3 data = request.get_data().decode('utf-8')
4 json_data = json.loads(data)
5
6 # 从Json中获取数据
7 name = json_data['name']
8 number = str(json_data['number'])
9 python = json_data['python']
10 cpp = json_data['cpp']
11 os = json_data['os']
12 network = json_data['network']
13
14 # 查询数据库中是否有该学生的信息
15 sql = "SELECT COUNT(*) FROM student.marks WHERE number=\"%s\"" % number
16 cursor.execute(sql)
17 count = cursor.fetchall()[0][0]
18 database.commit()
19 if count < 1: # 该学生信息不存在,返回404错误
20 return build_json(e_msg="student not found"), 404
21
22 # 计算总分平均分
23 total = python + cpp + os + network
24 ave = total / 4
25
26 # 向数据库中插入数据
27 sql = "UPDATE student.marks SET name=\"%s\", python=%s, cpp=%s, os=%s, network=%s, total=%s, ave=%s WHERE number=\"%s\"" % (name, python, cpp, os, network, total, ave, number)
28 cursor.execute(sql)
29 database.commit()
30
31 # 请求成功,返回201状态码
32 return build_json(), 2010X0C 搞定所有API
现在就搞定了所有的API编写。现在我把所有代码贴上来,注意这段代码是用于Python3的。如果需要测试的话可以用Python自带的requests模块或者Postman软件来测试该API。
- 声明:代码最后一行的
app.run()方法,现在是只在本地监听的。可以改成app.run('0.0.0.0')就对外部监听了。
1#!/usr/bin/env python3
2# coding=utf-8
3# @Time : 2017/6/3 11:34
4# @Author : Shawn
5# @Blog : https://blog.just666.cn
6# @Email : shawnbluce@gmail.com
7# @purpose : 演示Python_API
8
9from flask import Flask, request
10import json
11import pymysql
12import time
13
14app = Flask(__name__)
15database = pymysql.connect("115.29.52.14", "shawn", "zhangHAO8", "student")
16cursor = database.cursor()
17
18def build_json(search_list=None, e_msg=None) -> str:
19 json_data = {'time': time.time(), 'search_list': search_list, 'e_msg': e_msg}
20 return json.dumps(json_data)
21
22@app.route('/', methods=['GET'])
23def index():
24 return '<h1>hello,world</h1>'
25
26@app.route('/v1/student', methods=['POST']) # 路径为/v1/student,方法为POST
27def add_student():
28 data = request.get_data().decode('utf-8') # 将客户端传来的数据解码
29 json_data = json.loads(data) # 将数据转为Json
30
31 # 从Json中获取数据
32 name = json_data['name']
33 number = str(json_data['number'])
34 python = json_data['python']
35 cpp = json_data['cpp']
36 os = json_data['os']
37 network = json_data['network']
38
39 # 计算总分平均分
40 total = python + cpp + os + network
41 ave = total / 4
42
43 # 查询数据库中是否有该学生的信息
44 sql = "SELECT COUNT(*) FROM student.marks WHERE number=\"%s\"" % number
45 cursor.execute(sql)
46 count = cursor.fetchall()[0][0]
47 database.commit()
48 if count >= 1: # 该学生信息已经存在,返回400错误
49 return build_json(e_msg="student early exist"), 400
50
51 # 向数据库中插入数据
52 sql = "INSERT INTO student.marks (name, number, python, os, network, cpp, total, ave)" \
53 "VALUES (\"%s\", \"%s\", %s, %s, %s, %s, %s, %s)" % (name, number, python, os,
54 network, cpp, total, ave)
55 cursor.execute(sql)
56 database.commit()
57
58 # 请求成功,返回201状态码
59 return build_json(), 201
60
61@app.route('/v1/student/number=<number>', methods=['DELETE']) # 路径为/v1/student, 方法为DELETE
62def delete_student(number):
63 if not number.isdigit(): # 判断number是否合法
64 return build_json(e_msg="number should be digit"), 403
65 sql = "DELETE FROM student.marks WHERE number=\"%s\"" % number
66 cursor.execute(sql)
67 database.commit()
68 return build_json(), 204
69
70@app.route('/v1/student/sort_by=<sort_by>', methods=['GET'])
71def show_student(sort_by):
72 # 判断排序的key是否正确
73 if sort_by not in ['python', 'cpp', 'os', 'network', 'total', 'ave']:
74 return build_json(e_msg="sort_by key not found"), 404
75
76 # 构建查询SQL
77 sql = "SELECT name, number, python, cpp, os, network, total, ave FROM student.marks ORDER BY %s DESC" % sort_by
78 cursor.execute(sql)
79 database.commit()
80 res = cursor.fetchall() # 获取查询结果
81
82 # 构建查询结果Json
83 res_list = {}
84 count = 0
85 for i in res:
86 res_list['item' + str(count)] = {
87 'name': i[0],
88 'number': i[1],
89 'python': i[2],
90 'cpp': i[3],
91 'os': i[4],
92 'network': i[5],
93 'total': i[6],
94 'ave': [i[7]]
95 }
96 count += 1
97 return build_json(search_list=res_list), 200
98
99@app.route('/v1/student', methods=['PUT'])
100def modify_student():
101 data = request.get_data().decode('utf-8')
102 json_data = json.loads(data)
103
104 # 从Json中获取数据
105 name = json_data['name']
106 number = str(json_data['number'])
107 python = json_data['python']
108 cpp = json_data['cpp']
109 os = json_data['os']
110 network = json_data['network']
111
112 # 查询数据库中是否有该学生的信息
113 sql = "SELECT COUNT(*) FROM student.marks WHERE number=\"%s\"" % number
114 cursor.execute(sql)
115 count = cursor.fetchall()[0][0]
116 database.commit()
117 if count < 1: # 该学生信息不存在,返回404错误
118 return build_json(e_msg="student not found"), 404
119
120 # 计算总分平均分
121 total = python + cpp + os + network
122 ave = total / 4
123
124 # 向数据库中插入数据
125 sql = "UPDATE student.marks SET name=\"%s\", python=%s, cpp=%s, os=%s, network=%s, total=%s, ave=%s WHERE number=\"%s\"" \
126 % (name, python, cpp, os, network, total, ave, number)
127 cursor.execute(sql)
128 database.commit()
129
130 # 请求成功,返回201状态码
131 return build_json(), 201
132
133with app.test_request_context():
134 app.run()如果这篇文章对你有帮助,可以请我喝杯咖啡 ☕
评论