Flask 框架

本贴最后更新于 1276 天前,其中的信息可能已经事过景迁

Flask 基础

Flask 简介

Flask 是一个使用 Python 编写的轻量级 Web 应用框架。其 WSGI 工具箱采用 Werkzeug ,模板引擎则使用 Jinja2 。

所有的扩展功能都需要使用第三方扩展来实现。

image20200904105728490.png

英文指导文档:https://flask.palletsprojects.com/en/0.12.x/

中文指导文档:https://dormousehole.readthedocs.io/en/latest/

安装

  1. 安装 python

  2. 更换 pip 源

    1. 清华:https://pypi.tuna.tsinghua.edu.cn/simple
    2. 阿里云:https://mirrors.aliyun.com/pypi/simple/
    3. 豆瓣:https://pypi.douban.com/simple/

    临时使用:在使用 pip 的时候加参数-i https://pypi.tuna.tsinghua.edu.cn/simple
    CentOS 修改 ~/.pip/pip.conf (没有就创建一个), 修改 index-url 为国内镜像地址,内容如下:

    [global] 
    timeout = 60000
    index-url = https://pypi.tuna.tsinghua.edu.cn/simple 
    

    windows 下,直接在 C:\Users\XXX\目录中创建一个 pip 目录,如:C:\Users\xx\pip,新建文件 pip.ini,内容如下

    [global] 
    timeout = 60000
    index-url = https://pypi.tuna.tsinghua.edu.cn/simple 
    
  3. 安装 flask

    pip install flask
    

Hello World

使用 pyCharm 新建 Flask 项目

app.py

# 导入Flask扩展
from flask import Flask

# 创建Flask应用程序实例
# __name__ :为了确定资源所在路径
app = Flask(__name__)


# 定义路由和视图函数
# Flask中定义路由是通过装饰器实现的
# 路由默认只支持GET,如果需要增加,需要自行制定
@app.route('/', methods=['GET', 'POST'])
def index():
    return 'Hello Flask!'

运行项目,用浏览器打开 http://127.0.0.1/

参数讲解

app = Flask(name)

flask.app.Flask 

def __init__(self,
             import_name: Any,
             static_url_path: Any = None,
             static_folder: str = "static",
             static_host: Any = None,
             host_matching: bool = False,
             subdomain_matching: bool = False,
             template_folder: str = "templates",
             instance_path: Any = None,
             instance_relative_config: bool = False,
             root_path: Any = None) -> Any

常用参数:

  • import_name
    • Flask 程序所在的包(模块),传 __name__ 就可以
    • 其可以决定 Flask 在访问静态文件时查找的路径
  • static_url_path
    • 静态文件访问路径,可以不传,默认为:/ + static
  • static_folder
    • 静态文件存储的文件夹,可以不传,默认为 static
  • template_folder
    • 模板文件存储的文件夹,可以不传,默认为 templates

相关配置加载方式

在 Flask 程序运行的时候,可以设置相关配置比如:配置 Debug 模式,配置数据库连接地址等,设置 Flask 配置有以下三种方式:

  • 从配置对象中加载(常用)
    • app.config.from_object()
  • 从配置文件中加载
    • app.config.from_pyfile()
  • 从环境变量中加载(了解)
    • app.config.from_envvar()
# 配置对象,里面定义需要给 APP 添加的一系列配置
class Config(object):
    DEBUG = True
    
# 从配置对象中加载配置
app.config.from_object(Config)

# 从配置文件中加载配置
app.config.from_pyfile('config.ini')

# 加载指定环境变量名称所对应的相关配置
app.config.from_envvar('FLASKCONFIG')

配置文件

  • 创建配置文件 config.ini,在配置文件中添加配置

创建配置文件 1599201031159.png

环境变量(了解)

编辑运行配置.png

添加运行环境变量.png

app.run 的参数

  • 可以指定运行的主机 IP 地址,端口,是否开启调试模式
app.run(host="0.0.0.0", port=5000, debug = True)

路由设置

指定路由地址

# 指定访问视图函数index,访问路径为/index
@app.route('/index')
def demo1():
    return 'index'

路由传参

有时我们需要将同一类 URL 映射到同一个视图函数处理,比如:使用同一个视图函数来显示不同用户的个人信息。

# 路由传递参数,整数
@app.route('/user/<int:user_id>')
def user_info(user_id):
    return 'the num is %d' % user_id
# 路由传递参数,字符串,不指定path默认就是字符串
@app.route('/user/<path:user_id>')
def user_info(user_id):
    return 'hello %s' % user_id

提示:之所以 int,path 可以接收整数,字符串,是由于 werkzeug 提供了 IntegerConverter,PathConverter 对应转换器.

指定请求方式

在 Flask 中,定义一个路由,默认的请求方式为:

  • GET
  • OPTIONS(自带)
  • HEAD(自带)

如果想添加请求方试,那么可以使用 methods 指定,比如:

@app.route('/demo', methods=['GET', 'POST'])
def demo():
    # 直接从请求中取到请求方式并返回
    return request.method

响应

  • 使用 jsonify,生成 json 数据响应体
  • 使用 redirect,url_for,生成文本响应体
  • 直接响应,字符串,自定义状态码,返回文本响应体

生成 json 数据响应体

# 生成json 数据响应体
@app.route('/demo4')
def demo4():
    json_dict = {
        "user_id": 10,
        "user_name": "laowang"
    }
    return jsonify(json_dict)

使用 redirect,url_for,生成文本响应体

  • redirect 重定向到 黑马 官网
    格式: redirect('地址');
    地址: 可以是外链地址, 可以是视图函数地址
# 重定向
@app.route('/demo5')
def demo5():
    return redirect('http://www.itheima.com')
  • url_for 反解析:通过视图函数的名称,返回地址
    格式: url_for('视图函数名',key=value)
    提示: url_for 经常配合 redirect 使用,可以传递参数 ```python @app.route('/demo1') def demo1(): return 'demo1'

重定向

@app.route('/demo5') def demo5(): return redirect(url_for('demo1'))
- 重定向到视图函数,携带参数演示
```python
# 路由传递参数
@app.route('/user/<int:user_id>')
def user_info(user_id):
    return 'hello %d' % user_id

# 重定向
@app.route('/demo5')
def demo5():
    # 使用 url_for 生成指定视图函数所对应的 url
    return redirect(url_for('user_info', user_id=100))

返回字符串,自定义状态码,返回文本响应体

  • 在 Flask 中,可以很方便的返回自定义状态码,以实现不符合 http 协议的状态码,例如:status code: 666
@app.route('/demo6')
def demo6():
  return '状态码为666', 666

转换器

在 web 开发中,可能会出现限制用户访问规则的场景,那么这个时候就需要过滤指定用户, 所以可以使用转换器实现
转换器的本质:通过正则表达式,匹配路由地址

系统自带转换器

DEFAULT_CONVERTERS = {
    'default':          UnicodeConverter,
    'string':           UnicodeConverter,
    'any':              AnyConverter,
    'path':             PathConverter,
    'int':              IntegerConverter,
    'float':            FloatConverter,
    'uuid':             UUIDConverter,
}

系统自带的转换器具体使用方式在每种转换器的注释代码中有写,请留意每种转换器初始化的参数。

自定义转换器

自定义转换器,具体实现步骤为:

  • 导入转换器基类(BaseConverter):Flask 中所有的路由的匹配规则,都是使用转换器实现
  • 自定义转换器:自定义类继承于转换器基类
  • 添加转换器到默认的转换器字典中
  • 使用自定义转换器实现自定义匹配规则

代码实现

from flask import Flask
#导入基类转换器
from werkzeug.routing import BaseConverter

app = Flask(__name__)

# 1.自定义类,继承自BaseConverter
class MyRegexConverter(BaseConverter):

    # 2.编写初始化方法, init方法, 接收两个参数, url_map, regex, 并初始化父类空间和子类空间
    def __init__(self,url_map,regex):
        super(MyRegexConverter, self).__init__(url_map)
        self.regex = regex

# 3.将自定义转换器类,添加到默认的转换列表中
app.url_map.converters['re'] = MyRegexConverter

#使用自定义转换器
#接收3位整数
@app.route('/<re("\d{3}"):num>')
def hello_world(num):

    print("num = %s"%num)

    return "the num is %s"%num

#接收一个手机号
@app.route('/<re("1[345678]\d{9}"):mobile>')
def get_phone_number(mobile):

    return "the mobile is %s"%mobile

if __name__ == '__main__':
    app.run()

异常处理

abort,异常抛出

  • abort(code):主动抛出异常状态码
  • 参数 code:HTTP 的错误状态码
  • 例如:abort(404)

errorhandler,异常捕获

  • errorhandler(code_or_exception):用来监听捕捉异常,然后返回自定义的页面处理
  • 参数:code_or_exception – HTTP 的错误状态码或指定异常

代码演示:

from flask import Flask,abort

app = Flask(__name__)

@app.route('/game/<int:age>')
def play_game(age):
    #异常抛出
    abort(404)
    return "helloworld"

#异常捕获
@app.errorhandler(404)
def page_not_found(e):
    print(e)
    return "找不到服务器资源,服务器搬家了"

if __name__ == '__main__':
    app.run()

请求勾子

在客户端和服务器交互的过程中,有些准备工作或扫尾工作需要处理,比如:

  • 在请求开始时,建立数据库连接;
  • 在请求开始时,根据需求进行权限校验;
  • 在请求结束时,指定数据的交互格式;

为了让每个视图函数避免编写重复功能的代码,Flask 提供了通用设施的功能,即请求钩子

请求钩子是通过装饰器的形式实现,Flask 支持如下四种请求钩子:

  • before_first_request:在处理第一个请求前执行
  • before_request:在每次请求前执行,在该装饰函数中,一旦 return,视图函数不再执行
  • after_request:如果没有抛出错误,在每次请求后执行
    • 接受一个参数:视图函数作出的响应
    • 在此函数中可以对响应值,在返回之前做最后一步处理,再返回
  • teardown_request:在每次请求后执行
    • 接受一个参数:用来接收错误信息

代码测试

from flask import Flask
from flask import abort

app = Flask(__name__)

# 在第一次请求之前调用,可以在此方法内部做一些初始化操作
@app.before_first_request
def before_first_request():
    print("before_first_request")


# 在每次请求之前调用,这时候已经有请求了,可能在这个方法里面做请求的校验
# 如果请求的校验不成功,可以直接在此方法中进行响应,直接return之后那么就不会执行视图函数
@app.before_request
def before_request():
    print("before_request")
    # if 请求不符合条件:
    #     return "laowang"

# 在执行完视图函数之后会调用,并且会把视图函数所生成的响应传入,可以在此方法中对响应做最后一步统一的处理
@app.after_request
def after_request(response):
    print("after_request")
    response.headers["Content-Type"] = "application/json"
    return response

# 请每一次请求之后都会调用,会接受一个参数,参数是服务器出现的错误信息
@app.teardown_request
def teardown_request(e):
    print("teardown_request")

@app.route('/')
def index():
    return 'index'

if __name__ == '__main__':
    app.run(debug=True)
  • 在第 1 次请求时的打印:
before_first_request
before_request
after_request
teardown_request
  • 在第 2,3,..n 次请求时的打印:
before_request
after_request
teardown_request

request

request 就是 flask 中代表当前请求的 request 对象,其中一个请求上下文变量(理解成全局变量,在视图函数中直接使用可以取到当前本次请求)

常用的属性如下:

属性 说明 类型
data 记录请求的数据,并转换为字符串 *
form 记录请求中的表单数据 MultiDict
args 记录请求中的查询参数 MultiDict
cookies 记录请求中的 cookie 信息 Dict
headers 记录请求中的报文头 EnvironHeaders
method 记录请求使用的 HTTP 方法 GET/POST
url 记录请求的 URL 地址 string
files 记录请求上传的文件 *

Web 表单

WTForms 支持的 HTML 标准字段

字段对象 说明
StringField 文本字段
TextAreaField 多行文本字段
PasswordField 密码文本字段
HiddenField 隐藏文件字段
DateField 文本字段,值为 datetime.date 文本格式
DateTimeField 文本字段,值为 datetime.datetime 文本格式
IntegerField 文本字段,值为整数
DecimalField 文本字段,值为 decimal.Decimal
FloatField 文本字段,值为浮点数
BooleanField 复选框,值为 True 和 False
RadioField 一组单选框
SelectField 下拉列表
SelectMutipleField 下拉列表,可选择多个值
FileField 文件上传字段
SubmitField 表单提交按钮
FormField 把表单作为字段嵌入另一个表单
FieldList 一组指定类型的字段

WTForms 常用验证函数

验证函数 说明
DataRequired 确保字段中有数据
EqualTo 比较两个字段的值,常用于比较两次密码输入
Length 验证输入的字符串长度
NumberRange 验证输入的值在数字范围内
URL 验证 URL
AnyOf 验证输入值在可选列表中
NoneOf 验证输入值不在可选列表中

传统 Demo

login.html

<form method="post" action="/login">
    <label>用 户 名:</label><input type="text" name="username"><br>
    <label>密 码:</label><input type="password" name="password"><br>
    <label>确认密码:</label><input type="password" name="password2"><br>
    <input type="submit" value="提交"><br>
</form>

app.py

'''
 实现简单的登录处理
 1. 路由需要有get和post两种请求方式 --->判断请求方式
 2. 获取请求的参数
 3. 判断参数是否填写 & 密码是否相同
 4. 如果判断都没有问题,返回登录成功
'''


@app.route("/login", methods=['GET', 'POST'])
def login_deal():
	# request :请求对象 --获取请求方式,数据
	# 1. 获取请求方式
	if request.method == 'POST':
		# 2. 获取请求数据
		username = request.form.get('username')
		password = request.form.get('password')
		password2 = request.form.get('password2')
		# print(username)
		# 3。 判断参数是否完整
		if not all([username, password, password2]):
			return '参数不完整'
		# 4. 判断密码是否相同
		elif password == password2:
			return '登录成功'
		else:
			return '密码不一致'

	return render_template('login.html')

WTF DEMO

app.py

from flask import Flask, render_template, request, flash
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import DataRequired, EqualTo

# 自定义表单类
class login_form(FlaskForm):
	username = StringField('用户名', validators=[DataRequired()])
	password = PasswordField('密码', validators=[DataRequired()])
	password2 = PasswordField('确认密码', validators=[DataRequired(), EqualTo('password', message='密码错误')])
	submit = SubmitField('提交')


@app.route('/form', methods=['GET', 'POST'])
def form():
	loginForm = login_form()

	# 判断请求参数
	if request.method == 'POST':
		# 获取参数
		if loginForm.validate_on_submit():
			return 'SUCCESS'
		else:
			flash('参数有误')
	# 验证数据
	return render_template('form.html', form=loginForm)

form.html

<form method="post" action="/form">
    {{ form.csrf_token() }}
    {{ form.username.label }}{{ form.username }} <br>
    {{ form.password.label }}{{ form.password }} <br>
    {{ form.password2.label }}{{ form.password2 }} <br>
    {{ form.submit.label }}{{ form.submit }} <br>
    {% for message in  get_flashed_messages() %}
        {{ message }}
    {% endfor %}
</form>

进阶

状态保持

状态相关概念

  • http 是一种无状态协议,浏览器请求服务器是无状态的。
  • 无状态:指一次用户请求时,浏览器、服务器不知道之前这个用户做过什么,每次请求都是一次新的请求。
  • 无状态原因:浏览器与服务器是使用 socket 套接字进行通信的,服务器将请求结果返回给浏览器之后,会关闭当前的 socket 连接,而且服务器也会在处理页面完毕之后销毁页面对象。
  • 有时需要保持下来用户浏览的状态,比如用户是否登录过,浏览过哪些商品等
  • 实现状态保持主要有两种方式:
    • 在客户端存储信息使用 Cookie
    • 在服务器端存储信息使用 Session

什么是 cookie

* 指某些网站为了辨别用户身份、进行会话跟踪而储存在用户本地的数据(通常经过加密)。
* 复数形式Cookies。
* Cookie最早是网景公司的前雇员Lou Montulli在1993年3月的发明。
* Cookie是由服务器端生成,发送给客户端浏览器,浏览器会将Cookie的key/value保存,下次请求同一网站时就发送该Cookie给服务器.
* Cookie中的key/value可以由服务器端自己定义。

cookie 使用场景

  • 应用
    • 网站的广告推送,经常遇到访问某个网站时,会弹出小窗口,展示我们曾经在购物网站上看过的商品信息。
    • 购物车,用户可能会在一段时间内在同一家网站的不同页面中选择不同的商品,这些信息都会写入 Cookie,以便在最后付款时提取信息。
  • 提示
    • Cookie 是存储在浏览器中的一段纯文本信息,建议不要存储敏感信息如密码,因为电脑上的浏览器可能被其它人使用
    • Cookie 基于域名安全,不同域名的 Cookie 是不能互相访问的
      • 如访问 itcast.cn 时向浏览器中写了 Cookie 信息,使用同一浏览器访问 baidu.com 时,无法访问到 itcast.cn 写的 Cookie 信息
      • 浏览器的同源策略
    • 当浏览器请求某网站时,会将本网站下所有 Cookie 信息提交给服务器,所以在 request 中可以读取 Cookie 信息

三.设置,获取 cookie

代码展示

from flask import Flask, make_response, request

app = Flask(__name__)

#设置cookie值
@app.route('/set_cookie')
def set_cookie():

    response = make_response("set cookie")
    response.set_cookie("name","zhangsan")
    response.set_cookie("age","13",10) #10秒有效期

    return response

#获取cookie
@app.route('/get_cookie')
def get_cookie():

    #获取cookie,可以根据cookie的内容来推荐商品信息
    # name = request.cookies['haha']
    name = request.cookies.get('name')
    age = request.cookies.get('age')

    return "获取cookie,name is %s, age is %s"%(name,age)

if __name__ == '__main__':
    app.run(debug=True)

Session

一.session 作用

  • 对于敏感、重要的信息,建议要存储在服务器端,不能存储在浏览器中,如用户名、余额、等级、验证码等信息,所以可以使用 session 进行保存
  • 在服务器端进行状态保持的方案就是 Session

二.session 设置,获取

代码展示

from flask import Flask,session

app = Flask(__name__)

#设置SECRET_KEY
app.config["SECRET_KEY"] = "fhdk^fk#djefkj&*&*&"

#设置session
@app.route('/set_session/<path:name>')
def set_session(name):

    session["name"] = name
    session["age"] = "13"

    return "set session"

#获取session内容
@app.route('/get_session')
def get_session():

    name = session.get('name')
    age = session.get('age')

    return "name is %s, age is %s"%(name,age)

if __name__ == '__main__':
    app.run(debug=True)

提示:

session 的存储依赖于 cookie,在 cookie 保存的 session 编号
session 编号生成,需要进行加密,所以需要设置 secret_key secret_key 的作用参考:https://segmentfault.com/q/1010000007295395

上下文

上下文:相当于一个容器,保存了 Flask 程序运行过程中的一些信息。

Flask 中有两种上下文,请求上下文和应用上下文

一.请求上下文(request context)

思考:在视图函数中,如何取到当前请求的相关数据?比如:请求地址,请求方式,cookie 等等

在 flask 中,可以直接在视图函数中使用 request 这个对象进行获取相关数据,而 request 就是请求上下文的对象,保存了当前本次请求的相关数据,请求上下文对象有:request、session

  • request
    • 封装了 HTTP 请求的内容,针对的是 http 请求。举例:user = request.args.get('user'),获取的是 get 请求的参数。
  • session
    • 用来记录请求会话中的信息,针对的是用户信息。举例:session['name'] = user.id,可以记录用户信息。还可以通过 session.get('name')获取用户信息。

二.应用上下文(application context)

它的字面意思是 应用上下文,但它不是一直存在的,它只是 request context 中的一个对 app 的代理(人),所谓 local proxy。它的作用主要是帮助 request 获取当前的应用,它是伴 request 而生,随 request 而灭的。

应用上下文对象有:current_app,g

current_app

应用程序上下文,用于存储应用程序中的变量,可以通过 current_app.name 打印当前 app 的名称,也可以在 current_app 中存储一些变量,例如:

  • 应用的启动脚本是哪个文件,启动时指定了哪些参数
  • 加载了哪些配置文件,导入了哪些配置
  • 连了哪个数据库
  • 有哪些 public 的工具类、常量
  • 应用跑再哪个机器上,IP 多少,内存多大
current_app.name
current_app.test_value='value'

g 变量

g 作为 flask 程序全局的一个临时变量,充当者中间媒介的作用,我们可以通过它传递一些数据,g 保存的是当前请求的全局变量,不同的请求会有不同的全局变量,通过不同的 thread id 区别

g.name='abc'

注意:不同的请求,会有不同的全局变量

两者区别

  • 请求上下文:保存了客户端和服务器交互的数据
  • 应用上下文:flask 应用程序运行过程中,保存的一些配置信息,比如程序名、数据库连接、应用信息等

上下文中的对象只能在指定上下文中使用,超出范围不能使用 请求上下文和应用上下文原理实现:https://segmentfault.com/a/1190000004223296

Flask-Script 扩展

一.flask_script 作用

属于 flask 的扩展包,通过使用 Flask-Script 扩展,我们可以在 Flask 服务器启动的时候,通过命令行的方式传入参数。而不仅仅通过 app.run()方法中传参,比如我们可以通过:

python hello.py runserver -host ip地址

提示:通过 python hello.py runserver --help 可以查看,程序运行需要什么参数。

terminator.png

二.代码实现

  • 安装 Flask-Script 扩展
  • pip install flask-script
from flask import Flask
#1.从flask_script中导入Manager类
from flask_script import Manager

app = Flask(__name__)

# 2.使用Manager管理app对象
manager = Manager(app)

@app.route('/')
def hello_world():
    return "helloworld"

if __name__ == '__main__':
    manager.run()

Jinja2 模板引擎

一.Jinja2 模板概述

用来展示数据的 html 页面,这个过程也通常称为渲染,属于 Jinja2 的功能 使用模板的好处:

  • 视图函数只负责业务逻辑和数据处理(业务逻辑方面)
  • 而模板则取到视图函数的数据结果进行展示(视图展示方面)
  • 代码结构清晰,耦合度低

二.Jinja2 特点

  • Jinja2:是 Python 下一个被广泛应用的模板引擎,是由 Python 实现的模板语言,他的设计思想来源于 Django 的模板引擎,并扩展了其语法和一系列强大的功能,其是 Flask 内置的模板语言。
  • 模板语言:是一种被设计来自动生成文档的简单文本格式,在模板语言中,一般都会把一些变量传给模板,替换模板的特定位置上预先定义好的占位变量名。
  • 使用 render_template 函数封装模板引擎

模板的使用

一.Jinja2 模板语法

  • 获取变量值:
    <h1>整数:{ {number} }</h1>
    <h1>元祖:{ {tuple[0]} }</h1>
    <h1>列表:{ { list[0] } }</h1>
    <h1>字典:{ { dict['key'] } }</h1>
  • 分支语句 if
{ % if 条件 % }
    语句1
 { % else % }  
    语句2
{ % endif % }
  • for 循环
{% for 变量  in 容器 %}
语句
{% endfor%}
  • 注释
{# 注释内容 #}

二.代码展示

  • 使用函数: render_template('模板文件名',key=value)
  • 将数据携带到,文件中进行展示
  • 创建文件 demo01.py,代码如下:
from flask import Flask,render_template
app = Flask(__name__) #默认省略了三个参数,static_url_path, static_folder, template_folders

@app.route('/')
def hello_world():
    #定义数据,整数,字符串,元祖,列表,字典
    num = 10
    str = "hello"
    tuple = (1,2,3,4)
    list = [5,6,7,8]
    dict = {
        "name":"张三",
        "age":13
    }

    return render_template('file01.html',my_num=num,my_str=str,my_tuple=tuple,my_list=list,my_dict=dict)

if __name__ == '__main__':
    app.run(debug=True)
  • templates 文件夹下,创建文件 file01.html 文件,代码如下:
<h2>1.获取各种变量的值</h2>
    <h3>整数: {{ my_num + 20}}</h3>
    <h3>字符串: {{ my_str + " python" }}</h3>
    <h3>元组: {{ my_tuple }}, 分开获取:{{ my_tuple[0] }}, {{ my_tuple[1] }}</h3>
    <h3>列表: {{ my_list }}, 分开获取:{{ my_list[0] }}, {{ my_list[1] }}</h3>
    <h3>字典: {{ my_dict }},分开获取:{{ my_dict.name }}, {{ my_dict[age] }}</h3>
    <h2>2.遍历元祖中所有的元素</h2>
    {% for item in my_tuple %}
        <li>{{ item }}</li>
    {% endfor %}

    <h2>3.取出列表中所有偶数</h2>
    {% for item in my_list %}
        {% if item %2 == 0 %}
            {{ item }}
        {% endif %}
    {% endfor %}

    <h2>4.遍历字典内容</h2>
    {% for key in my_dict %}
        {# 如果直接是mydict.key ,那么这个key是一个字符串, 如果是 mydict[key], 那么key当成变量 #}
        <li>{{ key }} = {{ my_dict[key] }}</li>
    {% endfor %}

扩展:

在一个 for 循环块中你可以访问这些特殊的变量:

变量 描述
loop.index 当前循环迭代的次数(从 1 开始)
loop.index0 当前循环迭代的次数(从 0 开始)
loop.revindex 到循环结束需要迭代的次数(从 1 开始)
loop.revindex0 到循环结束需要迭代的次数(从 0 开始)
loop.first 如果是第一次迭代,为 True 。
loop.last 如果是最后一次迭代,为 True 。
loop.length 序列中的项目数。
loop.cycle 在一串序列间期取值的辅助函数。见下面示例程序。

Jinja2 自带过滤器

一.过滤器概述

过滤器的本质就是函数。有时候我们不仅仅只是需要输出变量的值,我们还需要修改变量的显示,甚至格式化、运算等等,而在模板中是不能直接调用 Python 中的某些方法,那么这就用到了过滤器。

二.两种过滤器

字符串

* 使用格式:{{ 字符串 | 字符串过滤器 }}
  • safe:禁用转义
<p>{{ '<em>hello</em>' | safe }}</p>
  • capitalize:把变量值的首字母转成大写,其余字母转小写
<p>{{ 'hello' | capitalize }}</p>
  • lower:把值转成小写
<p>{{ 'HELLO' | lower }}</p>
  • upper:把值转成大写
<p>{{ 'hello' | upper }}</p>
  • title:把值中的每个单词的首字母都转成大写
<p>{{ 'hello' | title }}</p>
  • reverse:字符串反转
<p>{{ 'olleh' | reverse }}</p>
  • format:格式化输出
<p>{{ '%s is %d' | format('name',17) }}</p>
  • striptags:渲染之前把值中所有的 HTML 标签都删掉
<p>{{ '<em>hello</em>' | striptags }}</p>

列表

* 使用格式:{{ 列表 | 列表过滤器 }}
  • first:取第一个元素
<p>{{ [1,2,3,4,5,6] | first }}</p>
  • last:取最后一个元素
<p>{{ [1,2,3,4,5,6] | last }}</p>
  • length:获取列表长度
<p>{{ [1,2,3,4,5,6] | length }}</p>
  • sum:列表求和
<p>{{ [1,2,3,4,5,6] | sum }}</p>
  • sort:列表排序
<p>{{ [6,2,3,1,5,4] | sort }}</p>

其他操作语句

语句块操作

{% filter upper %}
    #一大堆文字#
{% endfilter %}

链式调用


{{ "hello world" | reverse | upper }}

自定义过滤器

一.为什么要自定义过滤器

过滤器的本质是函数。当模板内置的过滤器不能满足需求,可以自定义过滤器。

二.自定义过滤器两种方式

方式一

  • 先定义函数
  • 后添加到过滤器列表,app.add_template_filter('函数名','过滤器名称')
def do_listreverse(li):
    # 通过原列表创建一个新列表
    temp_li = list(li)
    # 将新列表进行返转
    temp_li.reverse()
    return temp_li

app.add_template_filter(do_listreverse,'lireverse')

方式二

  • 定义函数,直接使用 @app.template_filter('过滤器名称')装饰
    @app.template_filter('lireverse')
    def do_listreverse(li):
      # 通过原列表创建一个新列表
      temp_li = list(li)
      # 将新列表进行返转
      temp_li.reverse()
      return temp_li
    

三.在 html 代码中使用过滤器

  • 在 html 中使用该自定义过滤器
<h2>my_array 原内容:{{ my_array }}</h2>
<h2> my_array 反转:{{ my_array | lireverse }}</h2>

模板使用练习

  • 实现的效果如下:

控制语句效果.png

  • 给定如下 5 条数据,只显示 4 行数据,背景颜色依次为:黄,绿,红,紫
my_list = [
    {
        "id": 1,
        "value": "我爱工作"
    },
    {
        "id": 2,
        "value": "工作使人快乐"
    },
    {
        "id": 3,
        "value": "沉迷于工作无法自拔"
    },
    {
        "id": 4,
        "value": "日渐消瘦"
    },
    {
        "id": 5,
        "value": "以梦为马,越骑越傻"
    }
]
  • 模板代码
{% for item in my_list if item.id != 5 %}
    {% if loop.index == 1 %}
        <li style="background-color: orange">{{ item.value }}</li>
    {% elif loop.index == 2 %}
        <li style="background-color: green">{{ item.value }}</li>
    {% elif loop.index == 3 %}
        <li style="background-color: red">{{ item.value }}</li>
    {% else %}
        <li style="background-color: purple">{{ item.value }}</li>
    {% endif %}
{% endfor %}
  • python 代码
from flask import Flask,render_template

app = Flask(__name__)

@app.route('/')
def hello_world():
    my_list = [
        {
            "id": 1,
            "value": "我爱工作"
        },
        {
            "id": 2,
            "value": "工作使人快乐"
        },
        {
            "id": 3,
            "value": "沉迷于工作无法自拔"
        },
        {
            "id": 4,
            "value": "日渐消瘦"
        },
        {
            "id": 5,
            "value": "以梦为马,越骑越傻"
        }
    ]
    return render_template("practice.html",my_list=my_list)

if __name__ == '__main__':
    app.run(debug=True)

模板代码复用

在模板中,可能会遇到以下情况:

  • 多个模板具有完全相同的顶部和底部内容
  • 多个模板中具有相同的模板代码内容,但是内容中部分值不一样
  • 多个模板中具有完全相同的 html 代码块内容

像遇到这种情况,可以使用 JinJa2 模板中的 宏、继承、包含来进行实现

一.什么是宏

宏是 Jinja2 中的函数,调用后,直接返回一个模板,或者字符串。当模板中出现大量重复功能代码的时候,可以使用宏来进行封装

二.定义宏,使用宏

  • 定义宏,可以在当前文件,也可以是其他文件
{% macro input(name,value='',type='text') %}
    <input type="{{type}}" name="{{name}}" value="{{value}}">
{% endmacro %}
  • 使用当前文件宏
{{ input('name' value='zs')}}
  • 使用其他文件宏
    {%import 'filename.html' as 别名%}
    {%别名.函数名(参数)%}

三.代码展示

  • 使用当前文件宏,其他文件宏
  • 当前文件 test.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

    {# 定义宏 #}
    {% macro input(name,password) %}
        <label>{{ name }}:</label><input type="text" name="username"><br>
        <label>{{ password }}:</label><input type="password" name="username"><br>
    {% endmacro %}

    {# 使用当前文件宏 #}
    {{ input("账号","密码") }}


    {# 使用其他文件宏 #}
    {% import 'other_macro.html' as other_macro %}
    {{ other_macro.input2('用户名',"密码") }}
    {{ other_macro.input3() }}

</body>
</html>
  • 其他文件 other_macro.html
{% macro input(name,password) %}
    <label>{{ name }}:</label><input type="text" name="username"><br>
    <label>{{ password }}:</label><input type="password" name="username"><br>
{% endmacro %}

模板继承,包含

一.什么是继承

将公共的内容抽取到父类模板,共子类使用的形式称为继承.
一般 Web 开发中,继承主要使用在网站的顶部菜单、底部。这些内容可以定义在父模板中,子模板直接继承,而不需要重复书写。

二.继承的格式

  • 父模板中使用多个 block 组成,格式: ```python
- 子模板使用
- 子模板使用格式: 
- 继承后,子类完全拥有父类内容,并且子类可以进行重写,如果写保留父类内容使用: super()


### 三.代码展示
#### 父模板
- base.html

```python
{% block top %}
  顶部菜单
{% endblock top %}

{% block content %}
  正文内容
{% endblock content %}

{% block bottom %}
  底部
{% endblock bottom %}

子模板

{% extends 'base.html' %}
{% block content %}
 需要填充的内容
{% endblock content %}
  • 模板继承使用时注意点:
    • 不支持多继承
    • 为了便于阅读,在子模板中使用 extends 时,尽量写在模板的第一行。
    • 不能在一个模板文件中定义多个相同名字的 block 标签。
    • 定义 block 模板的时候,一定要加上 endblock 结束标记

包含

一.什么是包含

Jinja2 模板中,除了宏和继承,还支持一种代码重用的功能,叫包含(Include)。它的功能是将另一个模板整个加载到当前模板中,并直接渲染。

二.包含的使用

格式:

{% include 'hello.html' %}
或者
{% include 'hello.html' ignore missing %}
  • 提示: ignore missing 加上后如果文件不存在,不会报错

三.代码展示

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    {% include '其他模板文件' ignore missing %}
</body>
</html>

小结

  • 宏(Macro)、继承(Block)、包含(include)均能实现代码的复用。
  • 继承(Block)的本质是代码替换,一般用来实现多个页面中重复不变的区域。
  • 宏(Macro)的功能类似函数,可以传入参数,需要定义、调用。
  • 包含(include)是直接将目标模板文件整个渲染出来。

模板中特有的变量和函数

你可以在自己的模板中访问一些 Flask 默认内置的函数和对象

config

你可以从模板中直接访问 Flask 当前的 config 对象:

{{config.DEBUG}}
输出:True

request

就是 flask 中代表当前请求的 request 对象:

{{request.url}}
输出:http://127.0.0.1

g 变量

在视图函数中设置 g 变量的 name 属性的值,然后在模板中直接可以取出

{{ g.name }}

url_for()

url_for 会根据传入的路由器函数名,返回该路由对应的 URL,在模板中始终使用 url_for()就可以安全的修改路由绑定的 URL,则不比担心模板中渲染出错的链接:

{{url_for('home')}}
/

如果我们定义的路由 URL 是带有参数的,则可以把它们作为关键字参数传入 url_for(),Flask 会把他们填充进最终生成的 URL 中:

{{ url_for('post', post_id=1)}}
/post/1
get_flashed_messages()

这个函数会返回之前在 flask 中通过 flask()传入的消息的列表,flash 函数的作用很简单,可以把由 Python 字符串表示的消息加入一个消息队列中,再使用 get_flashed_message()函数取出它们并消费掉:

{%for message in get_flashed_messages()%}
    {{message}}
{%endfor%}

CSRF(理解)

一. 什么是 CSRFToken?

  • CSRF 全拼为 Cross Site Request Forgery,译为跨站请求伪造。

  • CSRF

    指攻击者盗用了你的身份,以你的名义发送恶意请求。

    • 包括:以你名义发送邮件,发消息,盗取你的账号,甚至于购买商品,虚拟货币转账......
  • 造成的问题:个人隐私泄露以及财产安全。

二.CSRF 攻击示意图

  • 客户端访问服务器时没有同服务器做安全验证

三.防止 CSRF 攻击

  1. 在客户端向后端请求界面数据的时候,后端会往响应中的 cookie 中设置 csrf_token 的值
  2. 在 Form 表单中添加一个隐藏的的字段,值也是 csrf_token
  3. 在用户点击提交的时候,会带上这两个值向后台发起请求
  4. 后端接受到请求,以会以下几件事件:
    • 从 cookie 中取出 csrf_token
    • 从 表单数据中取出来隐藏的 csrf_token 的值
    • 进行对比
  5. 如果比较之后两值一样,那么代表是正常的请求,如果没取到或者比较不一样,代表不是正常的请求,不执行下一步操作
  6. 提示:代码展示:见 << webA >>, << webB >> 文件

四.csrf 校验机制作用域代码

  • flask_wtf 模块提供了 csrf 攻击的保护
  • 使用流程:
    • from flask_wtf.csrf import CSRFProtect
    • CSRFProtect(app)
  • CSRFProtect(app)保护原理:
    • 对应用程序 app 中的 post,put,dispatch,delete, 4 种类型的请求做保护,因为这些类型的请求是用于更改服务器的资源
    • 当以上面 4 种类型的请求,操作服务器资源的时候,会校验 cookie 中的 csrf_token, 表单中的 csrf_token 信息
    • 只有上面二者的值相等的时候,那么校验则通过,可以操作服务器资源

提示: csrf_token 值的生成需要加密, 所以设置 SECRET_KEY

  • 代码展示
  • 后端代码:
from flask import Flask,render_template
from flask_wtf import CSRFProtect

app = Flask(__name__)

#设置SECRET_KEY
app.config["SECRET_KEY"] = "fjkdjfkdfjdk"

#保护应用程序
CSRFProtect(app)

@app.route('/')
def show_page():

    return render_template('file01csrf.html')

@app.route('/add_data',methods=["POST"])
def add_data():

    return "登陆成功"

if __name__ == '__main__':
    app.run(debug=True)
  • 前端代码,file01csrf.html 文件
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form action="/add_data" method="post">
    {#设置隐藏的csrf_token,使用了CSRFProtect保护app之后,即可使用csrf_token()方法#}
    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">

    <label>用户名:</label> <input type="text" name="username"><br>
    <label>密码:</label> <input type="text" name="username"><br>
    <input type="submit" value="登陆">
</form>
</body>
</html>

数据库

ORM

一.什么是 ORM

  • ORM 全拼 Object-Relation Mapping. 称为对象-关系映射
  • 主要实现模型对象到关系数据库数据的映射.
    • 比如:把数据库表中每条记录映射为一个模型对象
      PeopleInfoTable.png

ORM 图解

ORM1599202467512.png

二.ORM 的优缺点有哪些?

优点 :

  • 对数据库的操作都转化成对类,属性和方法的操作.
  • 不用编写各种数据库的 sql语句.
  • 不在关注,使用的是 mysqloracle...等数据库

缺点 :

  • 相比较直接使用 SQL 语句操作数据库,有性能损失.

Flask-SQLAlchemy 安装及设置

  • SQLALchemy 实际上是对数据库的抽象,让开发者不用直接和 SQL 语句打交道,而是通过 Python 对象来操作数据库,在舍弃一些性能开销的同时,换来的是开发效率的较大提升
  • SQLAlchemy 是一个关系型数据库框架,它提供了高层的 ORM 和底层的原生数据库的操作。flask-sqlalchemy 是一个简化了 SQLAlchemy 操作的 flask 扩展。
  • 文档地址:http://docs.jinkan.org/docs/flask-sqlalchemy

一,安装

  • 安装 flask-sqlalchemy
pip install flask-sqlalchemy
  • 如果连接的是 mysql 数据库,需要安装 mysqldb
pip install flask-mysqldb

提示:如果 flask-mysqldb 安装不上,安装, pip install pymysql

二,数据库连接设置

  • 设置数据库的链接地址,追踪信息
  • 格式:< 用户名 >:< 密码 >@:< 端口 >/数据库名称
# 数据库链接地址
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root:mysql@127.0.0.1:3306/test'
# 跟踪数据库修改 ---->不建议开启,未来版本中会删除
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

查看映射的 sql 语句,设置: app.config['SQLALCHEMY_ECHO'] = True

  • 配置完成需要去 MySQL 中创建项目所使用的数据库
$ mysql -uroot -pmysql
$ create database flask charset utf8;

三,其他配置信息

名字 备注
SQLALCHEMY_DATABASE_URI 用于连接的数据库 URI 。例如:sqlite:////tmp/test.password@server/db
SQLALCHEMY_BINDS 一个映射 binds 到连接 URI 的字典。更多 binds 的信息见用 Binds 操作多个数据库
SQLALCHEMY_ECHO 如果设置为 Ture, SQLAlchemy 会记录所有 发给 stderr 的语句,这对调试有用。(打印 sql 语句)
SQLALCHEMY_RECORD_QUERIES 可以用于显式地禁用或启用查询记录。查询记录 在调试或测试模式自动启用。更多信息见 get_debug_queries()。
SQLALCHEMY_NATIVE_UNICODE 可以用于显式禁用原生 unicode 支持。当使用 不合适的指定无编码的数据库默认值时,这对于 一些数据库适配器是必须的(比如 Ubuntu 上 某些版本的 PostgreSQL )。
SQLALCHEMY_POOL_SIZE 数据库连接池的大小。默认是引擎默认值(通常 是 5 )
SQLALCHEMY_POOL_TIMEOUT 设定连接池的连接超时时间。默认是 10 。
SQLALCHEMY_POOL_RECYCLE 多少秒后自动回收连接。这对 MySQL 是必要的, 它默认移除闲置多于 8 小时的连接。注意如果 使用了 MySQL , Flask-SQLALchemy 自动设定 这个值为 2 小时。

连接其他数据库

完整连接 URI 列表请跳转到 SQLAlchemy 下面的文档 (Supported Databases) 。这里给出一些 常见的连接字符串。

  • Postgres:
postgresql://scott:tiger@localhost/mydatabase
  • MySQL:
mysql://scott:tiger@localhost/mydatabase
  • Oracle:
- oracle://scott:tiger@127.0.0.1:1521/sidname
  • SQLite (注意开头的四个斜线):
sqlite:////absolute/path/to/foo.db

常用的 SQLAlchemy 字段类型

类型名 python 中类型 说明
Integer int 普通整数,一般是 32 位
SmallInteger int 取值范围小的整数,一般是 16 位
BigInteger int 或 long 不限制精度的整数
Float float 浮点数
Numeric decimal.Decimal 普通整数,一般是 32 位
String str 变长字符串
Text str 变长字符串,对较长或不限长度的字符串做了优化
Unicode unicode 变长 Unicode 字符串
UnicodeText unicode 变长 Unicode 字符串,对较长或不限长度的字符串做了优化
Boolean bool 布尔值
Date datetime.date 时间
Time datetime.datetime 日期和时间
LargeBinary str 二进制文件

常用的 SQLAlchemy 列选项

选项名 说明
primary_key 如果为 True,代表表的主键
unique 如果为 True,代表这列不允许出现重复的值
index 如果为 True,为这列创建索引,提高查询效率
nullable 如果为 True,允许有空值,如果为 False,不允许有空值
default 为这列定义默认值

常用的 SQLAlchemy 关系选项

选项名 说明
backref 在关系的另一模型中添加反向引用
primary join 明确指定两个模型之间使用的联结条件
uselist 如果为 False,不使用列表,而使用标量值
order_by 指定关系中记录的排序方式
secondary 指定多对多关系中关系表的名字
secondary join 在 SQLAlchemy 中无法自行决定时,指定多对多关系中的二级联结条件

数据库基本操作

  • 在 Flask-SQLAlchemy 中,插入、修改、删除操作,均由数据库会话管理。
    • 会话用 db.session 表示。在准备把数据写入数据库前,要先将数据添加到会话中然后调用 db.session.commit() 方法提交会话。
  • 在 Flask-SQLAlchemy 中,查询操作是通过 query 对象操作数据。
    • 最基本的查询是返回表中所有数据,可以通过过滤器进行更精确的数据库查询。

一,在视图函数中定义模型类

from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)

#设置连接数据库的URL
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root:mysql@127.0.0.1:3306/test'
#设置数据库追踪信息,压制警告
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
#创建SQLAlchemy对象,读取app中配置信息
db = SQLAlchemy(app)
#定义角色模型(一方)
class Role(db.Model):
    # 定义表名
    __tablename__ = 'roles'
    # 定义列对象
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(64), unique=True)
    #设置关系属性,方便查询使用
    us = db.relationship('User', backref='role')
    #重写__repr__方法,方便查看对象输出内容
    def __repr__(self):
        return 'Role:%s'% self.name

#定义用户模型类(多方)
class User(db.Model):
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(64), unique=True, index=True)
    email = db.Column(db.String(64),unique=True)
    password = db.Column(db.String(64))
    role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))

    def __repr__(self):
        return 'User:%s'%self.name
if __name__ == '__main__':
    #删除所有和db相关联的表
    db.drop_all()
    #创建所有和db相关联的表
    db.create_all()
    app.run(debug=True)

二,关键代码格式

  • 一对多关系
class Role(db.Model):
    ...
    #关键代码
    us = db.relationship('User', backref='role', lazy='dynamic')
    ...

class User(db.Model):
    ...
    role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
  • 其中 realtionship 描述了 Role 和 User 的关系。
  • 第一个参数为对应参照的类"User"
  • 第二个参数 backref 为类 User,反向引用属性
  • 第三个参数 lazy 决定了什么时候 SQLALchemy 从数据库中加载数据
    • 如果设置为子查询方式(subquery),则会在加载完 Role 对象后,就立即加载与其关联的对象,这样会让总查询数量减少,但如果返回的条目数量很多,就会比较慢
      • 设置为 subquery 的话,role.users 返回所有数据列表
    • 另外,也可以设置为动态方式(dynamic),这样关联对象会在被使用的时候再进行加载,并且在返回前进行过滤,如果返回的对象数很多,或者未来会变得很多,那最好采用这种方式

三,常见的操作语句

  • db.session.add(obj) 添加对象
  • db.session.add_all([obj1,obj2,..]) 添加多个对象
  • db.session.delete(obj) 删除对象
  • db.session.commit() 提交会话
  • db.session.rollback() 回滚
  • db.session.remove() 移除会话

四,常用的 SQLAlchemy 查询过滤器

  • 用来过滤数据,返回查询的结果集
过滤器 说明
filter() 把过滤器添加到原查询上,返回一个新查询
filter_by() 把等值过滤器添加到原查询上,返回一个新查询
limit 使用指定的值限定原查询返回的结果
offset() 偏移原查询返回的结果,返回一个新查询
order_by() 根据指定条件对原查询结果进行排序,返回一个新查询
group_by() 根据指定条件对原查询结果进行分组,返回一个新查询

五,常用的 SQLAlchemy 查询执行器

  • 用来执行结果集,得到具体数据
方法 说明
all() 以列表形式返回查询的所有结果
first() 返回查询的第一个结果,如果未查到,返回 None
first_or_404() 返回查询的第一个结果,如果未查到,返回 404
get() 返回指定主键对应的行,如不存在,返回 None
get_or_404() 返回指定主键对应的行,如不存在,返回 404
count() 返回查询结果的数量
paginate() 返回一个 Paginate 对象,它包含指定范围内的结果

六,练习数据库查询操作

  • 代码准备
from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)

#配置信息
app.config["SQLALCHEMY_DATABASE_URI"] = "mysql+pymysql://root:123456@127.0.0.1:3306/basic8"
#设置压制警告信息,如果True会追踪数据库变化,会增加显著开销,所以建议设置为False
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False

#创建SQLAlchemy类对象,关联app
db = SQLAlchemy(app)

#编写模型类,继承db.Model
#角色,用户之间的关系
class Role(db.Model):
    __tablename__ = "roles" #指定表名称
    #参数1:表示整数类型,  参数2:表示主键
    id = db.Column(db.Integer,primary_key=True)
    #角色名唯一的
    name = db.Column(db.String(64),unique=True)


    #需要设置关系属性relationship(不会产生字段),设置在一方
    #给Role添加了users关系属性, 查询格式: role.users
    #给User添加了role关系属性(反向引用),查询格式: user.role
    users = db.relationship('User',backref='role')

    #为了方便的看到对象输出的内容__repr__, 如果是普通类__str__
    def __repr__(self):
        return "<Role:%s>"%self.name

# 用户(多方)
class User(db.Model):
    __tablename__ = "users"  # 指定表名称
    #参数1:表示整数类型,  参数2:表示主键
    id = db.Column(db.Integer,primary_key=True)
    #用户名唯一的
    name = db.Column(db.String(64),unique=True)
    #邮箱密码
    email = db.Column(db.String(64),unique=True)
    password = db.Column(db.String(64))

    #外键
    role_id = db.Column(db.Integer,db.ForeignKey(Role.id))

    #为了方便的看到对象输出的内容__repr__, 如果是普通类__str__
    def __repr__(self):
        return "<User:%s,%s,%s,%s>"%(self.id,self.name,self.email,self.password)

if __name__ == '__main__':

    #为了演示方便,先删除数据库表,和模型类关联的表
    db.drop_all()

    #创建表,所有继承自dbModel的表
    db.create_all()

    #创建测试数据
    ro1 = Role(name='admin')
    db.session.add(ro1)
    db.session.commit()
    # 再次插入一条数据
    ro2 = Role(name='user')
    db.session.add(ro2)
    db.session.commit()

    #多条用户数据
    us1 = User(name='wang', email='wang@163.com', password='123456', role_id=ro1.id)
    us2 = User(name='zhang', email='zhang@189.com', password='201512', role_id=ro2.id)
    us3 = User(name='chen', email='chen@126.com', password='987654', role_id=ro2.id)
    us4 = User(name='zhou', email='zhou@163.com', password='456789', role_id=ro1.id)
    us5 = User(name='tang', email='tang@itheima.com', password='158104', role_id=ro2.id)
    us6 = User(name='wu', email='wu@gmail.com', password='5623514', role_id=ro2.id)
    us7 = User(name='qian', email='qian@gmail.com', password='1543567', role_id=ro1.id)
    us8 = User(name='liu', email='liu@itheima.com', password='867322', role_id=ro1.id)
    us9 = User(name='li', email='li@163.com', password='4526342', role_id=ro2.id)
    us10 = User(name='sun', email='sun@163.com', password='235523', role_id=ro2.id)
    db.session.add_all([us1, us2, us3, us4, us5, us6, us7, us8, us9, us10])
    db.session.commit()

    app.run(debug=True)

需求: 编写方法,查询以下内容

  • 查询所有用户数据
  • 查询有多少个用户
  • 查询第 1 个用户
  • 查询 id 为 4 的用户[3 种方式]
  • 查询名字结尾字符为 g 的所有数据[开始/包含]
  • 查询名字不等于 wang 的所有数据[2 种方式]
  • 查询名字和邮箱都以 li 开头的所有数据[2 种方式]
  • 查询 password 是 123456 或者 emailitheima.com 结尾的所有数据
  • 查询 id 为 [1, 3, 5, 7, 9] 的用户列表
  • 查询 name 为 liu 的角色数据
  • 查询所有用户数据,并以邮箱排序
  • 每页 3 个,查询第 2 页的数据

七,代码参考

查询:filter_by 精确查询

返回名字等于 wang 的所有人

User.query.filter_by(name='wang').all()

filterbyall.png

first()返回查询到的第一个对象

User.query.first()

all()返回查询到的所有对象

User.query.all()

queryall.png

filter 模糊查询,返回名字结尾字符为 g 的所有数据。

User.query.filter(User.name.endswith('g')).all()

likequery.png

get():参数为主键,如果主键不存在没有返回内容

User.query.get()

逻辑非,返回名字不等于 wang 的所有数据

User.query.filter(User.name!='wang').all()

logicnot.png

not_ 相当于取反

from sqlalchemy import not_
User.query.filter(not_(User.name=='chen')).all()

not.png

逻辑与,需要导入 and,返回 and()条件满足的所有数据

from sqlalchemy import and_
User.query.filter(and_(User.name!='wang',User.email.endswith('163.com'))).all()

logicand.png

逻辑或,需要导入 or_

from sqlalchemy import or_
User.query.filter(or_(User.name!='wang',User.email.endswith('163.com'))).all()

logicor.png

查询数据后删除

user = User.query.first()
db.session.delete(user)
db.session.commit()
User.query.all()

更新数据

user = User.query.first()
user.name = 'dong'
db.session.commit()
User.query.first()

updatedata.png

关联查询示例:

角色和用户的关系是一对多的关系,一个角色可以有多个用户,一个用户只能属于一个角色。

  • 查询角色的所有用户
#查询roles表id为1的角色
ro1 = Role.query.get(1)
#查询该角色的所有用户
ro1.us.all()

rolequeryall.png

  • 查询用户所属角色
#查询users表id为3的用户
us1 = User.query.get(3)
#查询用户属于什么角色
us1.role

userinrole.png

多对多演练

在项目开发过程中,会遇到很多数据之间多对多关系的情况,比如:

  • 学生网上选课(学生和课程)
  • 老师与其授课的班级(老师和班级)
  • 用户与其收藏的新闻(用户和新闻)
  • 等等...

所以在开发过程中需要使用 ORM 模型将表与表的多对多关联关系使用代码描述出来。多对多关系描述有一个唯一的点就是:需要添加一张单独的表去记录两张表之间的对应关系

场景示例

需求分析

  • 学生可以网上选课,学生有多个,课程也有多个
  • 学生有:张三、李四、王五
  • 课程有:物理、化学、生物
  • 选修关系有:
    • 张三选修了化学和生物
    • 李四选修了化学
    • 王五选修了物理、化学和生物
  • 需求:
    1. 查询某个学生选修了哪些课程
    2. 查询某个课程都有哪些学生选择

思路分析

  • 可以通过分析得出
    • 用一张表来保存所有的学生数据
    • 用一张表来保存所有的课程数据
  • 具体表及测试数据可以如下:
学生表(Student)
主键(id) 学生名(name)
1 张三
2 李四
3 王五
选修课表(Course)
主键(id) 课程名(name)
1 物理
2 化学
3 生物
数据关联关系表(Student_Course)
主键(student.id) 主键(course.id)
1 2
1 3
2 2
3 1
3 2
3 3

结果

  • 查询某个学生选修了哪些课程,例如:查询王五选修了哪些课程
    • 取出王五的 id 去 Student_Course 表中查询 student.id 值为 3 的所有数据
    • 查询出来有 3 条数据,然后将这 3 条数据里面的 course.id 取值并查询 Course 表即可获得结果
  • 查询某个课程都有哪些学生选择,例如:查询生物课程都有哪些学生选修
    • 取出生物课程的 id 去 Student_Course 表中查询 course.id 值为 3 的所有数据
    • 查询出来有 2 条数据,然后将这 2 条数据里面的 student.id 取值并查询 Student 表即可获得结果

代码演练

  • 定义模型及表
tb_student_course = db.Table('tb_student_course',
                             db.Column('student_id', db.Integer, db.ForeignKey('students.id')),
                             db.Column('course_id', db.Integer, db.ForeignKey('courses.id'))
                             )


class Student(db.Model):
    __tablename__ = "students"
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(64), unique=True)

    courses = db.relationship('Course', secondary=tb_student_course,
                              backref='student',
                              lazy='dynamic')


class Course(db.Model):
    __tablename__ = "courses"
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(64), unique=True)
  • 添加测试数据
if __name__ == '__main__':
    db.drop_all()
    db.create_all()

    # 添加测试数据

    stu1 = Student(name='张三')
    stu2 = Student(name='李四')
    stu3 = Student(name='王五')

    cou1 = Course(name='物理')
    cou2 = Course(name='化学')
    cou3 = Course(name='生物')

    stu1.courses = [cou2, cou3]
    stu2.courses = [cou2]
    stu3.courses = [cou1, cou2, cou3]

    db.session.add_all([stu1, stu2, stu2])
    db.session.add_all([cou1, cou2, cou3])

    db.session.commit()

    app.run(debug=True)

数据库迁移

  • 在开发过程中,需要修改数据库模型,而且还要在修改之后更新数据库。最直接的方式就是删除旧表,但这样会丢失数据。
  • 更好的解决办法是使用数据库迁移框架,它可以追踪数据库模式的变化,然后把变动应用到数据库中。
  • 在 Flask 中可以使用 Flask-Migrate 扩展,来实现数据迁移。并且集成到 Flask-Script 中,所有操作通过命令就能完成。
  • 为了导出数据库迁移命令,Flask-Migrate 提供了一个 MigrateCommand 类,可以附加到 flask-script 的 manager 对象上。

首先要在虚拟环境中安装 Flask-Migrate。

pip install flask-migrate
  • 代码文件内容:
#coding=utf-8
from flask import Flask

from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate,MigrateCommand
from flask_script import Shell,Manager

app = Flask(__name__)
manager = Manager(app)

app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root:mysql@127.0.0.1:3306/Flask_test'
app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN'] = True
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
db = SQLAlchemy(app)

#第一个参数是Flask的实例,第二个参数是Sqlalchemy数据库实例
migrate = Migrate(app,db) 

#manager是Flask-Script的实例,这条语句在flask-Script中添加一个db命令
manager.add_command('db',MigrateCommand)

#定义模型Role
class Role(db.Model):
    # 定义表名
    __tablename__ = 'roles'
    # 定义列对象
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(64), unique=True)
    user = db.relationship('User', backref='role')

    #repr()方法显示一个可读字符串,
    def __repr__(self):
        return 'Role:'.format(self.name)

#定义用户
class User(db.Model):
    __talbe__ = 'users'
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(64), unique=True, index=True)
    #设置外键
    role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))

    def __repr__(self):
        return 'User:'.format(self.username)


if __name__ == '__main__':
    manager.run()

创建迁移仓库

#这个命令会创建migrations文件夹,所有迁移文件都放在里面。
python database.py db init

dbinit.png

创建迁移脚本

  • 自动创建迁移脚本有两个函数
    • upgrade():函数把迁移中的改动应用到数据库中。
    • downgrade():函数则将改动删除。
  • 自动创建的迁移脚本会根据模型定义和数据库当前状态的差异,生成 upgrade()和 downgrade()函数的内容。
  • 对比不一定完全正确,有可能会遗漏一些细节,需要进行检查
python database.py db migrate -m 'initial migration'

migration.png

更新数据库

python database.py db upgrade

返回以前的版本

可以根据 history 命令找到版本号,然后传给 downgrade 命令:

python app.py db history

输出格式:<base> ->  版本号 (head), initial migration
  • 回滚到指定版本
python app.py db downgrade 版本号

实际操作顺序:

  • 1.python 文件 db init
  • 2.python 文件 db migrate -m"版本名(注释)"
  • 3.python 文件 db upgrade 然后观察表结构
  • 4.根据需求修改模型
  • 5.python 文件 db migrate -m"新版本名(注释)"
  • 6.python 文件 db upgrade 然后观察表结构
  • 7.若返回版本,则利用 python 文件 db history 查看版本号
  • 8.python 文件 db downgrade(upgrade) 版本号

常见关系模板代码

以下罗列了使用关系型数据库中常见关系定义模板代码

一对多

  • 示例场景:
    • 用户与其发布的帖子(用户表与帖子表)
    • 角色与所属于该角色的用户(角色表与多用户表)
  • 示例代码
class Role(db.Model):
    """角色表"""
    __tablename__ = 'roles'

    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(64), unique=True)
    users = db.relationship('User', backref='role', lazy='dynamic')

class User(db.Model):
    """用户表"""
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(64), unique=True, index=True)

多对多

  • 示例场景
    • 讲师与其上课的班级(讲师表与班级表)
    • 用户与其收藏的新闻(用户表与新闻表)
    • 学生与其选修的课程(学生表与选修课程表)
  • 示例代码
tb_student_course = db.Table('tb_student_course',
                             db.Column('student_id', db.Integer, db.ForeignKey('students.id')),
                             db.Column('course_id', db.Integer, db.ForeignKey('courses.id'))
                             )

class Student(db.Model):
    __tablename__ = "students"
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(64), unique=True)

    courses = db.relationship('Course', secondary=tb_student_course,
                              backref=db.backref('students', lazy='dynamic'),
                              lazy='dynamic')

class Course(db.Model):
    __tablename__ = "courses"
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(64), unique=True)

自关联一对多(了解)

  • 示例场景
    • 评论与该评论的子评论(评论表)
    • 参考网易新闻
  • 示例代码
class Comment(db.Model):
    """评论"""
    __tablename__ = "comments"

    id = db.Column(db.Integer, primary_key=True)
    # 评论内容
    content = db.Column(db.Text, nullable=False)
    # 父评论id
    parent_id = db.Column(db.Integer, db.ForeignKey("comments.id"))
    # 父评论(也是评论模型)
    parent = db.relationship("Comment", remote_side=[id],
                             backref=db.backref('childs', lazy='dynamic'))

# 测试代码
if __name__ == '__main__':
    db.drop_all()
    db.create_all()

    com1 = Comment(content='我是主评论1')
    com2 = Comment(content='我是主评论2')
    com11 = Comment(content='我是回复主评论1的子评论1')
    com11.parent = com1
    com12 = Comment(content='我是回复主评论1的子评论2')
    com12.parent = com1

    db.session.add_all([com1, com2, com11, com12])
    db.session.commit()
    app.run(debug=True)

自关联多对多(了解)

  • 示例场景
    • 用户关注其他用户(用户表,中间表)
  • 示例代码
tb_user_follows = db.Table(
    "tb_user_follows",
    db.Column('follower_id', db.Integer, db.ForeignKey('info_user.id'), primary_key=True),  # 粉丝id
    db.Column('followed_id', db.Integer, db.ForeignKey('info_user.id'), primary_key=True)  # 被关注人的id
)

class User(db.Model):
    """用户表"""
    __tablename__ = "info_user"

    id = db.Column(db.Integer, primary_key=True)  
    name = db.Column(db.String(32), unique=True, nullable=False)

    # 用户所有的粉丝,添加了反向引用followed,代表用户都关注了哪些人
    followers = db.relationship('User',
                                secondary=tb_user_follows,
                                primaryjoin=id == tb_user_follows.c.followed_id,
                                secondaryjoin=id == tb_user_follows.c.follower_id,
                                backref=db.backref('followed', lazy='dynamic'),
                                lazy='dynamic')

综合案例-图书管理

创建数据库连接信息,定义模型

from flask import Flask, render_template, redirect, url_for, flash, request
from flask_sqlalchemy import SQLAlchemy
from flask_wtf.csrf import CSRFProtect

app = Flask(__name__)

#开启csrf保护
CSRFProtect(app)

#设置数据库配置信息
app.config['SQLALCHEMY_DATABASE_URI'] = "mysql+pymysql://root:123456@127.0.0.1:3306/library2"
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False #压制警告信息

#创建SQLAlchemy对象,关联app
db = SQLAlchemy(app)

#设置密码
app.config['SECRET_KEY'] = "jfkdjfkdkjf"

#编写模型类
#作者(一方)
class Author(db.Model):
    __tablename__ = 'authors'
    id = db.Column(db.Integer,primary_key=True)
    name = db.Column(db.String(64),unique=True)

    #关系属性和反向引用
    books = db.relationship('Book',backref='author')

#书籍(多方)
class Book(db.Model):
    __tablename__ = 'books'
    id = db.Column(db.Integer,primary_key=True)
    name = db.Column(db.String(64),unique=True)

    #外键
    author_id = db.Column(db.Integer,db.ForeignKey('authors.id')) #或者是, Author.id




#添加书籍
@app.route('/add_book', methods=['POST'])
def add_book():
    """
    思路分析:
    1.获取参数
    2.校验参数
    3.通过作者名称,查询作者对象
    4.判断作者,判断书籍,进行添加
    5.重定向展示页
    :return:
    """
    # 1.获取参数
    author_name = request.form.get("author")
    book_name = request.form.get("book")

    # 2.校验参数
    if not all([author_name,book_name]):
        return "作者或者书籍为空"

    # 3.通过作者名称,查询作者对象
    author = Author.query.filter(Author.name == author_name).first()  # 有金庸

    # 4.判断作者,判断书籍,进行添加
    # 判断作者是否存在
    if author:

        # 通过书籍名称,查询书籍对象 数据库,古龙写了 天龙八部
        book = Book.query.filter(Book.name == book_name, Book.author_id == author.id).first()

        # 判断书籍是否存在
        if book:
            flash('该作者有该书籍')
        else:
            # 创建书籍对象,添加到数据库
            book = Book(name=book_name, author_id=author.id)
            db.session.add(book)
            db.session.commit()

    else:
        # 创建作者添加到数据库
        author = Author(name=author_name)
        db.session.add(author)
        db.session.commit()

        # 创建书籍添加到数据库
        book = Book(name=book_name, author_id=author.id)
        db.session.add(book)
        db.session.commit()

    # 5.重定向展示页
    return redirect(url_for('show_page'))


#删除书籍
@app.route('/delete_book/<int:book_id>')
def delete_book(book_id):
    #1.根据编号获取书籍对象
    book = Book.query.get(book_id)

    #2.删除书籍
    db.session.delete(book)
    db.session.commit()

    #3.重定向页面展示
    return redirect(url_for('show_page'))

#删除作者
@app.route('/delete_author/<int:author_id>')
def delete_author(author_id):
    #1.通过编号获取作者对象
    author = Author.query.get(author_id)

    #2.删除作者书籍
    for book in author.books:
        db.session.delete(book)

    #3.删除作者对象
    db.session.delete(author)
    db.session.commit()

    #4.重定向展示页面
    return redirect(url_for('show_page'))


if __name__ == '__main__':

    app.run(debug=True)

创建表,添加测试数据

if __name__ == '__main__':

    #为了演示方便,先删除所有表,再创建
    db.drop_all()
    db.create_all()


    #添加测试数据库
    # 生成数据
    au1 = Author(name='老王')
    au2 = Author(name='老尹')
    au3 = Author(name='老刘')
    # 把数据提交给用户会话
    db.session.add_all([au1, au2, au3])
    # 提交会话
    db.session.commit()


    bk1 = Book(name='老王回忆录', author_id=au1.id)
    bk2 = Book(name='我读书少,你别骗我', author_id=au1.id)
    bk3 = Book(name='如何才能让自己更骚', author_id=au2.id)
    bk4 = Book(name='怎样征服美丽少女', author_id=au3.id)
    bk5 = Book(name='如何征服英俊少男', author_id=au3.id)
    # 把数据提交给用户会话
    db.session.add_all([bk1, bk2, bk3, bk4, bk5])
    # 提交会话
    db.session.commit()

    app.run(debug=True)

数据显示&表单添加

  • 后端代码:
#展示页面
@app.route('/')
def show_page():

    #查询数据库
    authors = Author.query.all()

    # 渲染到页面
    return render_template('library.html',authors=authors)
  • 前端代码
  • 创建文件 library.html ,编写以下代码:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    {#注册表单#}
    <form action="/add_book" method="post">

        {# 设置隐藏的csrf_token #}
        <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">

        <p>
        <label>作者</label><input type="text" name="author"><br>
        </p>

        <p>
        <label>书籍</label><input type="text" name="book"><br>
        </p>

        <p>
        <input type="submit" value="添加">
        </p>

        {% for message in get_flashed_messages() %}
            <span style="color: red">{{ message }}</span>
        {% endfor %}

    </form>
    <hr>

    {# 书籍展示 #}
    <h2>书籍展示</h2>
    <ul>
        {% for author in authors %}
            <li>作者: {{ author.name }} <a href="{{ url_for('delete_author',author_id=author.id) }}">删除</a></li><br>
            <ul>
                {% for book in author.books %}
                    <li>书籍: {{ book.name }} <a href="{{ url_for('delete_book',book_id=book.id) }}">删除</a></li><br>
                {% endfor %}
            </ul>
        {% endfor %}
    </ul>
</body>
</html>

添加数据

  • 添加书籍
    @app.route('/add_book', methods=['POST'])
    def add_book():
      """
      思路分析:
      1.获取参数
      2.校验参数
      3.通过作者名称,查询作者对象
      4.判断作者,判断书籍,进行添加
      5.重定向展示页
      :return:
      """
      # 1.获取参数
      author_name = request.form.get("author")
      book_name = request.form.get("book")
    
      # 2.校验参数
      if not all([author_name,book_name]):
          return "作者或者书籍为空"
    
      # 3.通过作者名称,查询作者对象
      author = Author.query.filter(Author.name == author_name).first()  # 有金庸
    
      # 4.判断作者,判断书籍,进行添加
      # 判断作者是否存在
      if author:
    
          # 通过书籍名称,查询书籍对象 数据库,古龙写了 天龙八部
          book = Book.query.filter(Book.name == book_name, Book.author_id == author.id).first()
    
          # 判断书籍是否存在
          if book:
              flash('该作者有该书籍')
          else:
              # 创建书籍对象,添加到数据库
              book = Book(name=book_name, author_id=author.id)
              db.session.add(book)
              db.session.commit()
    
      else:
          # 创建作者添加到数据库
          author = Author(name=author_name)
          db.session.add(author)
          db.session.commit()
    
          # 创建书籍添加到数据库
          book = Book(name=book_name, author_id=author.id)
          db.session.add(book)
          db.session.commit()
    
      # 5.重定向展示页
      return redirect(url_for('show_page'))
    

删除数据

  • 删除书籍
#删除书籍
@app.route('/delete_book/<int:book_id>')
def delete_book(book_id):
    #1.根据编号获取书籍对象
    book = Book.query.get(book_id)

    #2.删除书籍
    db.session.delete(book)
    db.session.commit()

    #3.重定向页面展示
    return redirect(url_for('show_page'))
  • 删除作者
#删除作者
@app.route('/delete_author/<int:author_id>')
def delete_author(author_id):
    #1.通过编号获取作者对象
    author = Author.query.get(author_id)

    #2.删除作者书籍
    for book in author.books:
        db.session.delete(book)

    #3.删除作者对象
    db.session.delete(author)
    db.session.commit()

    #4.重定向展示页面
    return redirect(url_for('show_page'))

蓝图&单元测试

Blueprint

模块化

随着 flask 程序越来越复杂,我们需要对程序进行模块化的处理,之前学习过 python 的模块化管理,于是针对一个简单的 flask 程序进行模块化处理

举例来说:

我们有一个博客程序,前台界面需要的路由为:首页,列表,详情等页面

源程序app.py文件:
from flask import Flask

app=Flask(__name__)

@app.route('/')
def index():
    return 'index'

@app.route('/list')
def list():
    return 'list'

@app.route('/detail')
def detail():
    return 'detail'

if __name__=='__main__':
    app.run()

如果博主需要编辑博客,要进入后台进行处理:后台主页,编辑,创建,发布博客

改进后程序:
from flask import Flask

app=Flask(__name__)

@app.route('/')
def index():
    return 'index'

@app.route('/list')
def list():
    return 'list'

@app.route('/detail')
def detail():
    return 'detail'

@app.route('/')
def admin_home():
    return 'admin_home'

@app.route('/new')
def new():
    return 'new'

@app.route('/edit')
def edit():
    return 'edit'

@app.route('/publish')
def publish():
    return 'publish'

if __name__=='__main__':
    app.run()
  • 问题: 这样就使得我们在一个 py 文件中写入了很多路由,将来维护代码会非常麻烦,
  • 此时,考虑到了模块化的处理方式,将 admin 相关的路由写到一个 admin.py 文件中
修改后的代码:
app.py
from flask import Flask

app=Flask(__name__)

@app.route('/')
def index():
    return 'index'

@app.route('/list')
def list():
    return 'list'

@app.route('/detail')
def detail():
    return 'detail'

if __name__=='__main__':
    app.run()

admin.py

@app.route('/')
def admin_home():
    return 'admin_home'

@app.route('/new')
def new():
    return 'new'

@app.route('/edit')
def edit():
    return 'edit'

@app.route('/publish')
def publish():
    return 'publish'
  • 问题: 发现 app.py 文件中的 app 直接报错,代码无法继续写下去
  • 解决: 需要使用蓝图,flask 中提供了 Blueprint 类,来专门处理模块化开发

Blueprint 概念

  • 简单来说,Blueprint 是一个存储操作方法的容器,
  • 这些操作在这个 Blueprint 被注册到一个应用之后就可以被调用,
  • Flask 可以通过 Blueprint 来组织 URL 以及处理请求。

Flask 使用 Blueprint 让应用实现模块化,在 Flask 中,Blueprint 具有如下属性:

  • 一个应用可以具有多个 Blueprint
  • 在一个应用中,一个模块可以注册多次
  • Blueprint 可以单独具有自己的模板、静态文件或者其它的通用操作方法
  • 在一个应用初始化时,就应该要注册需要使用的 Blueprint
  • 一个 Blueprint 并不是一个完整的应用,它不能独立于应用运行,而必须要注册到某一个应用中。

初识蓝图

蓝图/Blueprint 对象用起来和一个应用/Flask 对象差不多,最大的区别在于一个 蓝图对象没有办法独立运行,必须将它注册到一个应用对象上才能生效

使用蓝图可以分为三个步骤

  • 1,创建一个蓝图对象
admin=Blueprint('admin',__name__)
  • 2,在这个蓝图对象上进行操作,注册路由,指定静态文件夹,注册模版过滤器
@admin.route('/')
def admin_home():
    return 'admin_home'
  • 3,在应用对象上注册这个蓝图对象
app.register_blueprint(admin,url\_prefix='/admin')

当这个应用启动后,通过/admin/可以访问到蓝图中定义的视图函数

蓝图对象中的参数

蓝图的 url 前缀

  • 当我们在应用对象上注册一个蓝图时,可以指定一个 url_prefix 关键字参数(这个参数默认是/)
  • 在应用最终的路由表 url_map 中,在蓝图上注册的路由 URL 自动被加上了这个前缀,这个可以保证在多个蓝图中使用相同的 URL 规则而不会最终引起冲突,只要在注册蓝图时将不同的蓝图挂接到不同的自路径即可
  • url_for
url_for('admin.index') # /admin/

注册静态路由

和应用对象不同,蓝图对象创建时不会默认注册静态目录的路由。需要我们在 创建时指定 static_folder 参数。

下面的示例将蓝图所在目录下的 static_admin 目录设置为静态目录

admin = Blueprint("admin",__name__,static_folder='static_admin')
app.register_blueprint(admin,url_prefix='/admin')

现在就可以使用/admin/static_admin/ 访问 static_admin 目录下的静态文件了 定制静态目录 URL 规则 :可以在创建蓝图对象时使用 static_url_path 来改变静态目录的路由。下面的示例将为 static_admin 文件夹的路由设置为 /lib

admin = Blueprint("admin",__name__,static_folder='static_admin',static_url_path='/lib')
app.register_blueprint(admin,url_prefix='/admin')

设置模版目录

蓝图对象默认的模板目录为系统的模版目录,可以在创建蓝图对象时使用 template_folder 关键字参数设置模板目录

admin = Blueprint('admin',__name__,template_folder='my_templates')

注:如果在 templates 中存在和 my_templates 同名文件,则系统会优先使用 templates 中的文件 参考链接:https://stackoverflow.com/questions/7974771/flask-blueprint-template-folder

单元测试

为什么要测试?

Web 程序开发过程一般包括以下几个阶段:[需求分析,设计阶段,实现阶段,测试阶段]。其中测试阶段通过人工或自动来运行测试某个系统的功能。目的是检验其是否满足需求,并得出特定的结果,以达到弄清楚预期结果和实际结果之间的差别的最终目的。

测试的分类:

测试从软件开发过程可以分为:

  • 单元测试
    • 对单独的代码块(例如函数)分别进行测试,以保证它们的正确性
  • 集成测试
    • 对大量的程序单元的协同工作情况做测试
  • 系统测试
    • 同时对整个系统的正确性进行检查,而不是针对独立的片段

在众多的测试中,与程序开发人员最密切的就是单元测试,因为单元测试是由开发人员进行的,而其他测试都由专业的测试人员来完成。所以我们主要学习单元测试。

什么是单元测试?

程序开发过程中,写代码是为了实现需求。当我们的代码通过了编译,只是说明它的语法正确,功能能否实现则不能保证。 因此,当我们的某些功能代码完成后,为了检验其是否满足程序的需求。可以通过编写测试代码,模拟程序运行的过程,检验功能代码是否符合预期。

单元测试就是开发者编写一小段代码,检验目标代码的功能是否符合预期。通常情况下,单元测试主要面向一些功能单一的模块进行。

举个例子:一部手机有许多零部件组成,在正式组装一部手机前,手机内部的各个零部件,CPU、内存、电池、摄像头等,都要进行测试,这就是单元测试。

在 Web 开发过程中,单元测试实际上就是一些“断言”(assert)代码。

断言就是判断一个函数或对象的一个方法所产生的结果是否符合你期望的那个结果。 python 中 assert 断言是声明布尔值为真的判定,如果表达式为假会发生异常。单元测试中,一般使用 assert 来断言结果。

断言方法的使用:

断言.png

断言语句类似于:

if not expression:  
    raise AssertionError
 AssertionError

常用的断言方法:

assertEqual     如果两个值相等,则pass
assertNotEqual  如果两个值不相等,则pass
assertTrue      判断bool值为True,则pass
assertFalse     判断bool值为False,则pass
assertIsNone    不存在,则pass
assertIsNotNone 存在,则pass

单元测试的基本写法:

首先,定义一个类,继承自 unittest.TestCase

import unittest
class TestClass(unitest.TestCase):
    pass

其次,在测试类中,定义两个测试方法

import unittest
class TestClass(unittest.TestCase):

    #该方法会首先执行,方法名为固定写法
    def setUp(self):
        pass

    #该方法会在测试代码执行完后执行,方法名为固定写法
    def tearDown(self):
        pass

最后,在测试类中,编写测试代码

import unittest
class TestClass(unittest.TestCase):

    #该方法会首先执行,相当于做测试前的准备工作
    def setUp(self):
        pass

    #该方法会在测试代码执行完后执行,相当于做测试后的扫尾工作
    def tearDown(self):
        pass
    #测试代码
    def test_app_exists(self):
        pass

登录测试

  • 被测试的代码逻辑
@app.route('/login', methods=['POST'])
def login():
    username = request.form.get('username')
    password = request.form.get('password')

    # 判断参数是否为空
    if not all([username, password]):
        result = {
            "errcode": -2,
            "errmsg": "params error"
        }
        return jsonify(result)

    # a = 1 / 0
    # 如果账号密码正确
    # 判断账号密码是否正确
    if username == 'itheima' and password == 'python':
        result = {
            "errcode": 0,
            "errmsg": "success"
        }
        return jsonify(result)
    else:
        result = {
            "errcode": -1,
            "errmsg": "wrong username or password"
        }
        return jsonify(result)
  • 单元测试代码
import json
import unittest
from demo1_login import app

class LoginTest(unittest.TestCase):
    """为登录逻辑编写测试案例"""

    def setUp(self):
        app.testing = True
        self.client = app.test_client()

    def test_empty_username_password(self):
        """测试用户名与密码为空的情况[当参数不全的话,返回errcode=-2]"""
        response = app.test_client().post('/login', data={})
        json_data = response.data
        json_dict = json.loads(json_data)

        self.assertIn('errcode', json_dict, '数据格式返回错误')
        self.assertEqual(json_dict['errcode'], -2, '状态码返回错误')

        # TODO 测试用户名为空的情况

        # TODO 测试密码为空的情况

    def test_error_username_password(self):
        """测试用户名和密码错误的情况[当登录名和密码错误的时候,返回 errcode = -1]"""
        response = app.test_client().post('/login', data={"username": "aaaaa", "password": "12343"})
        json_data = response.data
        json_dict = json.loads(json_data)
        self.assertIn('errcode', json_dict, '数据格式返回错误')
        self.assertEqual(json_dict['errcode'], -1, '状态码返回错误')

        # TODO 测试用户名错误的情况

        # TODO 测试密码错误的情况

if __name__ == '__main__':
    unittest.main()

数据库测试:

#coding=utf-8
import unittest
from author_book import *

#自定义测试类,setUp方法和tearDown方法会分别在测试前后执行。以test_开头的函数就是具体的测试代码。
class DatabaseTestCase(unittest.TestCase):
    def setUp(self):
        app.config['TESTING'] = True
        app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root:mysql@localhost/test0'
        self.app = app
        db.create_all()

    def tearDown(self):
        db.session.remove()
        db.drop_all()

    #测试代码
    def test_append_data(self):
        au = Author(name='itcast')
        bk = Book(info='python')
        db.session.add_all([au,bk])
        db.session.commit()
        author = Author.query.filter_by(name='itcast').first()
        book = Book.query.filter_by(info='python').first()
        #断言数据存在
        self.assertIsNotNone(author)
        self.assertIsNotNone(book)

作者 : General

地址 : http://www.yanghelong.top

  • Flask
    18 引用 • 9 回帖
  • Python

    Python 是一种面向对象、直译式电脑编程语言,具有近二十年的发展历史,成熟且稳定。它包含了一组完善而且容易理解的标准库,能够轻松完成很多常见的任务。它的语法简捷和清晰,尽量使用无异义的英语单词,与其它大多数程序设计语言使用大括号不一样,它使用缩进来定义语句块。

    536 引用 • 672 回帖

相关帖子

回帖

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...