Shawn's Blog
目录 · 13 节

使用 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的可读性和可理解性。

状态码信息请求类型含义
200OK[GET]服务器成功返回用户请求的数据,该操作是幂等的(Idempotent)。
201CREATED[POST/PUT/PATCH]用户新建或修改数据成功。
202Accepted[*]表示一个请求已经进入后台排队(异步任务)。
204NO CONTENT[DELETE]用户删除数据成功。
400INVALID REQUEST[POST/PUT/PATCH]用户发出的请求有错误,服务器没有进行新建或修改数据的操作,该操作是幂等的。
401Unauthorized[*]表示用户没有权限(令牌、用户名、密码错误)。
403Forbidden[*]表示用户得到授权(与401错误相对),但是访问是被禁止的。
404NOT FOUND[*]用户发出的请求针对的是不存在的记录,服务器没有进行操作,该操作是幂等的。
406Not Acceptable[GET]用户请求的格式不可得(比如用户请求JSON格式,但是只有XML格式)。
410Gone[GET]用户请求的资源被永久删除,且不会再得到的。
422Unprocesable entity[POST/PUT/PATCH]当创建一个对象时,发生一个验证错误。
500INTERNAL SERVER ERROR[*]服务器发生错误,用户将无法判断发出的请求是否成功。

0X04 如何用Python实现

Python有大量第三方库可以实现REST风格的API,我这里选用的是相对轻量化的一个 Flask。安装这个库最简单的方式还是用pip,根据环境变量的不同可能具体命令有所不同,在我的Linux上是用pip3 install flask就可以直接安装好的。 安装好后进入Python的交互式界面输入import flask如果没有出现Import Error就是安装好了。

0X05创建数据库和表

现在可以开始设计API了。既然是成绩管理系统,那么首先就要创建一个数据库,我这里的数据库是用的MariaDB。

列名类型含义
idint编号主键
namevarchar(10)学生姓名
numberchar(11)学号
pythonfloatPy成绩
cppfloatc++成绩
osfloat操作系统成绩
networkfloat计算机网络成绩
totalfloat总分
avefloat平均分

0X06 创建Py脚本

python
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无关的代码填好,下面开始正式完成各项功能。其实也就是连接了数据库而已

python
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数据

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。 具体功能代码实现如下,

python
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(), 201

0X09 删除已经存在的数据

根据标准,将API设计成DELETE方法,URL为http://localhost/v1/student/number=<number> 第一行的number=<number>可以将url中符合这种规范的匹配出来,配合方法定义时的参数,可以直接将url参数传入到方法体中。

python
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(), 204

0X0A 查询学生成绩

根据标准,将API设计成GET方法,URL为http://localhost/v1/student/sort_by=<sort_by>。提供了python/cpp/network/os/total/ave排序方式,(其实是数据库实现的)。

python
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), 200

0X0B 修改学生成绩

修改学生成绩和添加成绩几乎是一样的操作,只有这么几点是不太一样的。添加信息时如果学号已经存在了那就不能再添加了,而修改的时候是如果学号不存在才错误;添加和修改的SQL不同。就没有别的区别了。

python
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(), 201

0X0C 搞定所有API

现在就搞定了所有的API编写。现在我把所有代码贴上来,注意这段代码是用于Python3的。如果需要测试的话可以用Python自带的requests模块或者Postman软件来测试该API。

  • 声明:代码最后一行的app.run()方法,现在是只在本地监听的。可以改成app.run('0.0.0.0')就对外部监听了。
python
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()
本文标题
使用 Flask 设计实现一套 REST API【成绩管理系统】
文章作者
Shawn
版权声明
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。

如果这篇文章对你有帮助,可以请我喝杯咖啡 ☕

评论