佛爷用Flask搭建前后端全栈网站(Machine Learning 数据标注工具)步骤详解

一、Flask

Flask是一个使用 Python 编写的轻量级 Web 应用框架。其 WSGI 工具箱采用 Werkzeug ,模板引擎则使用 Jinja2 。Flask使用 BSD 授权。
简单点说就是后端开发框架。简单分析下Python做Web开发的几个常用框架:

  1. Django:是一个重武器,包含了web开发中常用的功能、组件的框架;(ORM、Session、Form、Admin、分页、中间件、信号、缓存、ContenType…);
  2. Tornado:两大特性就是异步非阻塞、原生支持WebSocket协议;
  3. Flask:封装功能不及Django完善,性能不及Tornado,但是Flask的第三方开源组件比丰富;http://flask.pocoo.org/extensions/
  4. Bottle:比较简单

说这么多也没啥用,总结就是一句话,基于Python的前提下,小型web应用设计的功能点不多使用Flask;大型web应用设计的功能点比较多使用的组件也会比较多,使用Django(自带功能多不用去找插件);如果追求性能可以考虑Tornado。

二、Flask蓝图分离模块

Flask中的蓝图旨在针对这些情况:

  1. 把一个应用分解成一系列的蓝图。对于大型的应用是理想化的;一个项目能实例化一个应用, 初始化一些扩展,以及注册一系列的蓝图。
  2. 以一个 URL 前缀和/或子域在一个应用上注册蓝图。 URL 前缀/子域名中的参数即成为这个蓝图下的所有视图函数的共同的视图参数(默认情况下)。
  3. 在一个应用中用不同的 URL 规则多次注册一个蓝图。
  4. 通过蓝图提供模板过滤器、静态文件、模板和其它功能。一个蓝图不一定要实现应用或视图函数。
  5. 初始化一个 Flask 扩展时,在这些情况中注册蓝图。

上面是官方的介绍,总结一下就是:
蓝图功能,就好像把一个flask项目分成了N个flask项目。用来分离route路由表。基础flask教程中,route都写在main文件中。当项目够大的时候,几百个路由,怎么写?当项目是多人团队开发的时候怎么写?当新版本上线的时候,老版本怎么兼容?

使用蓝图,就能解决上面的问题。

大概就是把@app.route()给分离成多个文件来写,方便你找到需要修改的地方。方便团队协作时,个人写个人的功能模块,写在自己的文件里。新版本和旧版本可以同时在线,通过不同的URL来调用。

三、如何构建和注册蓝图(这里就是Flask项目目录以及架构搭建,方便未来当做模板使用)

佛爷用Flask搭建前后端全栈网站(Machine Learning 数据标注工具)步骤详解_第1张图片
需要掌握基本Flask项目的基础,这里不多赘述。我就按照我的理解去逐一说明这么模板架构,其实不管怎么分离文件,最终都是一个脚本,理论上,你写在一起,在一个文件中,都是可以正常运行的。只不过高大上和模块分离编程思想让我们改变了原始的编程方式。

首先,标注红字的专门针对蓝图去讲解,我们从外到内,从程序的开始到结束来讲解。一个完整的项目,需要再一个文件夹中,名字取项目名字,在这个项目的层级下,拥有项目核心app文件夹和一个项目启动py脚本,也就是manage.py文件(红色第一步)。我们看manage.py内都是怎么定义的:

from app import app
from gevent import monkey
import gevent
import gevent.pywsgi

if __name__ == '__main__':
    # monkey.patch_all() # 有待考证
    # app.run(host='127.0.0.1')
    gevent_server = gevent.pywsgi.WSGIServer(('127.0.0.1', 5000), app)
    gevent_server.serve_forever() 

其实,这就是入口运行文件,代码很简单,为了支持并发条件,使用gevent协程来起一个服务。接下来追溯到app项目目录,from app import app,来看看app的__init__文件,也就是红色第二步

from flask import Flask
from flask_pymongo import PyMongo
import pandas as pd

"""Flask项目初始化模块"""
app = Flask(__name__)
app.debug = True

"""数据库初始化模块"""
# 在flask项目中,Session, Cookies以及一些第三方扩展都会用到SECRET_KEY值,这是一个比较重要的配置值。
# 该值基本上自己定义,123456也行,但是最好生成一些复杂度高一点的字符串
app.config['SECRET_KEY'] = '72425d14d4e04f7288f9b5c7a7596b'
# 实例化数据库配置
app.config["MONGO_URI"] = "mongodb://localhost:27017/all_remark_data"
mongo = PyMongo(app)

"""定义部分其他全局变量模块"""
data_name_and_id = pd.read_csv('app/data_config.csv').values.tolist()
SCORE_TABLE_DICT = {0: 15.0,
                    1: 23.0,
                    2: 55.0,
                    3: 80.0,
                    4: 99.0}

"""蓝图注册模块"""
from app.home import home as home_blueprint

app.register_blueprint(home_blueprint)

从代码从可以看出文件其实就是各个功能模块的初始化,可以作为模板,千万注意,蓝图的注册要放在最后,最好不改变模块顺序,只做替换就好。在此同级目录下还有static和template两个项目目录,简单说明一下,static里面放所有静态资源,例如css,js,images等;template里面分应用目录(一会会说到,所有应用目录都统一),放的就是基于Jinjia2模板的Html文件,有它才能显示网页。
还有models.py文件(一会儿说)和home文件目录。这个home文件目录就是接下要说的应用功能模块,此项目中我只有一个应用功能,其实一般来说还有admin,有需要的直接复制架构就好。

在应用模块当中需要定义三个py文件,init、views和forms。让我们逐个说明,首先在注册蓝图的代码中有from app.home import home as home_blueprint,那自然咱们先要追溯__init__.py(也就是红色第三步),具体代码如下:

# -*- coding:utf-8 -*-
# 定义蓝图
from flask import Blueprint

home = Blueprint('home', __name__)

import app.home.views

这就是定义蓝图的关键代码,没什么好说的,直接模版吧。关键是最后一句代码import app.home.views,就把我们直接带入到view.py文件中,来看看主要view视图代码吧:

# 导入定义蓝图的变量
from . import home
# 导入flask中需要的模块
from flask import render_template, redirect, url_for, flash, session, request

"""数据模块的导入"""
from app.models import SaveScoresModel

"""表单提交模块的导入"""
from app.home.forms import SaveScoreForm

# 导入一些其他全局变量和一些所需的库
from app import data_name_and_id, SCORE_TABLE_DICT
import random

# 全局定义数据模型,模拟采用单例模型,主要是连接数据库只需要连接一次就好
ssm = SaveScoresModel()


# 主页面路由
@home.route('/')
def index():
    return render_template('home/home.html')


# 其他页面路由(这里可以复制很多)
@home.route("/partition-score-remark/", methods=['GET', 'POST'])
def partition_score_remark():
    ...
    form = SaveScoreForm()
 	...
    session['image_path'] = '../../static/data/images/{0}.png'.format(json_id)
    ...
    if form.validate_on_submit():
        data = form.data
      	print(data)
        return redirect(url_for('home.partition_score_remark'))
	...
    return render_template('home/partition-score-remark.html', form=form,
     image_path=session['image_path'])

一些关键的注释都写在代码中了,关键部分都可以当作模版使用。视图模块脚本就是所有数据传输的控制器,也是定义路由的地方。关键是在视图模块中使用了表单提交模块forms.py文件和数据库模块models.py文件。我们就来看看数据库模块:

from app import mongo
from datetime import datetime


class SaveScoresModel(object):
    def __init__(self):
        self.db = mongo.db

    def add_json_id_and_score(self, oid, score, ip_addr):
        new_record = {.....}
        return self.db.json_score_records.insert_one(new_record)

    def get_all_remark_nums_from_ip(self, ip_addr):
        return self.db.json_score_records.find({'ip': ip_addr}).count()

代码因为需要保护项目需要,部分信息经过处理。数据模板可以复用。

表单提交模块:

from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField, SelectField, SelectMultipleField, FloatField
from wtforms.validators import DataRequired, ValidationError
from flask import flash
from wtforms.fields.core import RadioField


class SaveScoreForm(FlaskForm):
    living_radio = RadioField(
        choices=(
            (0, "很差"),
            (1, "较差"),
            (2, "一般"),
            (3, "较好"),
            (4, "最好")
        ),
        coerce=int,  # 限制是int类型的
        render_kw={
            'type': "radio",
            'name': "living",
        }
    )
    dinner_radio = RadioField(
        choices=(
            (0, "很差"),
            (1, "较差"),
            (2, "一般"),
            (3, "较好"),
            (4, "最好")
        ),
        coerce=int,  # 限制是int类型的
        render_kw={
            'type': "radio",
            'name': "dinner",
        }
    )
    submit = SubmitField(
        'submit',
        render_kw={
            'type': 'submit',
            'class': 'form-control',
            'id': 'submit',
            'value': 'Submit Your Comment',
        }
    )

    def validate_living_radio(self, field):
        living_radio_value = field.data
        if not living_radio_value:
           flash('XXXXXX')

    def validate_dinner_radio(self, field):
        dinner_radio_value = field.data
        if not dinner_radio_value:
            flash('XXXXXX')

表单提交验证模块核心代码就在这了,flask_wtf 和 wtforms 模块很好的分装了表单中的类型。
于是所有的蓝图定义也就完毕,所有的脚本运行流程也就完成。其中注意数据绑定的问题,就是views模块到html文件中数据传递的问题就可以了。

四、Flask项目中各个组件的使用详情(日后随时更新)
(1)gevent.WSGIServer使用

WSGI:Web服务器网关接口(Python Web服务器网关接口,缩写为WSGI)是为Python语言定义的Web服务器和Web应用程序或框架之间的一种简单而通用的接口。

WSGI服务器:

  1. WSGI服务器所做的工作仅仅是将从客户端收到的请求传递给WSGI应用程序,然后将WSGI应用程序的返回值作为响应传给客户端;
  2. WSGI应用程序可以是Flask,Django等web框架。

WSGI app接口介绍

  1. WSGI application接口应该实现为一个可调用对象,例如函数,方法,类,含__call__方法的实例。这个可调用对象可以接收2个参数;
  2. 一个字典,该字典可以包含了客户端请求的信息以及其他信息,可以认为是请求上下文,一般叫做环境(编码中多简写为ENVIRON,ENV);
  3. 一个用于发送HTTP响应状态(HTTP status),响应头(HTTP headers)的回调函数;
  4. 同时,可调用对象的返回值是响应正文(response body),响应正文是可迭代的,并包含了多个字符串。

异步 WEB 架构的特点:

gevent 为 Python 提供了比较完善的协程支持,其基于 greenlet 实现协程。
当 greenlet 遇到如网络访问、磁盘 IO 等操作时,就将自动切换至其他的 greenlet,待操作完成后,在适合的时间点回切 greenlet 继续执行。由于网络访问、磁盘 IO 等操作耗时较长,且实际 CPU 使用率较低(大部分工作由 DMA 等设备完成)。所以倘若非异步,涉及以上操作并发将以顺序执行, CPU 长期处于空闲状态。而异步模式将能实现并发程序间的切换,从而保证 CPU 有较高的利用率,而不是等待如网络访问、磁盘 IO 等操作。

注意:gevent 的使用并不能减少实际 CPU 的使用量,所以若程序的执行过程消耗的全为 CPU 资源,则其异步也是毫无意义的。
注意:为实现 Flask 与 gevent 的结合,需在程序开头引入 monkey patch。monkey patch 将以闭包的形式修改可以实现异步的标准库,从而实现异步。
注意:需使用支持 gevent 的 WSGI,例如:gevent.pywsgi、gunicorn 等。

实例模板:

from flask import  Flask
import gevent.pywsgi
import gevent

app = Flask(__name__)

@app.route('/')
def handle():
    return 'welcome to gevent lesson!'
    
gevent_server = gevent.pywsgi.WSGIServer(('0.0.0.0', 5000), app)
gevent_server.serve_forever()
(2)MongoDB数据库和flask_pymongo模块

部分关于MongoDB数据库的知识在我的 Python 操作MongoDB数据库 文章中讲述。

关于Flask 扩展 Flask-PyMongo,笔者找到一篇讲解非常优秀的博文,
(https://www.cnblogs.com/Erick-L/p/7047064.html )。里面说的很细致,代码很全。

(3)flask插件系列之Flask-WTF表单

flask_wtf是flask框架的表单验证模块,可以很方便生成表单,也可以当做json数据交互的验证工具,支持热插拔。
根据三个网站的内容,足以说清楚表单提交和验证的方法。

官方网站:http://www.pythondoc.com/flask-wtf/
flask插件系列之Flask-WTF表单
一起学习python flask之三:用flask_wtf轻松实现表单

(4)理解url_for、redirect和render是如何配合完成页面跳转的(转载自(https://blog.csdn.net/zyself/article/details/83014342))
佛爷用Flask搭建前后端全栈网站(Machine Learning 数据标注工具)步骤详解_第2张图片

我们用上面的例子来分析删除一条记录的处理过程,程序流程大致是:

  • 客户在页面某条记录点击 “删除”按钮,这个按钮实质是一个包含了id参数的url链接。(比如/tag/del/3,其中3是id参数)
  • 收到访问页面请求,程序直接到tag_del函数块,在该函数中处理相关工作(比如从数据库中删除id=3的数据)
  • 完成处理后,根据逻辑就是重新显示记录列表,因此用url_for 拼接出访问记录列表页面的url(如:/tag/list/1,参考:图片中第一步的说明)
  • 有了url后,redirect重定向到url(这个操作类似用户主动在浏览器中输入该url访问页面,参考第二步说明)
  • 之后,就如客户主动访问页面类似,系统render_template渲染出页面展现给了用户。(参看第三、第四步说明)

附:url_for 、redirect、render 区别

  • url_for的作用:就是拼接出字符串类型的url。如 url_for() 函数最简单的用法是以视图函数名作为参数, 返回这个视图函数对应的头顶上@语法糖中的URL。url_for除了上面例子中说的拼接外,还可以拼接静态文件,请参考下面附加说明。
  • redirect 是重定向函数:这个函数的原型是def redirect(location, code=302, Response=None),其中location参数就是要重定向到的url,这个参数可以是直接的提供www.baid.com这样的,也可以url_for(XXXX)从拼接而来的,调用这个函数,就类似用户主动在浏览器中输入该url访问页面。例如 调用return redirect('https://www.baidu.com') 后,用户最终看到的就是页面跳转到了百度。
  • render渲染模板函数:这个函数是实实在在渲染模板,呈现给最终的用户。

简单说,就是:

  • url_for 拼接出网页的网址url,类似于拼接字符串
  • redirect 指示浏览器访问网址,类似用户地址栏输入具体地址访问网页
  • render 渲染出网页,类似用户输入了地址,按了回车后,浏览器展现出网页页面

附:url_for 是按什么规则拼接网页地址Url的?

url_for原型:def url_for(endpoint, **values)
1、反转url:一般我们通过一个URL就可以执行到某一个函数(@route路由)。如果反过来,我们知道一个函数,怎么去获得这个URL呢?url_for函数就可以帮我们实现这个功能。url_for()函数接收两个及以上的参数,他接收函数名作为第一个参数,第二个参数则接收对应URL里面参数(下面例子中page就是参数),如果还出现其他的参数,则会添加到URL的后面作为查询参数(类似这样:http://localhost:5000/admin/tag/list/1?abc),按这样的规则返回对应的URL。简单的讲就是,就是url_for会自动在视图模板中寻找和自己第一个参数endpoint一样函数,然后函数头顶@route中的url。 如果没有找到,就看下面第二点。
如下面的例子:因为有函数名tag_list和第一个参数一样,所以返回结果就是http://localhost:5000/admin/tag/list/1。当然,前面的http://localhost:5000/admin这一截是蓝图控制的。
在这里插入图片描述
2、直接字符串拼接:如果url_for没有找到和自己第一个参数一样名称的函数,则直接把第一个参数按普通字符串一样和后面的参数拼接。如下面的静态文件就是直接字符串模式拼接的,调用 url_for(‘static’,filename=‘base/image/logo.png’) ,得到的url是:http://localhost:5000/static/base/image/logo.png
佛爷用Flask搭建前后端全栈网站(Machine Learning 数据标注工具)步骤详解_第3张图片
web程序不仅由python代码和模板组成,还包括静态文件 ,例如:代码中引用的图片,javaScripts代码,CSS等
对静态文件static目录的引用,是被当做特殊的路由处理的 ,因为,默认情况下,flask在程序根目录下名为static的子目录下寻找静态文件,

为什么需要url_for

  1. 将来如果修改了URL,但没有修改该URL对应的函数名,就不用到处去替换URL了。
  2. url_for会自动的处理那些特殊的字符,不需要手动去处理。
    url = url_for('login',next='/')
    # 会自动的将/编码,不需要手动去处理。
    # url=/login/?next=%2F
    

强烈建议以后在使用url的时候,使用url_for来反转url。

(5)flask内置session处理机制

在解析 session 的实现之前,我们先介绍一下 session 怎么使用。session 可以看做是在不同的请求之间保存数据的方法,因为 HTTP 是无状态的协议,但是在业务应用上我们希望知道不同请求是否是同一个人发起的。比如张三,王二都在自己的手机上用淘宝购物,将想购买的商品放入购物车中,当王二,张三结账时,不能将他俩的购物车混淆了,服务器区分和保存购物车数据的方法就是session。

flask的session是基于cookie的会话保持。简单的原理即:

当客户端进行第一次请求时,客户端的HTTP request(cookie为空)到服务端,服务端创建session,视图函数根据form表单填写session,请求结束时,session内容填写入response的cookie中并返回给客户端,客户端的cookie中便保存了用户的数据。

当同一客户端再次请求时, 客户端的HTTP request中cookie已经携带数据,视图函数根据cookie中值做相应操作(如已经携带用户名和密码就可以直接登陆)。

在 flask 中使用 session 也很简单,只要使用 from flask import session 导入这个变量,在代码中就能直接通过读写它和 session 交互。
示例代码:

from flask import Flask, session, escape, request
 
app = Flask(__name__)
app.secret_key = 'please-generate-a-random-secret_key'
 
 
@app.route("/")
def index():
    if 'username' in session:
        return 'hello, {}\n'.format(escape(session['username']))
    return 'hello, stranger\n'
 
 
@app.route("/login", methods=['POST'])
def login():
    session['username'] = request.form['username']
    return 'login success'
 
 
if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=True)

上面这段代码模拟了一个非常简单的登陆逻辑,用户访问 POST /login 来登陆,后面访问页面的时候 GET /,会返回该用户的名字。

flask中session使用非常简单,但是实现原理却没那么简单,下面我们通过几个问题来弄清楚session是如何实现的。

(6)有待更新

你可能感兴趣的:(数据库,Python,机器学习,全栈工程师)