第5章

智能Web App




本章基于Flask框架搭建Web服务器,客户端通过浏览器向服务器提交待识别的图像,服务器基于第4章创建的DenseNet121模型做出预测,并将预测结果返回给客户端,从而实现智能化的Web服务能力。服务器采用RESTful风格的API搭建Web服务,客户端App无论采用何种语言编程,均可通过HTTP访问服务器。


视频讲解



5.1环境准备
Flask是一个基于Python的轻量级WSGI Web应用程序框架,可扩展性好,拥有丰富的第三方支持库。用pip install flask命令在项目虚拟环境中安装Flask框架。
Postman是一款模拟HTTP请求的客户端调试工具,帮助程序员完成简单直观的HTTP请求测试。DB Browser for SQLite是实现SQLite数据库可视化管理的工具软件。可到官方网站下载安装Postman和DB Browser for SQLite。
本章开发的智能Web App基本架构如图5.1所示,服务器部署第3章创建的apple.db数据库和第4章完成的DenseNet121模型,客户机通过浏览器向服务器发送HTTP请求,服务器运行基于Flask开发的RESTful API,向客户机返回响应消息。


图5.1Web App智能服务框架


为了便于观察客户机与服务器之间的HTTP请求/响应关系,客户机采用Postman发送请求并接收响应消息。


视频讲解


5.2项目概要设计
服务器基于Flask框架实现Web API设计,包含的服务模块如图5.2所示,各模块的功能简述如下。
(1) HTTP状态码模块。用于学习和理解如何捕获Web响应的状态。
(2) 获取URL参数模块。用于理解Web API获取客户机请求参数的方法。
(3) 用户注册模块。获取客户机提交的注册数据,将其写入apple.db数据库中。
(4) 用户登录模块。配合JSON Web Token机制,完成用户的身份验证。
(5) 发送邮件模块。用户登录成功时向用户发送邮件,或者需要找回密码时,向用户发送邮件。
(6) 查询记录模块。实现两种查询方法: 一是查询apple.db所有记录; 二是有条件查询。操作结果反馈给客户机。
(7) 添加记录模块。向apple.db插入一条来自客户机的新数据。操作结果反馈给客户机。
(8) 更新记录模块。根据客户机指定的ID修改apple.db中的指定记录。操作结果反馈给客户机。
(9) 删除记录模块。根据指定记录的ID从数据库apple.db中删除指定的记录。操作结果反馈给客户机。
(10) 分类预测模块。调用DenseNet预测模型,对客户机发送过来的图像做预测,并将预测结果返回给客户机。








图5.2Web服务模块概要设计




视频讲解


5.3新建Flask Web项目
在NetworkProgram项目下新建文件夹chapter5,在chapter5下新建子目录static、templates和models。将第4章完成的DenseNet121模型复制到子目录models。第3章爬虫收集的数据存储在apple.db中,将数据库apple.db复制到chapter5目录。
在chapter5目录新建主程序文件app.py,当前项目结构如图5.3所示。


完成app.py的初始测试程序段P5.1,运行app.py,分别在浏览器和Postman观察客户端输出结果。

P5.1# Web服务测试

01from flask import Flask, jsonify

02import os

03app = Flask('__name__')  # 初始化App

04db_path = os.path.join(os.getcwd(), 'apple.db')  # 数据库路径

05model_path = os.path.join(os.getcwd(), 'models/densenet121.h5')  # 模型路径

06@app.route('/')   # 根目录Web服务

07def index():

08message = {'db':'apple.db', 'model': 'densenet121.h5'}

09return jsonify(message)

10if __name__ == '__main__':

11app.run(debug=True)




图5.3Web App项目初始结构


程序段第6行定义路径为根目录的Web服务,第7行定义服务调用的函数,第8行用字典模式表达返回客户端的消息格式,第9行用jsonify()函数将消息转换为JSON格式后返回。第11行设置Flask运行于Debug模式,便于调试程序时,客户端能够动态捕获服务器端的变化与调整。



视频讲解


5.4HTTP状态码
Web基于HTTP实现数据交换过程,请求过程与响应过程均包含HTTP控制头,HTTP控制头包含对终端用户不可见的元数据信息,分别用于描述请求过程和响应过程。

状态码是HTTP响应控制头中包含的数据,用于提示请求的结果类型。常见的HTTP状态码如表5.1所示。


表5.1常见的HTTP状态码



状态码描述举例
1×× 
服务器收到请求,需要请求者继续执行操作
100: 客户端应继续提交请求
2××
请求被成功接收并处理
200: 请求成功
3××
重定向,需要进一步的操作以完成请求
301: 资源被转移到其他URL
4××
客户端错误,请求包含语法错误或无法完成请求
404: 请求的资源不存在
5××
服务器错误,服务器在处理请求的过程中发生了错误
500: 服务器内部错误


HTTP状态码极其有用,客户机可以通过HTTP状态码判断请求的结果以及需要进一步采取的操作。
程序段P5.2演示了在Web服务中添加状态响应码的编程方法。

P5.2# HTTP状态码测试

01@app.route('/')   # 根目录Web服务

02def index():

03message = {'db':'apple.db', 'model': 'densenet121.h5'}

04return jsonify(message), 200

05@app.route('/not_found')

06def not_found():

07return jsonify(message='请求的资源不存在!'), 404

第4行添加了成功响应请求的状态码200,第7行语句添加了无法访问资源的状态码404。在Postman中分别对两个服务测试,注意观察状态码的反馈值是否与对应的服务相匹配。


视频讲解


5.5获取URL参数
客户端可以通过在URL末尾附加参数的方式向服务器提交数据,URL与参数之间以问号“?”间隔。为了测试URL参数获取方法,假定携带苹果树病虫害名称参数的URL如下所示: http://localhost?name=苹果黑星病。
程序段P5.3演示了服务器端获取URL参数值的方法。

P5.3# 获取URL参数

01@app.route('/parameters')

02def parameters():

03name = request.args.get('name')  # 获取URL参数

04if name in ['健康叶片','苹果黑星病','苹果锈病','多种病症']:

05return jsonify(msessage='数据库存在请求的数据:' + name), 200

06else:

07return jsonify(message='数据库不存在请求的数据:' + name), 401

第3行用Flask自带的request模块读取参数值。第5行、第7行在返回信息到客户端的同时,指定了HTTP状态码。
在Postman中测试客户端请求,观察反馈的信息与HTTP状态码。


视频讲解


5.6定义用户数据表
用DB Browser for SQLite打开chpater5目录下的数据库apple.db,新增用户数据表users,表结构定义如图5.4所示。


图5.4用户数据表的结构定义


字段id为自增长主键,name、email、password三个字段非空,其中email必须唯一。




视频讲解


5.7用户注册
定义用户注册API如程序段P5.4所示。用户注册数据一般通过表单提交,所以register服务被指定为用post()方法获取数据。对于邮箱账号已存在的用户,取消注册逻辑,反馈状态码为409的提示信息,表示禁止注册。如果邮箱账号不存在,则允许注册,完成注册逻辑后,反馈状态码为201的提示信息,提示用户注册成功。

P5.4# 用户注册

01@app.route('/register', methods=['post'])

02def register():

03email = request.form['email']

04conn = lite.connect(db_path)  # 打开数据库

05with conn:

06cur = conn.cursor()

07sql = f"select count(email) from users where email='{email}'"

08cur.execute(sql)

09count = cur.fetchone()[0]

10if count > 0:  # 邮箱账号已存在

11return jsonify(message='注册的邮箱已存在!'), 409

12else:

13name = request.form['name']

14password = request.form['password']

15sql = f"insert into users(name, email, password) values ('{name}', 

16'{email}', '{password}')"

17cur.execute(sql)

18return jsonify(message='用户注册成功!'), 201

用Postman做表单数据提交测试,如图5.5所示,收到的状态码201表示注册成功。


图5.5Postman提交用户注册数据并接收Web服务反馈信息


修改Postman表单数据,验证邮箱存在的情况,注意观察服务器响应的状态码以及提示信息。


视频讲解


5.8JSON Web令牌
如果需要限定只有users表中注册的用户才能对apples表中的数据做增、删、改、查操作,则需要在服务器端定义验证用户登录的服务模块。
HTTP是一种无状态的协议,如果用户向服务器提供了用户名和密码做身份认证,那么在做下一次请求时,需要重新提交用户名和密码进行身份验证,常用的解决方案是在服务器端存储一份用户登录的信息,这就是传统的基于Session的认证机制。
随着认证用户的增多,基于Session的认证在服务器端的开销会明显增大,在面对多服务器集群模式时,用户的Session会被迫绑定到一台服务器上,限制了集群服务器的负载均衡能力。
基于JSON Web令牌的认证机制不需要在服务器端保留用户的认证信息或会话信息,这就意味着基于令牌的认证机制不需要考虑用户在哪一台服务器登录,可扩展性明显优于Session机制。
本章采用JSON Web令牌作为身份验证方法。在项目虚拟环境执行命令: 

pip install flask-jwt-extended

完成FlaskJWTExtended模块的安装。调用create_access_token() 函数创建JSON Web令牌,调用jwt_required()函数保护Web服务,get_jwt_identity()返回用户的身份标识。
JSON Web Token的基本结构如图5.6所示,包括Header、Payload、Signature三部分。Header用Base64编码封装加密算法等信息,Payload用Base64编码封装传递的敏感数据信息,Signature封装基于Header、Payload以及密钥secret生成的签名。


图5.6JSON Web Token的基本结构


JSON Web令牌的认证机制如图5.7所示,简述如下。
(1) 用户通过用户名和密码来请求服务器,服务器验证用户的信息。
(2) 验证通过后,服务器向用户发送一个令牌(Token)。
(3) 客户端存储令牌,每次新请求时附加自己的令牌,服务器端通过验证令牌,决定是否提供服务。


图5.7JSON Web Token认证机制




视频讲解


5.9用户登录
程序段P5.5实现了基于JSON Web Token的登录逻辑。

P5.5# 用户登录

01from flask_jwt_extended import JWTManager, jwt_required, create_access_token

02app.config['SECRET_KEY'] = 'my-secret'

03jwt = JWTManager(app)

04@app.route('/login', methods=['post'])

05def login():

06email = request.form['email']

07password = request.form['password']

08conn = lite.connect(db_path)  # 打开数据库

09with conn:

10cur = conn.cursor()

11sql = f"select count(email) from users where email='{email}' and password='{password}'"

12cur.execute(sql)

13count = cur.fetchone()[0]

14if count > 0:  # 允许登录

15acess_token = create_access_token(identity=email) # 创建Token

16return jsonify(message='登录成功!', acess_token=acess_token), 202

17else:  # 拒绝登录

18return jsonify(message='用户Email或者密码错误,登录失败!'), 401


复制图5.8所示的access_token,粘贴到https://jwt.io/网站的编码解码框,可以观察解码后的Token结构信息。


图5.8用Postman提交用户登录数据并接收Token




视频讲解


5.10发送邮件找回密码
应用系统经常需要向用户自动发送邮件,来实现找回密码、激活账户以及推送某些信息。
Python标准库自带的smtplib包可用于发送电子邮件,基于smtplib实现的FlaskMail邮件扩展框架提供了更为简单的API接口,与Flask无缝集成,只需在应用程序中设置好邮件服务器的SMTP参数,即可轻松编写发送邮件的脚本。
在项目虚拟环境执行命令pip install flaskmail,安装FlaskMail扩展库。
实践中密码是加密存储的,原则上不允许找回原密码,但可以根据收到的验证码去设置新密码,程序段P5.6为了演示发送邮件的逻辑,暂且允许用户找回原密码,根据用户提供的邮箱,向用户发送一封包含密码信息的邮件。

P5.6# 发送邮件,找回密码

01from flask_mail import Mail, Message

02app.config['MAIL_SERVER'] = 'smtp.163.com'   # 发送邮件的服务器,需要启用SMTP服务

03app.config['MAIL_PORT'] = 465  # 采用SSL通信的端口,否则为25

04app.config['MAIL_USE_TLS'] = False

05app.config['MAIL_USE_SSL'] = True  # 采用SSL通信

06app.config['MAIL_USERNAME'] = 'your_webmaster'  # 用户账号,此处为示例

07app.config['MAIL_PASSWORD'] = 'ANURWYMPLMNDEFXLQK'  # SMTP授权码,此处为示例

08mail = Mail(app)  # 创建邮件服务对象

09@app.route('/get_password', methods=['post'])

10def get_password():

11email = request.form['email']

12conn = lite.connect(db_path)  # 打开数据库

13with conn:

14cur = conn.cursor()

15sql = f"select email, password from users where email='{email}'"

16cur.execute(sql)

17records = cur.fetchall()

18if records:  # 允许找回

19password = records[0][1]

20msg = Message(f'找回的密码是:{password}',

21sender='your_webmaster@163.com',

22recipients=[email])  # 定义邮件消息

23mail.send(msg)  # 发送邮件

24return jsonify(message=f'密码已发送至邮箱:{email}'), 202

25else:  # 拒绝找回密码

26return jsonify(message='用于找回密码的Email地址不正确!'), 402



视频讲解


5.11查询记录
程序段P5.7根据指定的ID对apples数据表进行查询,返回记录详细信息。

P5.7# 根据ID查询苹果数据集单条记录

01@app.route('/get_details/<int:apple_id>', methods=['get'])

02def get_details(apple_id:int):

03conn = lite.connect(db_path)  # 打开数据库

04with conn:

05cur = conn.cursor()

06sql = f"select id,name,feature,regular,cure,img_url from apples where id='{apple_id}'"

07cur.execute(sql)

08records = cur.fetchall()

09if records:  # 找到记录

10record = dict(zip(['id', 'name', 'feature', 'regular', 'cure', 'img_url'], records[0]))

11return jsonify(record), 200

12else:  # 没找到

13return jsonify(message=f'ID为{apple_id}的记录不存在!'), 403

程序段P5.8对apples数据表进行查询,返回所有记录详细信息。

P5.8# 查询苹果数据集所有记录

01@app.route('/get_all_details', methods=['get'])

02def get_all_details():

03all_records = []

04conn = lite.connect(db_path)  # 打开数据库

05with conn:

06cur = conn.cursor()

07sql = f"select * from apples"

08cur.execute(sql)

09records = cur.fetchall()

10if records:  # 找到记录

11for record in records:

12item = dict(zip(['id', 'name', 'feature', 'regular', 'cure', 'img_url'], record))

13all_records.append(item)

14return jsonify(all_records), 200

15else:  # 没找到

16return jsonify(message=f'数据集为空!'), 404



视频讲解


5.12添加记录
程序段P5.9向apples数据表添加一条新记录。第2行语句要求用户提供身份验证,即提供其合法的JSON Web Token才能访问该模块,完成添加新数据的操作。

P5.9# 添加记录

01@app.route('/add_apple', methods=['post'])

02@jwt_required()  # 需要身份验证

03def add_apple():

04name = request.form['name']

05conn = lite.connect(db_path)  # 打开数据库

06with conn:

07cur = conn.cursor()

08sql = f"select count(name) from apples where name='{name}'"

09cur.execute(sql)

10count = cur.fetchone()[0]

11if count > 0:  # 记录已存在

12return jsonify(message='添加的记录已存在!'), 409

13else:

14feature = request.form['feature']

15regular = request.form['regular']

16cure = request.form['cure']

17img_url = request.form['img_url']

18sql = f"insert into apples(name, feature, regular, cure, img_url) " \

19f"values ('{name}', '{feature}', '{regular}', '{cure}', '{img_url}')"

20cur.execute(sql)

21return jsonify(message='成功添加新记录!'), 201

在Postman中做应用场景测试,观察反馈的输出结果,验证模块逻辑的正确性。




视频讲解


5.13更新记录
程序段P5.10更新apples数据表中的记录。第2行语句要求用户提供身份验证,即提供其合法的JSON Web Token才能访问该模块,完成数据修改操作。

P5.10# 更新记录

01@app.route('/update_apple', methods=['post'])

02@jwt_required()  # 需要身份验证

03def update_apple():

04id = request.form['id']

05conn = lite.connect(db_path)  # 打开数据库

06with conn:

07cur = conn.cursor()

08sql = f"select count(id) from apples where id='{id}'"

09cur.execute(sql)

10count = cur.fetchone()[0]

11if count <= 0:  # 记录不存在

12return jsonify(message='修改的记录不存在!'), 409

13else:

14name = request.form['name']

15feature = request.form['feature']

16regular = request.form['regular']

17cure = request.form['cure']

18img_url = request.form['img_url']

19sql = f"update apples set name = '{name}', feature = '{feature}', " \

20f"regular = '{regular}', cure = '{cure}', img_url = '{img_url}' " \

21f"where id =  '{id}'"

22cur.execute(sql)

23return jsonify(message='成功修改记录!'), 201

在Postman中做应用场景测试,观察反馈的输出结果,验证模块逻辑的正确性。


视频讲解


5.14删除记录
从数据库中删除记录,一般有两种策略: 一种策略是不做数据的物理删除,而是添加一个新字段,用以标记记录已被删除,删除的数据可被恢复; 另一种策略是直接物理删除数据。为简单起见,程序段P5.11直接物理删除apples数据表中的记录。第2行语句要求用户提供身份验证,即提供其合法的JSON Web Token才能访问该模块,完成记录删除操作。


P5.11# 删除记录

01@app.route('/delete_apple/<int:id>', methods=['delete'])

02@jwt_required()  # 需要身份验证

03def delete_apple(id:int):

04conn = lite.connect(db_path)  # 打开数据库

05with conn:

06cur = conn.cursor()

07sql = f"select count(id) from apples where id='{id}'"

08cur.execute(sql)

09count = cur.fetchone()[0]

10if count <= 0:  # 记录不存在

11return jsonify(message='删除的记录不存在!'), 404

12else:

13sql = f"delete from apples where id =  '{id}'"

14cur.execute(sql)

15return jsonify(message='成功删除记录!'), 202

在Postman中做应用场景测试,观察反馈的输出结果,验证模块逻辑的正确性。


视频讲解


5.15分类预测
程序段P5.12定义了一个Web预测服务模块。

P5.12# 定义模型预测服务

01import io

02import base64

03import numpy as np

04from PIL import Image

05from tensorflow.keras.models import load_model

06from tensorflow.keras.preprocessing.image import img_to_array

07model = load_model('./models/densenet121.h5')  # 加载模型

08# 图像预处理

09def preprocess_image(image, target_size):

10if image.mode != 'RGB':

11image = image.convert('RGB')

12image = image.resize(target_size)

13image = img_to_array(image)

14image = image / 255.

15image = np.expand_dims(image, axis=0)

16return image

17# 模型预测

18@app.route('/predict', methods=['post'])

19def predict():

20message = request.get_json(force=True)

21image = message['image']

22decode_image = base64.b64decode(image)

23image = Image.open(io.BytesIO(decode_image))

24processed_image = preprocess_image(image, target_size=(512, 512))

25prediction = model.predict(processed_image)[0].tolist()  # 预测

26response = {

27'prediction': {

28'healthy': prediction[0],

29'multiple_diseases': prediction[1],

30'rust': prediction[2],

31'scab': prediction[3]

32}

33}

34return jsonify(response), 200



视频讲解


5.16前端页面
程序段P5.13实现Web前端页面的基本逻辑。

P5.13# Web前端页面设计

01<!DOCTYPE html>

02<html lang="en">

03<head>

04<meta charset="UTF-8">

05<title>苹果树病虫害预测</title>

06</head>

07<body>

08<input id='image-selector' type='file'>

09<button id='predict-button' type='button'>预 测</button>

10<p style="font-weight: bold">预测结果:</p>

11<p>healthy:<span id="healthy"></span></p>

12<p>multiple_diseases:<span id="multiple_diseases"></span></p>

13<p>rust:<span id="rust"></span></p>

14<p>scab:<span id="scab"></span></p>

15<img id="selected-image" src=""/>

16<script src="jquery-3.6.0.min.js"></script>

17<script>

18let base64Image;

19<!--选择图片-->

20$('# image-selector').change(function () {

21let reader = new FileReader();

22reader.onload = function (e) {

23let dataUrl = reader.result;

24$('# selected-image').attr('src', dataUrl);

25base64Image = dataUrl.replace('data:image/jpeg;base64,', '');

26console.log(base64Image)

27} <!-- end on load -->

28reader.readAsDataURL($('# image-selector')[0].files[0]);

29$('# healthy').text('');

30$('# multiple_diseases').text('');

31$('# rust').text('');

32$('# scab').text('');

33}); <!-- end on change -->

34<!--单击"预测"按钮-->

35$('# predict-button').click(function (event) {

36let message = {

37image: base64Image

38}

39console.log(message)

40$.post('http://localhost:5000/predict', JSON.stringify(message), function (response) {

41$('# healthy').text(response.prediction.healthy.toFixed(6));

42$('# multiple_diseases').text(response.prediction.multiple_diseases.toFixed(6));

43$('# rust').text(response.prediction.rust.toFixed(6));

44$('# scab').text(response.prediction.scab.toFixed(6));

45});

46});

47</script>

48</body>

49</html>

启动服务器,打开浏览器,在地址栏中输入预测页面地址http://localhost:5000/static/predict.html,从测试集目录中任意选择一幅测试图片,单击“预测”按钮,观察服务器返回的预测结果,如图5.9所示。


图5.9前端页面测试结果


本章设计的Web服务器已经部署于腾讯云服务器,读者可以访问页面http://120.53.107.28/static/predict.html做应用测试。


视频讲解


5.17小结
本章以第3章网络爬虫生成的数据库apple.db为基础,运用Flask构建Web服务框架,实现了用户注册,登录,邮件找回密码,对数据库的增、删、改、查,基于JSON Web Token的认证,全面介绍了基于RESTful风格的Web API设计方法。
基于第4章完成的DenseNet121训练模型,定义了Web API预测服务,通过与前端页面的联合测试,展示了智能Web App的设计原理与方法,项目的可扩展性好,易于部署推广,应用价值高。
5.18习题
一、 简答题
1. 智能Web App将智能模型部署于服务器端,其优点是什么?缺点是什么?
2. Flask作为基于Python的轻量级Web框架,有哪些特点?
3. Postman作为模拟HTTP请求的客户端调试工具,有哪些优点?
4. DB Browser for SQLite作为SQLite数据库可视化管理工具,有哪些优点?
5. 简述基于Flask框架定义的主程序和Web API服务的一般语法形式。
6. 如何实现Web API服务与客户机之间的JSON数据交换?
7. 简述HTTP状态码的分类及其含义。
8. 客户端可以通过在URL末尾附加参数的方式向服务器提交数据,简述URL参数的一般语法形式。
9. 描述用DB Browser for SQLite创建用户数据表的基本步骤。
10. 在Web服务器端,如何区分客户机提交的请求类型是get还是post?
11. JSON Web Token的基本结构是什么?
12. 发送邮件,既可以采用Python自带的库模块smtplib,也可以采用第三方扩展库模块FlaskMail。描述用FlaskMail发送邮件的基本步骤。
13. 前端页面将待发送的图片进行Base64编码后再发给服务器,简述Base64编码的特点。
14. 简述以JSON格式表达数据的优点。
二、 编程题
修改客户机向服务器发送图片的逻辑,不采用Base64编码,而是直接将图片的字节流数据发送到服务器,服务器端的接收逻辑也需要做出同步修改。