#Flask概述

Flask 是一个微型的 Python 开发的 Web框架,基于Werkzeug WSGI工具箱和Jinja2 模板引擎。 Flask使用BSD授权。 Flask也被称为“microframework”,因为它使用简单的核心,用extension增加其他功能。Flask没有默认使用的数据库、窗体验证工具。然而,Flask保留了扩增的弹性,可以用Flask-extension加入这些功能:ORM、窗体验证工具、文件上传、各种开放式身份验证技术。

示例代码:

1
2
3
4
5
6
7
8
9
from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
return "Hello World!"

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

必须在项目中导入Flask模块。 Flask类的一个对象是我们的WSGI应用程序。

Flask构造函数使用当前模块(__name __的名称作为参数。

Flask类的route()函数是一个装饰器,它告诉应用程序哪个URL应该调用相关的函数。

1
app.route(rule, options)
  • rule 参数表示与该函数的URL绑定。
  • options 是要转发给基础Rule对象的参数列表。

在上面的示例中,’/ ‘ URL与hello_world()函数绑定。因此,当在浏览器中打开web服务器的主页时,将呈现该函数的输出。

最后,Flask类的run()方法在本地开发服务器上运行应用程序。

1
app.run(host='192.168.0.235',port=8081,debug=True)
  • host: 要监听的主机名。 默认为127.0.0.1(localhost)。设置为“0.0.0.0”以使服务器在外部可用
  • port: 指定端口,默认值为5000
  • debug=True: 相当于热部署,代码改变服务器会重新加载最新代码,用于开发环境。默认为Flase,代码发生改变不会自动加载,用于生产环境

启动:

1
2
$ pip install Flask
$ python hello.py

##特性

  • 内置开发用服务器和debugger
  • 集成单元测试(unit testing)
  • RESTful request dispatching
  • 使用Jinja2模板引擎
  • 支持secure cookies(client side sessions)
  • 100% WSGI 1.0兼容
  • Unicode based
  • 详细的文件、教学
  • Google App Engine兼容
  • 可用Extensions增加其他功能

Flask 环境

为开发环境安装virtualenv

virtualenv是一个虚拟的Python环境构建器。它可以帮助用户并行创建多个Python环境。 因此,它可以避免不同版本的库之间的兼容性问题。

Flask 路由

现代Web框架使用路由技术来帮助用户记住应用程序URL。可以直接访问所需的页面,而无需从主页导航。

Flask中的route()装饰器用于将URL绑定到函数。例如:

1
2
3
4
# 这个装饰器其实就是将rule字符串跟视图函数进行了绑定,通过add_url_rule()实现的绑定
@app.route(‘/hello’)
def hello_world():
return ‘hello world’

在这里,URL ‘/ hello’ 规则绑定到hello_world()函数。 因此,如果用户访问http:// localhost:5000 / hello URL,hello_world()函数的输出将在浏览器中呈现。

application对象的add_url_rule()函数也可用于将URL与函数绑定,如上例所示,使用route()

装饰器的目的也由以下表示:

1
2
3
4
def hello_world():
return ‘hello world’

app.add_url_rule(‘/’, ‘hello’, hello_world)

Flask 变量规则

Flask 变量规则

通过向规则参数添加变量部分,可以动态构建URL。此变量部分标记为。它作为关键字参数传递给与规则相关联的函数。

在以下示例中,route()装饰器的规则参数包含附加到URL ‘/hello’因此,如果在浏览器中输入http://localhost:5000/hello/w3cschool作为URL,则‘w3cschool’将作为参数提供给hello()函数。

1
2
3
4
5
6
7
8
9
from flask import Flask
app = Flask(__name__)

@app.route('/hello/<name>')
def hello_name(name):
return 'Hello %s!' % name

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

将上述脚本保存为hello.py并从Python shell运行它。接下来,打开浏览器并输入URL http:// localhost:5000/hello/w3cschool。

以下输出将显示在浏览器中:

1
Hello w3cschool!

除了默认字符串变量部分之外,还可以使用以下转换器构建规则:

序号 转换器和描述
1 int接受整数
2 float对于浮点值
3 path接受用作目录分隔符的斜杠

在下面的代码中,使用了所有这些构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
from flask import Flask
app = Flask(__name__)

@app.route('/blog/<int:postID>')
def show_blog(postID):
return 'Blog Number %d' % postID

@app.route('/rev/<float:revNo>')
def revision(revNo):
return 'Revision Number %f' % revNo

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

从Python Shell运行上面的代码。访问浏览器中的URL http://localhost:5000/blog/11

给定的数字用作show_blog()函数的参数。浏览器显示以下输出:

1
Blog Number 11

在浏览器中输入此URL - http://localhost:5000/rev/1.1

revision()函数将浮点数作为参数。以下结果显示在浏览器窗口中:

1
Revision Number 1.100000

Flask的URL规则基于Werkzeug的路由模块。这确保形成的URL是唯一的,并且基于Apache规定的先例。

考虑以下脚本中定义的规则:

1
2
3
4
5
6
7
8
9
10
11
12
13
from flask import Flask
app = Flask(__name__)

@app.route('/flask')
def hello_flask():
return 'Hello Flask'

@app.route('/python/')
def hello_python():
return 'Hello Python'

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

这两个规则看起来类似,但在第二个规则中,使用斜杠(/)。因此,它成为一个规范的URL。因此,使用 /python/python/返回相同的输出。但是,如果是第一个规则,/flask/ URL会产生“404 Not Found”页面。

Flask URL构建

Flask URL构建

url_for()函数对于动态构建特定函数的URL非常有用。该函数接受函数的名称作为第一个参数,以及一个或多个关键字参数,每个参数对应于URL的变量部分。

以下脚本演示了如何使用url_for()函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from flask import Flask, redirect, url_for
app = Flask(__name__)
@app.route('/admin')
def hello_admin():
return 'Hello Admin'
@app.route('/guest/<guest>')def hello_guest(guest):
return 'Hello %s as Guest' % guest
@app.route('/user/<name>')
def hello_user(name):
if name =='admin':
return redirect(url_for('hello_admin'))
else:
return redirect(url_for('hello_guest',guest = name))
if __name__ == '__main__':
app.run(debug = True)

上述脚本有一个函数user(name),它接受来自URL的参数的值。

User()函数检查接收的参数是否与‘admin’匹配。如果匹配,则使用url_for()将应用程序重定向到hello_admin()函数,否则重定向到将接收的参数作为guest参数传递给它的hello_guest()函数。

保存上面的代码并从Python shell运行。

打开浏览器并输入URL - http://localhost:5000/user/admin

浏览器中的应用程序响应是:

1
Hello Admin

在浏览器中输入以下URL - http://localhost:5000/user/mvl

应用程序响应现在更改为:

1
Hello mvl as Guest

Flask Request对象

来自客户端网页的数据作为全局请求对象发送到服务器。为了处理请求数据,应该从Flask模块导入。

Request对象的重要属性如下所列:

  • Form - 它是一个字典对象,包含表单参数及其值的键和值对。
  • args - 解析查询字符串的内容,它是问号(?)之后的URL的一部分。
  • Cookies - 保存Cookie名称和值的字典对象。
  • files - 与上传文件有关的数据。
  • method - 当前请求方法。

默认情况下,Flask路由响应GET请求。但是,可以通过为route()装饰器提供方法参数来更改此首选项。

为了演示在URL路由中使用POST方法,首先让我们创建一个HTML表单,并使用POST方法将表单数据发送到URL。将以下脚本另存为login.html

1
2
3
4
5
6
7
8
9
10
11
<html>
<body>

<form action = "http://localhost:5000/login" method = "post">
<p>Enter Name:</p>
<p><input type = "text" name = "nm" /></p>
<p><input type = "submit" value = "submit" /></p>
</form>

</body>
</html>

现在在Python shell中输入以下脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from flask import Flask, redirect, url_for, request
app = Flask(__name__)

@app.route('/success/<name>')
def success(name):
return 'welcome %s' % name

@app.route('/login',methods = ['POST', 'GET'])
def login():
if request.method == 'POST':
# 获取post请求的参数
user = request.form['nm']
return redirect(url_for('success',name = user))
else:
# 获取get请求的参数
user = request.args.get('nm')
return redirect(url_for('success',name = user))

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

开发服务器开始运行后,在浏览器中打开login.html,在文本字段中输入name,然后单击提交

Flask Response对象与模板

Response对象

Flask的视图函数return可以返回那些值?

  1. 可以返回字符串: 返回的字符串其实也是做了一个response对象的封装。最终的返回结果还是response对象

  2. dict对象: 转换成了application/json数据

  3. tuple对象: 返回元组时,顺序依次是:返回内容,返回状态码,返回的header

    1
    return 'hello world', 200, {'Locateion':'www.baidu.com'}
  4. response对象

  5. make_response()

  6. redirect() : 有两次响应:1. 302状态码 + location 2. 返回location请求地址内容

  7. render_template() 模板渲染 + 模板

  8. jsonify()函数,它把JSON输出到一个flask.Response与应用程序/ JSON的mimetype对象

###标准WSGI接口

实际上,flask是基于Werkzeug工具包的一个web服务框架,所以flask里视图函数的实现,实际上是对werkzeug的一层封装,我们先看一下一个简单的werkzeug应用是怎么实现的:

1
2
3
4
5
6
7
from werkzeug.wrappers import Request, Response

def application(environ, start_response):
request = Request(environ)
text = 'Hello %s!' % request.args.get('name', 'World')
response = Response(text, mimetype='text/plain')
return response(environ, start_response)

可以看到,实际上werkzeug要求每次请求的返回值是Response类型。所以实际上,flask的视图函数返回值无论如何变,它都不会离开Response类型。

flask里组装过程

flask视图函数调用过程如下:

1
2
3
4
5
6
7
8
rv = full_dispatch_request() 
dispatch_request()
self.view_functions[rule.endpoint](**req.view_args)
finalize_request(rv)
make_response(rv)
rv = self.response_class(rv, status=status, headers=headers)
# 这里response_class实际上就是Response类
return rv

可以看到,无论flask里视图函数的返回值是什么样式,最终都会调用Flask.make_response()里的response_class构造一个Response对象。回头看flask视图函数的返回值:

  • 如果返回的是3个元素的元组,则会在构造Response时使用元组里的后两个作为Response的返回code以及header
  • 如果返回的值是单个元素
    • 如果是字符串,那么在Flask.make_response()里会加上默认的code和header,并且header里content-type是text/html
    • 如果是jsonify(), 会在jsonify()方法里组装一个Response对象,并在header里添加content-type为text/json。然后返回
  • 如果返回的是render_template, 会通过flask里的渲染引擎将html渲染成字符串返回,之后便和返回字符串形式相同
  • 如果返因的是make_response, 则会直接生成Response对象

在Flask.make_response()里,会根据视图函数返回的值的个数,类型的不同,来做不同的处理,生成最终的Response对象,并为字添加code和header等。

模板

标签呈现‘Hello World’

1
2
3
4
5
6
7
8
9
from flask  import Flask
app = Flask(__name__)

@app.route('/')
def index():
return '<html><body><h1>'Hello World'</h1></body></html>'

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

但是,从Python代码生成HTML内容很麻烦,尤其是在需要放置变量数据和Python语言元素(如条件或循环)时。这需要经常从HTML中转义。

render_template()

这是可以利用Flask所基于的Jinja2模板引擎的地方。而不是从函数返回硬编码HTML,可以通过render_template()函数呈现HTML文件。

1
2
3
4
5
6
7
from flask import Flask,render_template
app = Flask(name)
@app.route('/')
def index():
return render_template(‘hello.html’)
if name == 'main':
app.run(debug = True)

Flask将尝试在templates文件夹中找到HTML文件,该文件存在于此脚本所在的文件夹中。

  • Application folder
    • Hello.py
    • templates
      • hello.html

术语‘web templating system(web模板系统)’指的是设计一个HTML脚本,其中可以动态插入变量数据。web模板系统包括模板引擎,某种数据源和模板处理器。

Flask使用jinga2模板引擎。Web模板包含用于变量和表达式(在这些情况下为Python表达式)的HTML语法散布占位符,这些是在呈现模板时替换的值。

以下代码在templates文件夹中另存为hello.html

1
2
3
4
5
6
7
8
<!doctype html>
<html>
<body>
{# 变量部分插入{{name}}占位符。#}
<h1>Hello {{ name }}!</h1>

</body>
</html>

接下来,从Python shell运行以下脚本:

1
2
3
4
5
6
7
8
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/hello/')
def hello_name(user):
# render_template('模板名字',**context)
return render_template('hello.html', name = user)
if __name__ == '__main__':
app.run(debug = True)

当开发服务器开始运行时,打开浏览器并输入URL - http://localhost:5000/hello/mvl

模板语法

Jinja2模板引擎使用以下分隔符从HTML转义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
1.{% ... %}用于语句
# 1. 条件判断
{% if 条件 %}

{% endif %}

# 2. 嵌套条件
{% if 条件 %}
条件为True
{% else %}
条件为False
{% endif %}

{% if 条件 %}
pass
{% elif 条件 %}
pass
....
{% endif %}
# 3. 循环遍历
{% for 变量 in 可迭代的对象 %}
for循环要做的任务
# 可以使用loop变量
loop.index 序号从1开始
loop.index0 序号从0开始
loop.revindex reverse 序号是倒着的
loop.revindex0
loop.first 布尔类型 是否是第一行
loop.last 布尔类型 是否是第二行
{% endfor %}
2.{{ ... }}用于表达式可以打印到模板输出
在模板中获取view中传递的变量值:{{ 变量名key }}
# render_template('模板名字',key=value,key=value)
name = '沈凯' # str
age = 18 # int
friends = ['建义', '陈璟', '小岳岳', '郭麒麟'] # list
dict1 = {'gift': '大手镯', 'gift1': '鲜花', 'gift2': '费列罗'} # dict
# 创建对象
girlfriend = Girl('美美', '安徽阜阳') # 自定义的类构建的类型:Girl对象

模板:
{{name}}
{{age}}
{{ list.0 }} 同 {{ list[0] }}
{{ dict.key }} 同 {{ dict.get(key) }}
{{ girl.name }} 同 {{ 对象.属性 }}
3.{# ... #}用于未包含在模板输出中的注释

过滤器

当遇到特殊的值需要打印到模板输出时,我们可以使用过滤器,过滤器的本质就是函数

模板语法中过滤器:

1
2
{{ 变量名 | 过滤器(*args) }}
{{ 变量名 | 过滤器 }}

常见的过滤器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
1。 safe : 禁用转译
msg = '<h1>520快乐!</h1>'
return render_template('show_2.html', girls=girls, users=users, msg=msg)
不想让其转译:
{{ msg | safe }}
2。 capitalize:单词的首字母大写
{{ n1 | capitalize }}
3。lower和upper
大小写的转换
4。title 一句话中每个单词的首字母大写
msg = 'She is a beautiful girl'
{{ msg | title}}
5。reverse 翻转
{{ n1 | reverse}}
6。format
{{ '%s is %d years old' | format('lily',18) }}
7.truncate 字符串截断
list的操作:
{# 列表过滤器的使用 #}
{{ girls | first }}<br>
{{ girls | last }}<br>
{{ girls | length }}<br>
{#{{ girls | sum }} 整型的计算 #}
{{ [1,3,5,7,9] | sum }}<br>
{{ [1,8,5,7,3] | sort }}<br>


dict:
{% for v in users.0.values() %} ---->获取值
<p>{{ v }}</p>
{% endfor %}

<hr>
{% for k in users.0.keys() %} ----》获取键
<p>{{ k }}</p>
{% endfor %}

<hr>

{% for k,v in users.0.items() %} ---》获取键值
<p>{{ k }}---{{ v }}</p>
{% endfor %}

自定义过滤器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#第一种方式
通过flask模块中的add_template_filter方法
1. 定义函数,带有参数和返回值
2. 添加过滤器 app.add_template_filter(function,name='')
3. 在模板中使用: {{ 变量 | 自定义过滤器 }}

def replace_hello(value):
print('------>', value)
value = value.replace('hello', '')
print('======>', value)
return value.strip() # 将 替换的结果返回

app.add_template_filter(replace_hello, 'replace') # replace 自定义过滤器的名字


使用装饰器完成
1. 定义函数,带有参数和返回值
2. 通过装饰器完成,@app.template_filter('过滤器名字')装饰步骤一的函数
3. 在模板中使用: {{ 变量 | 自定义过滤器 }}
# 第二种方式 装饰器
@app.template_filter('listreverse')
def reverse_list(li):
temp_li = list(li)
temp_li.reverse()
return temp_li

模板代码的复用

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

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

对宏(macro)的理解:

  • 可以把宏理解为一个函数,它会返回一个模板或者 HTML 字符串
  • 为了避免反复地编写同样的模板代码,出现代码冗余,可以把他们写成函数以进行重用
  • 需要在多处重复使用的模板代码片段可以写入单独的文件,再包含在所有模板中,以避免重复

使用

方式一

1、定义宏

1
2
3
{% macro input(name,value='',type='text') %}
<input type="{{type}}" name="{{name}}" value="{{value}}" class="form-control">
{% endmacro %}

2、调用宏

1
{{ input('name' value='zs')}}

3、这会输出

1
<input type="text" name="name" value="zs" class="form-control">
方式二

4、把宏单独抽取出来,封装成html文件,其它模板中导入使用,文件名可以自定义macro.html

1
2
3
{% macro function(type='text', name='', value='') %}
<input type="{{type}}" name="{{name}}" value="{{value}}" class="form-control">
{% endmacro %}

5、在其它模板文件中先导入,再调用

1
2
{% import 'macro.html' as func %}
{% func.function() %}

继承

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

  • 相当于在父模板中挖个坑,当子模板继承父模板时,可以进行填充。
  • 子模板使用 extends 指令声明这个模板继承自哪个模板
  • 父模板中定义的块在子模板中被重新定义,在子模板中调用父模板的内容可以使用super()

父模板

base.html

1
2
3
4
5
6
7
8
9
10
{% block top %}
顶部菜单
{% endblock top %}

{% block content %}
{% endblock content %}

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

子模板

extends指令声明这个模板继承自哪

1
2
3
4
{% extends 'base.html' %}
{% block content %}
需要填充的内容
{% endblock content %}

模板继承使用时注意点:

  1. 不支持多继承
  2. 为了便于阅读,在子模板中使用extends时,尽量写在模板的第一行。
  3. 不能在一个模板文件中定义多个相同名字的block标签。
  4. 当在页面中使用多个block标签时,建议给结束标签起个名字,当多个block嵌套时,阅读性更好。

包含

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

1、include的使用

1
{% include 'hello.html' %}

包含在使用时,如果包含的模板文件不存在时,程序会抛出TemplateNotFound异常,可以加上 ignore missing 关键字。如果包含的模板文件不存在,会忽略这条include语句。

2、include 的使用加上关键字ignore missing

1
{% include 'hello.html' ignore missing %}

小结:

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

Flask 静态文件

Web应用程序通常需要静态文件,例如javascript文件或支持网页显示的CSS文件。通常,配置Web服务器并为您提供这些服务,但在开发过程中,这些文件是从您的包或模块旁边的static文件夹中提供,它将在应用程序的/static中提供。

特殊端点static用于生成静态文件的URL。

在下面的示例中,在index.html中的HTML按钮的OnClick事件上调用hello.js中定义的javascript函数,该函数在Flask应用程序的“/”URL上呈现。

1
2
3
4
5
6
7
8
9
from flask import Flask, render_template
app = Flask(__name__)

@app.route("/")
def index():
return render_template("index.html")

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

index.html的HTML脚本如下所示:

1
2
3
4
5
6
7
8
<html>
<head>
<script type = "text/javascript" src = "{{ url_for('static', filename = 'hello.js') }}" > </script>
</head>
<body>
<input type = "button" onclick = "sayHello()" value = "Say Hello" />
</body>
</html>

Hello.js包含sayHello()函数。

1
2
3
function sayHello() {
alert("Hello World")
}

Flask 蓝图

什么是蓝图

蓝图(blueprint):用于实现单个应用的视图、模板、静态文件的集合

蓝图就是模块化处理的类。类似于django中app,子应用。简单来说,蓝图就是一个存储操作路由映射方法的容器,主要用来实现客户端请求和URL相互关联的功能。 在Flask中,将项目模块化(把写在一个文件里的路由拆封成多个文件里写路由),blueprint,是flask自带的一种开发模式,目的是为了方便开发大型的项目

蓝图的运行机制

蓝图是保存了一组将来可以在应用对象上执行的操作。

注册路由就是一种操作,当在程序实例上调用route装饰器注册路由时,这个操作将修改对象的url_map路由映射列表。当我们在蓝图对象上调用route装饰器注册路由时,它只是在内部的一个延迟操作记录列表defered_functions中添加了一个项。当执行应用对象的 register_blueprint() 方法时,应用对象从蓝图对象的 defered_functions 列表中取出每一项,即调用应用对象的 add_url_rule()方法,这将会修改程序实例的路由映射列表。

蓝图的使用

  1. 创建项目并设置settings.py文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    # app配置类
    class Config:
    # 步骤3 - 现在创建一个Flask应用程序对象并为要使用的数据库设置URI。
    DEBUG = True
    # mysql+pymysql://user:password@hostip:port/databasename
    SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://root:1234561@172.16.0.192:3306/flaskday05'
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    SQLALCHEMY_ECHO = True

    # 开发环境
    class DevelopmentConfig(Config):
    ENV = 'development'

    # 生产环境
    class ProductionConfig(Config):
    ENV = 'production'
    DDEBUG = False
  2. 删除app.py中的部分内容

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    from flask import Flask
    from flask_migrate import Migrate, MigrateCommand
    from flask_script import Manager

    from apps import create_app
    from exts import db

    app = create_app()

    # flask-script
    manager = Manager(app=app)
    # 使用里面的Manager进行命令得到管理和使用:
    migrate = Migrate(app=app, db=db)
    manager.add_command('db', MigrateCommand)

    if __name__ == '__main__':
    manager.run()
  3. 创建包apps,并在包中__init__.py中创建app的工厂函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    from flask import Flask
    import settings
    from apps.user.view import user_bp
    from exts import db

    def create_app():
    app = Flask(__name__, template_folder='../templates', static_folder='../static') # app是一个核心对象
    # 加载配置
    app.config.from_object(settings.DevelopmentConfig)
    # 初始化配置db
    db.init_app(app=app)
    # 注册蓝图
    # app.register_blueprint(user_bp) # 将蓝图对象绑定到app上
    return app
  4. 在apps中创建项目的程序包比如user,goods,order等

  5. 以user为例,在user包中创建view.py,在view.py中定义蓝图

    1
    2
    user_bp = Blueprint('user', __name__)
    # 其中第一个参数是蓝图的名字,第二个参数是import_name
  6. 使用蓝图定义路由和视图函数

    1
    2
    3
    @user_bp.route('/')
    def user_center():
    return render_template('user/show.html', users=users)

    注意在蓝图中进行了模板的加载,而模板的文件夹默认是跟flask对象是同级的,如果不是同级的注意在Flask对象创建的时候初始化设置

  7. 注册蓝图到app对象上,在__init__.py方法create_app()中添加

    1
    2
    # 注册蓝图
    # app.register_blueprint(user_bp) # 将蓝图对象绑定到app上

Flask-Script

Flask-Script是一个让你的命令行支持自定义命令的工具,它为Flask程序添加一个命令行解释器。可以让我们的程序从命令行直接执行相应的程序

通过使用Flask-Script扩展,我们可以在Flask服务器启动的时候,通过命令行的方式传入参数。而不仅仅通过app.run()方法中传参,比如我们可以通过python hello.py runserver –h ip地址 -p 端口地址,告诉服务器在哪个网络接口监听来自客户端的连接。默认情况下,服务器只监听来自服务器所在计算机发起的连接,即localhost连接。我们可以通过python hello.py runserver --help来查看参数。

Flask-Script插件为在Flask里编写额外的脚本提供了支持。包括了一个开发服务器,一个定制的Python命令行,用于执行初始化数据库、定时任务和其他属于web应用之外的命令行任务的脚本

安装

1
pip install flask-script

集成Flask-Script到falsk应用中

app.py文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from flask import Flask

app = Flask(__name__)

"""使用flask-script启动项目"""
from flask_script import Manager

# 使用里面的Manager进行命令得到管理和使用:
manager = Manager(app=app)

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

if __name__ == "__main__"
# 使用manager启动
manager.run()

在终端使用命令启动:

1
python app.py runserver

设置主机和端口启动:

1
python app.py runserver -h 0.0.0.0 -p 5001

自定义命令

使用@manager.command装饰器,注意:必须要引入当前的app对象(即Flask的初始化对象,app只是一个变量名),然后传入给Manager对象,赋值给一个变量为manager

1
2
3
4
# 装饰命令的功能添加到注册表中
@manager.command
def init():
print('初始化')

@manager.command中的manager为Manager对象的变量引用,只要遵循变量的命令规则即可使用

终端:

1
python app.py init

#Flask-migrate

Flask-Migrate 插件提供了和 Django 自带的 migrate 类似的功能。

即 Alembic(Database migration数据迁移跟踪记录)提供的数据库升级和降级的功能。它所能实现的效果有如 Git 管理项目代码一般。

安装 Flask-Migrate

1
pip install flask-migrate

多数情况下 Flask-Migrate 是会和命令行工具插件 Flask-Script 和数据库插件 flask_sqlalchemy 一起使用的,所有也把 Flask-Script 安装一下:

1
pip install flask-script

实例

官方文档给出的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_script import Manager
from flask_migrate import Migrate, MigrateCommand

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db'

db = SQLAlchemy(app)

# 初始化 migrate
# 两个参数一个是 Flask 的 app,一个是数据库 db
migrate = Migrate(app, db)

# 初始化管理器
manager = Manager(app)
# 添加 db 命令,并与 MigrateCommand 绑定
manager.add_command('db', MigrateCommand)

# 构建我们的数据模型
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(128))

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

假设上面的代码保存在文件 manage.py 中,我们可以这样执行相关命令:

1
2
3
4
$ python manage.py db init
$ python manage.py db migrate
$ python manage.py db upgrade
$ python manage.py db --help

当然,更多情况下我们不会把代码都放到一个文件里面,我们应该建立一个 manage.py 来放置相关代码,详细源码我传到 Github 上面:https://github.com/SingleDiego/flask-migrate

查看指令 manager db 的可用选项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
>>> python manage.py db
usage: Perform database migrations

Perform database migrations

positional arguments:
{init,revision,migrate,edit,merge,upgrade,downgrade,show,history,heads,branches,current,stamp}
init 创建新的迁移存储库
revision 创建新修订文件。.
migrate “修订--自动生成”的别名
edit 编辑当前版本。
merge 将两个修订合并在一起。创建新迁移文件
upgrade 升级到更高版本
downgrade 还原为以前的版本
show 显示用给定符号表示的修订。
history 按时间顺序列出变更集脚本。
heads 在脚本目录中显示当前可用的头
branches 显示当前分支点
current 显示每个数据库的当前修订。
stamp 在修订表上“盖章”给定版本;不要运行任何迁移

optional arguments:
-?, --help show this help message and exit

初始化 DB Migrate

1
>>> python manage.py db init

执行该命令后会创建一个 migrations 目录,所有的更改记录文件都会被保存在该目录下。

开始第一次跟踪

1
>>> python manage.py db migrate -m "Initial migration"

该指令会扫描所有的 SQLAlchemy 对象,并将没有记录过的行和列记录成为一个 python 文件并保存到 migrations/versions 路径下。-m 的描述相对于提交说明,会在版本文件_后显示然后给本次扫描结果分配一个版本号。生成的数据库会有一张名为 alembic_version 的空表来记录数据库版本号。

例如,我的例子中第一个版本的版本号就是: 6c1971cccc11_initial_migration.py,打开该文件,可以看到该文件记录了数据库的结果,比如这样:

1
2
3
4
5
6
7
8
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('user',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=128), nullable=True),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###

根据记录文件生成数据库

1
>>> python manage.py db upgrade

该命令会根据 migrations/versions 路径下的文件来创建或修改数据库,生成的数据库会有一张名为 alembic_version 的表来记录数据库版本号。

回滚到某历史版本

1
2
3
4
5
# 获取 History ID
>>> python manage.py db history

# 回滚到某个 history
>>> python manage.py db downgrade <history_id>

总结

flask_migrate相关的命令:

  • python manage.py db init:初始化一个迁移脚本的环境,只需要执行一次。
  • python manage.py db migrate:将模型生成迁移文件,只要模型更改了,就需要执行一遍这个命令。
  • python manage.py db upgrade:将迁移文件真正的映射到数据库中。每次运行了migrate命令后,就记得要运行这个命令。

问题一:修改字段长度无法迁移

使用flask-migrate进行数据库迁移时,如果更改db.String()的长度?该如何同步

比如有一个简单的模型

1
2
3
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(32), index=True)

一开始,username的长度限制在32,现在将其增大,比如说变为128,flask-migrate似乎不会检测到这个String变长的变化。

我使用了命令自动生成迁移脚本

1
2
python app.py db migrate -m "some comment"
python app.py db upgrade

之后,发现username的列的长度依旧被限制在32.

如何进行String长度的变化这种迁移?

解决问题:

alem­bic 支持检测字段长度改变,不过它不是默认的,需要配置;找到 mi­gra­tions/env.py文件,在 run_mi­gra­tions_on­line 函数(87行左右)加入如下内容:

1
2
3
4
5
6
7
8
context.configure(
connection=connection,
target_metadata=target_metadata,
process_revision_directives=process_revision_directives,
compare_type=True, # 检查字段类型
compare_server_default=True, # 比较默认值
**current_app.extensions['migrate'].configure_args
)

重新进行模型迁移

1
python manage.py db migrate

查看 up­grade 函数内,可以发现更改的内容已在里面。

##问题二:Target database is not up to date

flask-migrate数据迁移添加新的表,执行python manager.py db migrate 出现Target database is not up to date(目标数据库不是最新的。)

解决:

1
2
3
4
5
6
7

$: python manager.py db heads # 1. 查看migrate的状态
$ python manager.py db current # 2. 查看当前的状态
# 3. 发现版本号不一致
$ python manager.py db stamp head # 4. 更新版本一致
$ python manager.py db migrate # 5. 将模型生成迁移文件
$ python manager.py db upgrade # 6. 将迁移文件真正的映射到数据库中

Flask SQLAlchemy

Flask SQLAlchemy

Flask Web应用程序中使用原始SQL对数据库执行CRUD操作可能很繁琐。相反, SQLAlchemy ,Python工具包是一个强大的ORMapper,它为应用程序开发人员提供了SQL的全部功能和灵活性。Flask-SQLAlchemy是Flask扩展,它将对SQLAlchemy的支持添加到Flask应用程序中。

什么是ORM(Object Relation Mapping,对象关系映射)?

大多数编程语言平台是面向对象的。另一方面,RDBMS服务器中的数据存储为表。对象关系映射是将对象参数映射到底层RDBMS表结构的技术。ORM API提供了执行CRUD操作的方法,而不必编写原始SQL语句。

Flask SQLAlchemy环境搭建

步骤1 - 安装Flask-SQLAlchemy扩展。pymysql,flask-migrate

1
2
3
pip install pymysql   # mysql连接
pip install flask-migrate # 发布命令工具
pip install flask-sqlalchemy # 实现ORM映射

步骤2-创建settings.py配置数据库的连接路径

1
2
3
4
5
6
7
8
9
10
11
12
13
class Config:
DEBUG = True
# mysql+pymysql://user:password@hostip:port/databasename
SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://root:root@127.0.0.1:3306/flaskday05'
SQLALCHEMY_TRACK_MODIFICATIONS = False
SQLALCHEMY_ECHO = True

class DevelopmentConfig(Config):
ENV = 'development'

class ProductionConfig(Config):
ENV = 'production'
DDEBUG = False

步骤2 - 创建包ext,在__init__.py中添加

1
2
3
#创建一个映射对象
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy() # 必须跟app联系

步骤3 - 创建包apps,在__init__.py中添加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from flask import Flask

import settings
# 敲黑板:需要将你想要映射到数据库中的模型,都要导入到manage(app).py文件中,如果没有导入进去,就不会映射到数据库中
from apps.user.view import user_bp
from ext import db

def create_app():
app = Flask(__name__, template_folder='../templates', static_folder='../static')
app.config.from_object(settings.DevelopmentConfig)
db.init_app(app) # 将db对象与app进行了关联
# 注册蓝图
app.register_blueprint(user_bp)
return app

步骤4 - 在app.py配置flask-migrate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from flask import Flask
from flask_migrate import Migrate, MigrateCommand
from flask_script import Manager
from apps.user.models import User,UserInfo
from apps import create_app
from ext import db

app = create_app()
manager = Manager(app=app)
# flask-migrate命令工具
migrate = Migrate(app=app, db=db)
manager.add_command('db', MigrateCommand)

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

步骤5- 然后使用应用程序对象作为参数创建SQLAlchemy类的对象。该对象包含用于ORM操作的辅助函数。它还提供了一个父Model类,使用它来声明用户定义的模型。在下面的代码段中,创建了students模型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# ORM   类 ----》 表
# 类对象 ---〉表中的一条记录
from datetime import datetime

from ext import db

#相当于SQL:create table user(id int primarykey auto_increment,username varchar(20) not null,..)
class User(db.Model):
# db.Column(类型,约束) 映射表中的列
'''
类型:
Integer 整型
String(size) 字符串类型,务必指定大小
Text 长文本类型
DateTime 日期时间
Float 浮点类型
Boolean 布尔类型
PickleType 存储pickle类型 主要跟序列化有关
LargeBinary 存储大的二进制类型

可选的:
primary_key=True 主键
autoincrement=True 自增
nullable=False 不允许为空
unique=True 唯一
default=datetime.now 默认值 可以设置成当前系统时间或者其他的值
'''
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
username = db.Column(db.String(15), nullable=False)
password = db.Column(db.String(12), nullable=False)
phone = db.Column(db.String(11), unique=True)
email = db.Column(db.String(20))
rdatetime = db.Column(db.DateTime, default=datetime.now)

def __str__(self):
return self.username

class UserInfo(db.Model):
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
realname = db.Column(db.String(20))
gender = db.Column(db.Boolean, default=False)

def __str__(self):
return self.realname

敲黑板:需要将你想要映射到数据库中的模型,都要导入到manage(app).py文件中,如果没有导入进去,就不会映射到数据库中

步骤5 - 在终端使用db命令生成数据库

1
2
python app.py db init   -----》 产生一个文件夹migrations
python app.py db migrate -----> 在versions文件夹下自动产生了一个.py的版本文件

项目结构

1
2
3
4
5
6
7
8
9
10
| ---apps
| ---ext
| ---static
| ---templates
| ---migrations # python app.py db init 只需要init一次
|---versions 版本文件夹
|---71edde7ee937_.py ---》 python app.py db migrate 迁移
|---cc0dca61130f_.py
# python app.py db upgrade 根据记录文件生成数据库
# python app.py db downgrade 回滚到某历史版本

测试添加数据

添加数据:以注册用户为例

  1. 在templates文件夹中创建user/register.html页面

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>用户注册</title>
    </head>
    <body>
    <h1>用户注册</h1>
    <form action='{{ url_for('user.register') }}' method="post">
    <p><input type="text" name="username" placeholder="用户名"></p>
    <p><input type="password" name="password" placeholder="密码"></p>
    <p><input type="password" name="repassword" placeholder="确认密码"></p>
    <p><input type="text" name="phone" placeholder="手机号码"></p>
    <p><input type="submit" value="用户注册"></p>
    </form>
    </body>
    </html>

  2. 在apps包下创建user/view.py文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    from flask import Blueprint, request, render_template
    from apps.user.models import User
    from ext import db
    user_bp = Blueprint('user', __name__)


    @user_bp.route('/register', methods=['GET', 'POST'])
    def register():
    if request.method == 'POST':
    username = request.form.get('username')
    password = request.form.get('password')
    repassword = request.form.get('repassword')
    phone = request.form.get('phone')
    if password == repassword:
    # 1. 找到模型类并创建对象
    user = User()
    # 2. 给对象的属性赋值
    user.username = username
    user.password = hashlib.sha256(password.encode('utf-8')).hexdigest()
    user.phone = phone
    # 3. 将user对象添加到session中(类似缓存)
    db.session.add(user)
    # 4.提交数据
    db.session.commit()
    # 在使用蓝图的情况下使用url_for(),必须带有蓝图名
    return redirect(url_for('user.user_center'))

    return render_template('user/register.html')

    注意:在view中定义了蓝图,需要在app对象中进行注册 # app.register_blueprint(user_bp)

  3. center.html

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>用户中心</title>
    </head>
    <body>
    <div><a href="{{ url_for('user.register') }}">注册</a> <a href="{{ url_for('user.login') }}">登录</a> <a href="">退出</a>
    </div>
    <div>
    <h1>所有用户信息如下:</h1>
    {% if users %}
    <table border="1" cellspacing="0" width="50%">
    <tr>
    <th>序号</th>
    <th>用户名</th>
    <th>手机号</th>
    <th>注册时间</th>
    <th>操作</th>
    </tr>
    {% for user in users %}
    <tr>
    <th>{{ loop.index }}</th>
    <th>{{ user.username }}</th>
    <th>{{ user.phone }}</th>
    <th>{{ user.rdatetime }}</th>
    <th><a href="">更新</a> <a href="">删除</a></th>
    </tr>
    {% endfor %}

    </table>
    {% else %}
    <p style="color: red; font-size: 20px;">当前还没有任何的用户,抓紧时间注册吧!!!</p>
    {% endif %}
    </div>
    </body>
    </html>

CRUD方法

SQLAlchemySession对象管理ORM对象的所有持久性操作。

以下session方法执行CRUD操作:

  • db.session.add (模型对象) - 将记录插入到映射表中
  • db.session.delete (模型对象) - 从表中删除记录
  • model.query.all() - 从表中检索所有记录(对应于select * from user)。

您可以通过使用filter属性将过滤器应用于检索到的记录集。例如,要在学生表中检索city =’Hyderabad’的记录,请使用以下语句:

1
Students.query.filter_by(city = ’Hyderabad’).all()

查询

1)查询所有:

1
模型类.query.all()    ~  select * from user;

2)如果有条件的查询:

1
2
模型类.query.filter_by(字段名 = 值)   ~  select * from user where 字段=值;
模型类.query.filter_by(字段名 = 值).first() ~ select * from user where 字段=值 limit..

3)模型类.query.filter()

1
2
3
4
5
6
7
8
9
1. 模型类.query.filter().all()   -----> 列表
2. 模型类.query.filter().first() ----->对象
3. User.query.filter(User.username.endswith('z')).all()
select * from user where username like '%z';
User.query.filter(User.username.startswith('z')).all()
# select * from user where username like 'z%';
User.query.filter(User.username.contains('z')).all()
# select * from user where username like '%z%';
User.query.filter(User.username.like('z%')).all()

4)多条件

1
2
3
4
5
6
7
并且: and_    获取: or_   非: not_

1. User.query.filter(or_(User.username.like('z%'), User.username.contains('i'))).all()
类似: select * from user where username like 'z%' or username like '%i%';
2. User.query.filter(and_(User.username.contains('i'), User.rdatetime.__gt__('2020-05-25 10:30:00'))).all()
# select * from user where username like '%i%' and rdatetime < 'xxxx'
3. User.query.filter(not_(User.username.contains('i'))).all()

补充:__gt__,__lt__,__ge__(gt equal),__le__(le equal) —-》通常应用在范围(整型,日期) 也可以直接使用 > < >= <= !=

5)排序:order_by

1
2
3
4
5
6
user_list = User.query.filter(User.username.contains('z')).order_by(-User.rdatetime).all() 
# 先筛选再排序
user_list = User.query.order_by(-User.id).all() 对所有的进行排序
注意:order_by(参数):
1. 直接是字符串: '字段名' 但是不能倒序
2. 填字段名: 模型.字段 order_by(-模型.字段) 倒序

6)限制: limit

1
2
3
# limit的使用 + offset
# user_list = User.query.limit(2).all() 默认获取前两条
user_list = User.query.offset(2).limit(2).all() 跳过2条记录再获取两条记录

总结:

  1. User.query.all() 所有
  2. User.query.get(pk) 一个
  3. User.query.filter()
    • 如果要检索的字段是字符串(varchar,db.String):
      • User.username.startswith(‘’)
      • User.username.endswith(‘’)
      • User.username.contains(‘’)
      • User.username.like(‘’)
      • User.username.in_([‘’,’’,’’])
      • User.username == ‘zzz’
    • 如果要检索的字段是整型或者日期类型:
      • User.age.__lt__(18)
      • User.rdatetime.__gt__('.....')
      • User.age.__le__(18)
      • User.age.__ge__(18)
      • User.age.between(15,30)
    • 多个条件一起检索:and_, or_
    • 非的条件: not_
    • 排序:order_by()
    • 获取指定数量: limit() offset()
  4. User.query.filter_by()

添加

1
2
3
4
5
6
7
8
9
10
# 1. 找到模型类并创建对象
user = User()
# 2. 给对象的属性赋值
user.username = username
user.password = hashlib.sha256(password.encode('utf-8')).hexdigest()
user.phone = phone
# 3. 将user对象添加到session中(类似缓存)
db.session.add(user)
# 4.提交数据
db.session.commit()

删除

逻辑删除

逻辑删除(定义数据库中的表的时候,添加一个字段isdelete,通过此字段控制是否删除),实际上是更新操作

1
2
3
4
5
id = request.args.get(id)
user = User.query.get(id)
user.isdelete = True
# 提交更改
db.session.commit()

####物理删除

物理删除(彻底从数据库中删掉)

1
2
3
4
5
id = request.args.get(id)
user = User.query.get(id)
db.session.delete(user)
# 提交更改
db.session.commit()

更新

1
2
3
4
5
6
7
id = request.args.get(id)
user = User.query.get(id)
# 修改对象的属性
user.username= xxxx
user.phone =xxxx
# 提交更改
db.session.commit()

分页paginate

1
2
3
4
5
6
7
8
9
10
11
12
13
@user_bp.route('/')
def index():
page = int(request.args.get('page', 1))
pagination = Article.query.order_by(-Article.pdatetime).paginate(page=page, per_page=3)
print(pagination.items) # [<Article 4>, <Article 3>, <Article 2>]
print(pagination.page) # 当前的页码数
print(pagination.prev_num) # 当前页的前一个页码数
print(pagination.next_num) # 当前页的后一页的页码数
print(pagination.has_next) # True
print(pagination.has_prev) # True
print(pagination.pages) # 总共有几页
print(pagination.total) # 总的记录条数
return render_template('user/index.html', user=user, types=types, pagination=pagination)

为了显示某页中的记录,要把 all()换成 Flask-SQLAlchemy 提供的paginate() 方法。页 数是 paginate()方法的第一个参数,也是唯一必需的参数。可选参数 per_page用来指定 每页显示的记录数量;如果没有指定,则默认显示 20 个记录。另一个可选参数为 error_ out,当其设为 True 时(默认值),如果请求的页数超出了范围,则会返回 404 错误;如果 设为 False,页数超出范围时会返回一个空列表。

paginate()方法的返回值是一个 Pagination 类对象,这个类在 Flask-SQLAlchemy 中定义。 这个对象包含很多属性,用于在模板中生成分页链接,因此将其作为参数传入了模板。

  • print(pagination.items) # 当前页面中的记录
  • print(pagination.query) # 分页的源查询
  • print(pagination.page) # 当前的页码数
  • print(pagination.prev_num) # 当前页的前一个页码数
  • print(pagination.next_num) # 当前页的后一页的页码数
  • print(pagination.has_next) # 如果有下一页,返回True
  • print(pagination.has_prev) # 如果有上一页,返回True
  • print(pagination.pages) # 查询得到的总页数
  • print(pagination.total) # 总的记录条数
  • print(pagination.per_page) # 每页显示的记录数

Flask 模型关系

python框架中不方便直接写表连接查询和子查询。python框架为了简化多表的查询,制定了多表的关系:

一对多

flask的框架中如何体现一对多的模型关系?

就是通过外键ForignKey(一般在多的一方维护一的数据,即把外键创建在多的一方,对一的一方的引用,就好比一个班级有一名老师(teacher)多名学生(student),老师记住所有学生的名字简单还是学生记住老师的名字简单,很明显学生记住老师的名字简单)和relationship体现。

ForignKey是给映射关系(表)说的,一般写在多的一方。relationship是给模板(models)使用的,双方模型多可以写,一般写在一的一方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class User(db.Model):
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
username = db.Column(db.String(15), nullable=False)
password = db.Column(db.String(64), nullable=False)
phone = db.Column(db.String(11), unique=True, nullable=False)
email = db.Column(db.String(30))
icon = db.Column(db.String(100))
isdelete = db.Column(db.Boolean, default=False)
rdatetime = db.Column(db.DateTime, default=datetime.now)
# 增加一个字段,相当于放到了一个list集合中,只需要模型.articles就可以拿到数据
articles = db.relationship('Article', backref='user')
#
def __str__(self):
return self.username
1
2
3
4
5
6
7
8
9
10
11
12
13
14
from datetime import datetime

from exts import db

class Article(db.Model):
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
title = db.Column(db.String(50), nullable=False)
content = db.Column(db.Text, nullable=False)
pdatetime = db.Column(db.DateTime, default=datetime.now)
click_num = db.Column(db.Integer, default=0)
save_num = db.Column(db.Integer, default=0)
love_num = db.Column(db.Integer, default=0)
# 外键 同步到数据库的外键关系
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)

多对多

1
2
3
4
5
6
7
8
9
10
11
12
13
class Page(db.Model):
id = db.Column(db.Integer, primary_key=True)
tags = db.relationship('Tag', secondary='Page_tag',backref='pages')

class Tag(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(20),nullable=False)

# 中间表 维护多对多关系
class Page_tag(db.Model):
id = db.Column(db.Integer, primary_key=True)
db.Column('tag_id', db.Integer, db.ForeignKey('tag.id'))
db.Column('page_id', db.Integer, db.ForeignKey('page.id'))

Flask-Boostrap

我们可以手动引入Bootstrap框架用来美化界面,也可以使用Flask的扩展Flask-Bootstrap,作用

  • 自动引用 CDN
  • 可以与 WTForm 或其他 Flask 插件比如 Flask-nav 一起使用

##安装 Flask-Bootstrap:

1
pip install flask-bootstrap

##关联 Flask app

内置的block

1
2
3
4
5
6
7
8
9
10
11
12
13
{% block title %}首页{% endblock %}

{% block navbar %} {% endblock %}

{% block content %} {% endblock %}

{% block styles %}{{super()}} {% endblock %}

{% block srcipts %} {{super()}}{% endblock %}

{% block head %} {% endblock %}

{% block body %} {% endblock %}

##使用 Flask-Bootstrap 渲染 WTF Form

edit_form.html 为例,之前的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Edit Note</title>
</head>
<body>
<h2>Edit Note</h2>

<form method="POST">
{{ form.body(rows='10',cols='100') }}<br/>
{{ form.submit }}
</form>
</body>
</html>

其中 Form 中的字段是通过 WTForm 传入的,对这些字段如何套用 Bootstrap 的样式呢?

  • 引用 Flask-Bootstrap 的 wtf.html 文件
1
{% import 'bootstrap/wtf.html' as wtf %}
  • 调用 wtf.form_field() 表单宏:
1
{{ wtf.form_field(some_field) }}

经过改写的 edit_form.html 如下:

1
2
3
4
5
6
7
8
9
10
11
{% extends 'base.html' %}
{% import 'bootstrap/wtf.html' as wtf %}

{% block body_content %}
<h2>Edit Note</h2>

<form class="form" method="POST">
{{ wtf.form_field(form.body) }}
{{ wtf.form_field(form.submit) }}
</form>
{% endblock %}

##自定义表单样式

对于表单的样式,比如希望输入 body 的时候有多行,有两种定义方法。方法一是在定义 Form 字段的时候,传入一个 render_kw 参数,这个参数为 dict 类型,如下面这样:

1
2
3
class EditNoteForm(Form):
body = TextAreaField('body', render_kw={'class': 'text-body', 'rows': 10, 'placeholder': '输入您的想法...'})
submit = SubmitField('Update')

方法二是在渲染时控制样式:

1
{{ wtf.form_field(form.body, , class="text-body", rows="10")) }}

问题一:访问资源404

使用其他CDN

如果你想使用其他CDN资源,那么可以直接在Flask-Bootstrap的源码里修改,找到\venv\Lib\site-packages\flask_bootstrap_init_.py,在文件末尾,将下面这些文件的地址修改成你想引用的CDN地址即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
bootstrap = lwrap(
WebCDN('//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/%s/' %
BOOTSTRAP_VERSION), local)

jquery = lwrap(
WebCDN('//cdnjs.cloudflare.com/ajax/libs/jquery/%s/' %
JQUERY_VERSION), local)

html5shiv = lwrap(
WebCDN('//cdnjs.cloudflare.com/ajax/libs/html5shiv/%s/' %
HTML5SHIV_VERSION))

respondjs = lwrap(
WebCDN('//cdnjs.cloudflare.com/ajax/libs/respond.js/%s/' %
RESPONDJS_VERSION))

比如换成cdn.bootcss.com提供的资源:

1
2
3
4
5
6
7
8
9
10
11
bootstrap = lwrap(
WebCDN('//cdn.bootcss.com/bootstrap/%s/' % BOOTSTRAP_VERSION), local)

jquery = lwrap(
WebCDN('//cdn.bootcss.com/jquery/%s/' % JQUERY_VERSION), local)

html5shiv = lwrap(
WebCDN('//cdn.bootcss.com/html5shiv/%s/' % HTML5SHIV_VERSION))

respondjs = lwrap(
WebCDN('//cdn.bootcss.com/respond.js/%s/' % RESPONDJS_VERSION))

Flask 会话机制

  在网站中,HTTP请求是无状态的。也就是说,即使第一次用户访问服务器并登录成功后,第二次请求服务器依然不知道当前发起请求的是哪个用户。cookie的出现就是为了解决这个问题,第一次登录后服务器返回一些数据(cookie)给浏览器,浏览器将这些数据保存在本地。当用户发起第二次请求的时候,浏览器自动的将上次请求得到的cookie数据携带给服务器,服务器通过这些cookie数据就能分辨出当前发起请求的是哪个用户了。cookie存储的数据量有限,不同的浏览器有不同的存储大小,但一般不超过4K,因此使用cookie只能存储一些少量的数据。

##session

  sessioncookie的作用有点类似,都是为了存储用户相关的信息。不同的是,cookie是存储在本地浏览器,session存储在服务器。存储在服务器的数据会更加的安全,不容易被窃取。但存储在服务器也有一定的弊端,就是会占用服务器的资源。

cookie和session的结合使用

web开发发展至今,cookiesession的使用已经出现了一些非常成熟的方案。在如今的市场和企业里,一般有两种存储方式:

  • 存储在服务器:通过cookie存储一个session_id,然后具体的数据则保存在session中。当用户已经登录时,会在浏览器的cookie中保存一个session_id,下次再次请求的时候,会把session_id携带上来,服务器根session_id在session库中获取用户的session数据,从而能够辨别用户身份,以及得到之前保存的状态信息。这种专业术语叫做server side session
  • 将session数据加密,然后存储在cookie中。这种专业术语叫做client side session,flask采用的就是这种方式,但是也可以替换成其它形式

实现方式:

cookie方式:

  • 保存:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    通过response对象保存。
    response = redirect(xxx)
    response = render_template(xxx)
    response = Response()
    response = make_response()
    response = jsonify()
    通过对象调用方法
    response.set_cookie(key,value,max_age)
    其中max_age表示过期时间,单位是秒
    也可以使用expires设置过期时间,expires=datetime.now()+timedelta(hour=1)
  • 获取:

    1
    2
    3
    4
    5
    通过request对象获取。
    request.form.get()
    request.args.get()
    cookie也在request对象中
    request.cookies.get(key) ----> value
  • 删除:

    1
    2
    3
    4
    5
    6
    7
    8
    通过response对象删除。 把浏览器中的key=value删除了
    response = redirect(xxx)
    response = render_template(xxx)
    response = Response()
    response = make_response()
    response = jsonify()
    通过对象调用方法
    response.delete_cookie(key)

session方式:

session:是在服务器端进行用户信息的保存。一个字典

注意:使用session必须要设置配置文件,在配置文件中添加SECRET_KEY='xxxxx'
添加SECRET_KEY的目的就是用于sessionid的加密。如果不设置会报错。

  • 设置:

    如果要使用session,需要直接导入:

    from flask import session
    把session当成字典使用,因此:session[key]=value就会将key=value保存到session的内存空间并会在响应的时候自动在response中自动添加有一个cookie:session=加密后的id

  • 获取
    用户请求页面的时候就会携带上次保存在客户端浏览器的cookie值,其中包含session=加密后的id
    获取session值的话通过session直接获取,因为session是一个字典,就可以采用字典的方式获取即可。
    value = session[key]或者value = session.get(key)

这个时候大家可能会考虑携带的cookie怎么用的????

其实是如果使用session获取内容,底层会自动获取cookie中的sessionid值,进行查找并找到对应的session空间

  • 删除
    session.clear() : 删除session的内存空间和删除cookie
    del session[key]: 只会删除session中的这个键值对,不会删除session空间和cookie

Flask 密码加密解密

  • 使用werkzeug.security自带的函数实现加密:generate_password_hash,一般用于用户注册写入数据库加密后的密码

  • 使用werkzeug.security自带的函数实现解密:check_password_hash(pwdHash,password),一般用于登录

    • 返回值为bool,如果flag=True表示匹配,否则密码不匹配
    • pwhash -数据库哈希加密字符串
    • password -明文密码进行比较的哈希值

Flask 钩子函数

钩子函数是指在执行函数和目标函数之间挂载的函数,框架开发者给调用方提供一个point-挂载点,至于挂载什么函数由调用方决定,大大提供了灵活性。

##常用的钩子函数

###@before_first_request
在对应用程序实例的第一个请求之前注册要运行的函数,只会运行一次

1
2
3
4
# 服务器被第一次访问执行的钩子函数
@app.before_first_request
def first_request():
print("Hello World")

###@before_request
在每次请求之前执行. 通常使用这个钩子函数预处理一些变量, 视图函数可以更好调用

1
2
3
4
5
6
7
# 在服务器接收的请求还没分发到视图函数之前执行的钩子函数
@app.before_request
def before_request():
# print("我勾住了每次请求")
user_id = session.get("user_id")
if user_id:
g.user = "DaYe"

###@after_request
在每个请求之后注册一个要运行的函数,每次请求完成后都会执行。
需要接受一个Response对象作为参数,并返回一个新的Response对象,或者返回接收的Response对象

###@teardown_request
注册一个函数,同样在每次请求之后运行.注册的函数至少需要含有一个参数,这个参数实际上为服务器的响应,且函数中需要返回这个响应参数.

1
2
3
@app.teardown_request
def td_request(param):
return param

###@teardown_appcontext

当APP上下文被移除之后执行的函数, 可以进行数据库的提交或者回滚

1
2
3
4
5
6
7
@app.teardown_appcontext
def teardown(exc=None):
if exc is None:
db.session.commit()
else:
db.session.rollback()
db.session.remove()

###@context_processor

上下文处理器,返回的字典可以在全部的模板中使用

1
2
3
4
5
6
7
8
9
@app.context_processor()
def context():
# 必须返回一个字典
# hasattr(obj, attr) 判断obj是否有attr属性, 注意此时的attr应该是字符串
if hasattr(g, "user"):
return {"current_username": "DaYe"}
else:
# 注意: 必须返回一个字典
return {}

###@template_filter(‘xxxxxx’)
增加模板过滤器,可以在模板中使用该函数,后面的参数是名称,在模板中用到

1
2
3
@app.template_filter
def upper_filter(s):
return s.upper()

###@errorhandler(400)
发生一些异常时,比如404,500,或者抛出异常(Exception)之类的,就会自动调用该钩子函数

  1. 发生请求错误时,框架会自动调用相应的钩子函数,并向钩子函数中传入error参数
  2. 如果钩子函数没有定义error参数,就会报错
  3. 可以使用abort函数来手动终止请求抛出异常,如果要是发生参数错误,可以abort(404)之类的
1
2
3
4
5
6
7
8
@app.errorhander(404)
def page_not_found(error):
return render_template("error400.html"), 404


@app.errorhander(500)
def server_error(error):
return render_template("error505.html"), 500

Flask 文件上传

在Flask中处理文件上传非常简单。它需要一个HTML表单,其enctype属性设置为“multipart / form-data”,将文件发布到URL。URL处理程序从request.files[‘icon’]对象中提取文件,并将其保存到所需的位置。

每个上传的文件首先会保存在服务器上的临时位置,然后将其实际保存到它的最终位置。目标文件的名称可以是硬编码的,也可以从request.files[file]request.files.get(‘icon’)对象的filename属性中获取。但是,建议使用secure_filename()函数获取它的安全版本。

可以在Flask对象的配置设置中定义默认上传文件夹的路径和上传文件的最大大小。

app.config[‘UPLOAD_FOLDER’] 定义上传文件夹的路径
app.config[‘MAX_CONTENT_PATH’] 指定要上传的文件的最大大小(以字节为单位)

以下代码具有’/ upload’ URL规则,该规则在templates文件夹中显示’upload.html’,以及‘/ upload-file’ URL规则,用于调用uploader()函数处理上传过程。

1
2
3
4
5
6
7
8
9
10
11
<html>
<body>

<form action = "http://localhost:5000/uploader" method = "POST"
enctype = "multipart/form-data">
<input type = "file" name = "file" />
<input type = "submit"/>
</form>

</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from flask import Flask, render_template, request
from werkzeug import secure_filename
app = Flask(__name__)

@app.route('/upload')
def upload_file():
return render_template('upload.html')

@app.route('/uploader', methods = ['GET', 'POST'])
def upload_file():
if request.method == 'POST':
f = request.files['file'] # FileStorage 相当于一个储存文件的大容器
# 或者 f = request.files.get('file')
f.save(secure_filename(f.filename))
# secure_filename()保证文件名是符合python的命名规则
return 'file uploaded successfully'

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

思考:如果用户量足够多图片都放在服务器,硬盘要足够大,加载会很慢

实际开发我们的文件会保存在第三方的云储存服务器上如:(七牛云,腾讯云,阿里云…),可以加快访问和节空间,但是需要收费

Flask 邮件

基于web的应用程序通常需要具有向用户/客户端发送邮件的功能。Flask-Mail扩展使得与任何电子邮件服务器建立简单的接口变得非常容易。

步骤1-首先,应该在pip实用程序的帮助下安装Flask-Mail扩展。

1
pip install Flask-Mail

然后需要通过设置以下应用程序参数的值来配置Flask-Mail。

步骤2 - 然后按照以下设置配置Flask-Mail

1
2
3
4
5
6
app.config['MAIL_SERVER']='smtp.qq.com'  # 电子邮件服务器的名称/IP地址
app.config['MAIL_PORT'] = 465 # 使用的服务器的端口号
app.config['MAIL_USERNAME'] = '15096000421@qq.com' # 发件人的用户名
app.config['MAIL_PASSWORD'] = 'nukhrruyvdhkdjff' # 发件人的密码
app.config['MAIL_USE_TLS'] = False # 启用/禁用传输安全层加密
app.config['MAIL_USE_SSL'] = True # 启用/禁用安全套接字层加密

步骤3 - 创建Mail类的实例

1
mail = Mail(app)  # 与Flask对象关联

##Mail类的方法

序号 方法与描述
1 send()发送Message类对象的内容
2 connect()打开与邮件主机的连接
3 send_message()发送消息对象

步骤4 - 创建路由规则映射的Python函数中设置Message对象

1
2
3
4
5
6
7
8
9
10
@app.route("/send")
def index():
# Message 它封装了一封电子邮件
msg = Message('Hello', sender = '15096000421@qq.com', recipients = ['zysheep@126.com'])
# 纯文本邮件
msg.body = "Hello Flask message sent from Flask-Mail"

# 发送Message类对象的内容
mail.send(msg)
return "Sent"

Message类

它封装了一封电子邮件。Message类构造函数有几个参数:

1
2
flask-mail.Message(subject, recipients, body, html, sender, cc, bcc, 
reply-to, date, charset, extra_headers, mail_options, rcpt_options)
  • subject -电子邮件的主题标题
  • recipients -电子邮件地址列表
  • body -纯文本邮件
  • html - HTML邮件
  • sender -电子邮件发件人地址,或** ** MAIL_DEFAULT_SENDER默认
  • cc - CC列表
  • bcc - BCC列表
  • attachments -附件实例列表
  • reply_to -回复地址
  • date -发送日期
  • charset -消息字符集
  • extra_headers -为消息的附加标题的字典
  • mail_options -的ESMTP选项列表在Mail中使用FROM指令
  • rcpt_options -的ESMTP选项列表在RCPT命令中使用

Message类方法

attach() - 为邮件添加附件。此方法采用以下参数:

  • filename - 要附加的文件的名称
  • content_type - MIME类型的文件
  • data - 原始文件数据
  • 处置 - 内容处置(如果有的话)。

add_recipient() - 向邮件添加另一个收件人

Flask短信服务

短信服务(Short Message Service)这里我们使用第三方网易易盾提供的服务。网易易盾为用户提供的一种通信服务的能力,目前支持验证码类短信、通知类短信、运营类短信、语音类短信、国际短信等事务性短信。

smssend.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# -*- coding: utf-8 -*-
import hashlib
import json
import random
import ssl
import time
import urllib
import urllib.parse
import urllib.request
import requests


class SecretPair(object):
def __init__(self, secret_id, secret_key):
self.secret_id = secret_id
self.secret_key = secret_key


class SmsSendAPIDemo(object):
"""易盾短信发送接口示例代码"""
API_URL = "https://sms.dun.163yun.com/v2/sendsms"
VERSION = "v2"

def __init__(self, business_id, secret_pair):
self.business_id = business_id
self.secret_pair = secret_pair

def gen_signature(self, params=None):
"""生成签名信息
Args:
params (object) 请求参数
Returns:
参数签名md5值
"""
buff = ""
for k in sorted(params.keys()):
buff += str(k) + str(params[k])
buff += self.secret_pair.secret_key
buff = buff.encode('utf-8')
return hashlib.md5(buff).hexdigest()

def send(self, params):
"""请求易盾接口
Args:
params (object) 请求参数
Returns:
请求结果,json格式
"""
params["secretId"] = self.secret_pair.secret_id
params["businessId"] = self.business_id
params["version"] = self.VERSION
params["timestamp"] = int(time.time() * 1000)
params["nonce"] = int(random.random() * 100000000)
params["signature"] = self.gen_signature(params)

try:
params = urllib.parse.urlencode(params)
params = params.encode('utf-8')
context = ssl._create_unverified_context() # 忽略安全
request = urllib.request.Request(self.API_URL, params)
response = urllib.request.urlopen(request, timeout=1, context=context)
content = response.read()

return json.loads(content)
except Exception as ex:
print("调用API接口失败:", str(ex))


if __name__ == "__main__":
"""示例代码入口"""
SECRET_ID = "dcc535cbfaefa2a24c1e6610035b6586" # 产品密钥ID,产品标识
SECRET_KEY = "d28f0ec3bf468baa7a16c16c9474889e" # 产品私有密钥,服务端生成签名信息使用,请严格保管,避免泄露
BUSINESS_ID = "748c53c3a363412fa963ed3c1b795c65" # 业务ID,易盾根据产品业务特点分配
secret_pair = SecretPair(SECRET_ID, SECRET_KEY)
api = SmsSendAPIDemo(BUSINESS_ID, secret_pair)

params = {
# 短信接收者
"mobile": "15010185644",
# 模板Id
"templateId": "11732",
"paramType": "json",
# 验证码
"params": {"code": "2901"}
# 国际短信对应的国际编码(非国际短信接入请注释掉该行代码)
# "internationalCode": "对应的国家编码"
}
ret = api.send(params)
print(ret)
if ret is not None:
if ret["code"] == 200:
taskId = ret["data"]["requestId"]
print("taskId = %s" % taskId)
else:
print("ERROR: ret.code=%s,msg=%s" % (ret['code'], ret['msg']))

网易易盾工具类utils.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from smssend import SmsSendAPIDemo, SecretPair

# 发送短信息
def send_duanxin(phone):
SECRET_ID = "dcc535cbfaefa2a24c1e6610035b6586" # 产品密钥ID,产品标识
SECRET_KEY = "d28f0ec3bf468baa7a16c16c9474889e" # 产品私有密钥,服务端生成签名信息使用,请严格保管,避免泄露
BUSINESS_ID = "748c53c3a363412fa963ed3c1b795c65" # 业务ID,易盾根据产品业务特点分配
secret_pair = SecretPair(SECRET_ID, SECRET_KEY)
api = SmsSendAPIDemo(BUSINESS_ID, secret_pair)
# 验证码随机产生
code = ""
for i in range(4):
ran = random.randint(0, 9)
code += str(ran)

params = {
"mobile": phone,
"templateId": "11732",
"paramType": "json",
# 以前是网易产生的,现在需要自己产生
"params": {"code": code}
}

ret = api.send(params)
return ret, code

view视图页面调用工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 发送短信息
@user_bp1.route('/sendMsg')
def send_message():
phone = request.args.get('phone')
# 补充验证手机号码是否注册,去数据库查询
ret, code = send_duanxin(phone)
# 验证是否发送成功
if ret is not None:
if ret["code"] == 200:
# cache.set(key,value,timeout=second)
# 设置短信验证码保存到redis中,设置过期时间
# 1,安装 pip3 install flask-caching
cache.set(phone, code, timeout=180)
return jsonify(code=200, msg='短信发送成功!')
else:
print("ERROR: ret.code=%s,msg=%s" % (ret['code'], ret['msg']))
return jsonify(code=400, msg='短信发送失败!')

Flask-caching(缓存)

什么是缓存?为什么使用缓存?

由于短信验证码有过期有过期时间,很显然存入到数据库解决不了过期时间的需求,所以就需要使用redis缓存技术。数据库是 web 应⽤性能的瓶颈,为了提⾼web 应用访问效率,尽可能减少数据库的操作,可以将经常访问的数据缓存起来,再次使⽤用时直接从缓存中获取,而不是每次都操作数据库。

第一步:先下载flask-caching,redis

1
2
pip install redis
pip install flask-caching

第二步:引入缓存并注册

在exts包下__init__.py文件中引入

1
2
from flask_caching  import Cache  #引入
cache = Cache() #缓存 初始化

在apps包下__init__.py文件中注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# redis配置
config ={
CACHE_NO_NULL_WARNING = "warning" # null类型时的警告消息
CACHE_ARGS = [] # 在缓存类实例化过程中解包和传递的可选列表,用来配置相关后端的额外的参数
CACHE_OPTIONS = {} # 可选字典,在缓存类实例化期间传递,也是用来配置相关后端的额外的键值对参数
CACHE_DEFAULT_TIMEOUT # 默认过期/超时时间,单位为秒
CACHE_THRESHOLD # 缓存的最大条目数

CACHE_TYPE = null # 默认的缓存类型,无缓存
CACHE_TYPE = 'simple' # 使用本地python字典进行存储,线程非安全

CACHE_TYPE = 'filesystem' # 使用文件系统来存储缓存的值
CACHE_DIR = "" # 文件目录

CACHE_TYPE = 'memcached' # 使用memcached服务器缓存
CACHE_KEY_PREFIX # 设置cache_key的前缀
CAHCE_MEMCACHED_SERVERS # 服务器地址的列表或元组
CACHE_MEMCACHED_USERNAME # 用户名
CACHE_MEMCACHED_PASSWORD # 密码

CACHE_TYPE = 'uwsgi' # 使用uwsgi服务器作为缓存
CACHE_UWSGI_NAME # 要连接的uwsgi缓存实例的名称

CACHE_TYPE = 'redis' # 使用redis作为缓存
CACHE_KEY_PREFIX # 设置cache_key的前缀
CACHE_REDIS_HOST # redis地址
CACHE_REDIS_PORT # redis端口
CACHE_REDIS_PASSWORD # redis密码
CACHE_REDIS_DB # 使用哪个数据库
# 也可以一键配置
CACHE_REDIS_URL 连接到Redis服务器的URL。示例redis://user:password@localhost:6379/2
}
# 初始化缓存文件
cache.init_app(app=app,config=config)

第三步:在Views.py中引入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from exts import  cache 

# 发送短信息
@user_bp.route('/sendMsg')
def send_message():
phone = request.args.get('phone')
# 补充验证手机号码是否注册,去数据库查询
ret, code = send_duanxin(phone)
# 验证是否发送成功
if ret is not None:
if ret["code"] == 200:
# cache.set(key,value,timeout=second)
# 设置短信验证码保存到redis中,设置过期时间
# 1,安装 pip3 install flask-caching
cache.set(phone, code, timeout=180)
return jsonify(code=200, msg='短信发送成功!')
else:
print("ERROR: ret.code=%s,msg=%s" % (ret['code'], ret['msg']))
return jsonify(code=400, msg='短信发送失败!')

相关使用

缓存视图函数

1
2
3
4
5
6
7
8
9
10
@app.route('/')
@cache.cached(timeout=30) # timeout 设置超时时间
def index():
print('查询数据库')
return '缓存视图函数'

@app.route('/clear/')
def index():
cache.clear()
return '清除所有的缓存,操作需慎重,不推荐使用'

缓存普通函数(无参)

1
2
3
4
5
6
7
8
9
10
# 缓存普通函数时,推荐指定 key_prefix,缓存 key 的前缀
# 否则 key 即为调⽤的视图函数所在的路由
# 单独缓存某个函数,提供更好的复用性
@cache.cached(timeout=50, key_prefix='get_list')
def get_list():
return [random.randint(0, 10) for i in range(10)]

@app.route('/random/')
def random():
return get_list()

缓存普通函数(有参)

1
2
3
4
5
6
7
8
9
10
11
12
13
@cache.memoize(timeout=50, make_name='param_func')
def param_func(a, b):
return a+b+random.randint(1, 50)

@app.route('/cache/')
def cache():
param_func(1, 2)
return 'cache'

@app.route('/delete/')
def delete():
cache.delete_memoized(param_func, 1, 2)
return 'delete'

缓存对象(键值对)

1
2
3
4
5
6
7
8
9
10
11
@app.route('/test')
def test():
cache.set('name','小明', timeout=30)
cache.set('person', {'name':'小明', 'age':18})
x = cache.get('name')
print(x)
cache.set_many([('name1','小明'),('name2','老王')])
print(cache.get_many("name1","name2"))
print(cache.delete("name"))
print(cache.delete_many("name1","name2"))
return 'test'

cache 对象的主要方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
cache.cached:装饰器,装饰无参数函数,使得该函数结果可以缓存
参数:
timeout:超时时间
key_prefix:设置该函数的标志
unless:设置是否启用缓存,如果为True,不启用缓存
forced_update:设置缓存是否实时更新,如果为True,无论是否过期都将更新缓存
query_string:为True时,缓存键是先将参数排序然后哈希的结果

cache.memoize:装饰器,装饰有参数函数,使得该函数结果可以缓存
make_name:设置函数的标志,如果没有就使用装饰的函数
# 其他参数同cached

cache.delete_memoized:删除缓存
参数:
func_name:缓存函数的引用
*args:函数参数

cache.clear() # 清除缓存所有的缓存,这个操作需要慎重
cache.
get(key) #获取一个键的值,如果值是json格式会自动转化成字典
set(key,value,timeout) #设置一个键值,value可以是字典,会自动转化json格式的字符串,timeout过期时间
add(key, value, timeout=None) #设置一个键值,如果存在就pass,注意和set的区别
delete(key) #删除键 如果删除成功就返回True,否则返回 False

Flask对象储存

为什么使用对象储存?

  1. 本地去存储图片,占用自己的服务器资源
  2. 本地资源太多,加载时间长

总结:所以使用第三方平台七牛云提供的对象储存,因为它提供了一定时间免费时间和第三方服务商有提供数据分析方便观察,节省服务器空间,缺点需要收费

七牛云工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
from qiniu import Auth, put_file, etag, put_data, BucketManager
import qiniu.config

# 文件上传
def upload_qiniu(filestorage):
# 需要填写你的 Access Key 和 Secret Key
access_key = '3lObV41wal56iu2nqfsbpNg5E8XNHn3raOiMiahM'
secret_key = 'c8BhUBveLYC9DXEQa71CZflOLY9uiNYdZrHUccVB'
# 构建鉴权对象
q = Auth(access_key, secret_key)
# 要上传的空间
bucket_name = 'flask-upload-test'
# 上传后保存的文件名
filename = filestorage.filename
ran = random.randint(1, 1000)
suffix = filename.rsplit('.')[-1]
key = filename.rsplit('.')[0] + '_' + str(ran) + '.' + suffix
# 生成上传 Token,可以指定过期时间等
token = q.upload_token(bucket_name, key, 3600)
# 要上传文件的本地路径
# localfile = './sync/bbb.jpg'
# ret, info = put_file(token, key, localfile)
ret, info = put_data(token, key, filestorage.read())
return ret, info

# 文件删除
def delete_qiniu(filename):
# 需要填写你的 Access Key 和 Secret Key
access_key = '3lObV41wal56iu2nqfsbpNg5E8XNHn3raOiMiahM'
secret_key = 'c8BhUBveLYC9DXEQa71CZflOLY9uiNYdZrHUccVB'
# 构建鉴权对象
q = Auth(access_key, secret_key)
# 要上传的空间
bucket_name = 'flask-upload-test'
# 初始化BucketManager
bucket = BucketManager(q)
# key就是要删除的文件的名字
key = filename
ret, info = bucket.delete(bucket_name, key)
return info

调用执行实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 上传照片
@user_bp.route('/upload_photo', methods=['GET', 'POST'])
def upload_photo():
# 获取上传的内容
photo = request.files.get('photo') # FileStorage
# photo.filename,photo.save(path)
# 工具模块中封装方法
ret, info = upload_qiniu(photo)
if info.status_code == 200:
photo = Photo()
photo.photo_name = ret['key']
photo.user_id = session.get('uid')
db.session.add(photo)
db.session.commit()
return '上传成功!'
else:
return '上传失败!'

Flask-WTF

Web应用程序的一个重要方面是为用户提供用户界面。HTML提供了一个

标签,用于设计界面。可以适当地使用Form(表单)元素,例如文本输入,单选按钮,选择等。

用户输入的数据以Http请求消息的形式通过GET或POST方法提交给服务器端脚本。

  • 服务器端脚本必须从http请求数据重新创建表单元素。因此,实际上,表单元素必须定义两次 - 一次在HTML中,另一次在服务器端脚本中。
  • 使用HTML表单的另一个缺点是很难(如果不是不可能的话)动态呈现表单元素。HTML本身无法验证用户的输入。

这就是WTForms的作用,一个灵活的表单,渲染和验证库,能够方便使用。Flask-WTF扩展为这个WTForms库提供了一个简单的接口。

使用Flask-WTF,我们可以在Python脚本中定义表单字段,并使用HTML模板进行渲染。还可以将验证应用于WTF字段。

WTForms和Flask-WTF的关系

WTForms是一个Flask集成的框架,或者是说库。用于处理浏览器表单提交的数据。它在Flask-WTF 的基础上扩展并添加了一些随手即得的精巧的帮助函数,这些函数将会使在 Flask 里使用表单更加有趣。

Flask-WTF是集成WTForms,并带有 csrf 令牌的安全表单和全局的 csrf 保护的功能。每次我们在建立表单所创建的类都是继承与flask_wtf中的FlaskForm,而FlaskForm是继承WTForms中forms。

功能

  • 集成 wtforms。
  • 带有 csrf 令牌的安全表单。
  • 全局的 csrf 保护。
  • 支持验证码(Recaptcha)。
  • 与 Flask-Uploads 一起支持文件上传。
  • 国际化集成。

相关使用

首先,需要安装Flask-WTF扩展。

1
pip install flask-WTF

一丶创建基础表单

1. 基础表单

例如,form.py:

1
2
3
4
5
6
7
class UserForm(FlaskForm):
name = StringField(label='用户名', validators=[DataRequired(), Length(min=6, max=12, message='用户名长度必须在6~12位之间')])
password = PasswordField(label='密码', validators=[DataRequired(), Length(min=6, max=12, message='密码长度必须在6~12位之间')])
confirm_pwd = PasswordField(label='确认密码',
validators=[DataRequired(), Length(min=6, max=12, message='密码长度必须在6~12位之间'),
EqualTo('password', '两次密码不一致')])
phone = StringField(label='手机号码', validators=[DataRequired(), Length(min=11, max=11, message='手机号码必须11位长度')])

2.CSRF(跨站点请求伪造)保护

CSRF概念:CSRF跨站点请求伪造(Cross—Site Request Forgery),跟XSS攻击一样,存在巨大的危害性,你可以这样来理解:

攻击者盗用了你的身份,以你的名义发送恶意请求,对服务器来说这个请求是完全合法的,但是却完成了攻击者所期望的一个操作,比如以你的名义发送邮件、发消息,盗取你的账号,添加系统管理员,甚至于购买商品、虚拟货币转账等。 如下:其中Web A为存在CSRF漏洞的网站,Web B为攻击者构建的恶意网站,User C为Web A网站的合法用户。

CSRF攻击攻击原理及过程如下:

  1. 用户C打开浏览器,访问受信任网站A,输入用户名和密码请求登录网站A;

  2. 在用户信息通过验证后,网站A产生Cookie信息并返回给浏览器,此时用户登录网站A成功,可以正常发送请求到网站A;

  3. 用户未退出网站A之前,在同一浏览器中,打开一个TAB页访问网站B;

  4. 网站B接收到用户请求后,返回一些攻击性代码,并发出一个请求要求访问第三方站点A;

  5. 浏览器在接收到这些攻击性代码后,根据网站B的请求,在用户不知情的情况下携带Cookie信息,向网站A发出请求。网站A并不知道该请求其实是由B发起的,所以会根据用户C的Cookie信息以C的权限处理该请求,导致来自网站B的恶意代码被执行。

任何使用FlaskForm创建的表单发送请求,都会有CSRF的全部保护,需要在app中添加secret_key的值,否则报错,在对应的template中HTML渲染表单时,可以加入form.csrf_token:

1
2
3
4
# 全局使用csrf保护,
csrf = CSRFProtect(app=app)
# 必须需要设置SECRET_KEY这个配置项
app.config['SECRET_KEY'] = 'fgfhdf4564'
1
2
3
<form method="post">
{{ form.csrf_token }}
</form>

但是如果模板中没有表单,则可以使用一个隐藏的input标签加入csrf_token。

1
2
3
<form method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
</form>

3. 使用

视图中必须渲染form对象带页面

1
2
form =UserForm()
return render_template('user.html',form=form)

user.html中获取

1
2
3
4
5
6
<form action='' method=''>
{{form.csrf_token}}
{{form.name}}
{{form.password}}
<input type='submit' value=''/>
</form>

4. 提交验证

1
2
3
4
5
6
7
8
@app.route('/',methods=['GET','POST'])
def hello_world():
uform = UserForm()
if uform.validate_on_submit(): ------->主要通过validate_on_submit进行校验
print(uform.name)
print(uform.password)
return '提交成功!'
return render_template('user.html', uform=uform)

使用validate_on_submit 来检查是否是一个 POST 请求并且请求是否有效。

二丶文件上传

Flask-WTF 提供 FileField 来处理文件上传,它在表单提交后,自动从 flask.request.files 中抽取数据。FileFielddata 属性是一个 Werkzeug FileStorage 实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from werkzeug import secure_filename
from flask_wtf.file import FileField

# 获取项目的绝对路径
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
# 路径后面添加staitc目录
STATIC_DIR = os.path.join(BASE_DIR, 'static')
# 路径后面添加upload目录
UPLOAD_DIR = os.path.join(STATIC_DIR, 'upload')

class PhotoForm(Form):
# 上传使用的就是FileField,如果需要指定上传文件的类型需要使用:FileAllowed
photo = FileField(label='用户头像', validators=[FileRequired(), FileAllowed(['jpg', 'png', 'gif'], message='必须是图片文件格式')])

@app.route('/upload/', methods=('GET', 'POST'))
def upload():
form = PhotoForm()
if form.validate_on_submit():
filename = secure_filename(form.photo.data.filename)
form.photo.data.save(os.path.join(UPLOAD_DIR, filename))
else:
filename = None
return render_template('upload.html', form=form, filename=filename)

注意:在 HTML 表单的 enctype 设置成multipart/form-data,如下:

1
2
3
<form action="/upload/" method="POST" enctype="multipart/form-data">
....
</form>

三丶验证码

Flask-WTF 通过 RecaptchaField 也提供对验证码的支持:

1
2
3
4
5
6
7
from flask_wtf import Form, RecaptchaField
from wtforms import TextField

class SignupForm(Form):
# recaptcha = RecaptchaField()
# 因为使用的是google的,所以这里使用Python3图像处理库pillow
recaptcha = StringField(label='验证码') # 把图形验证码显示在后面

Pillow是PIL的一个派生分支,但如今已经发展成为比PIL本身更具活力的图像处理库。pillow可以说已经取代了PIL,将其封装成python的库(pip即可安装),且支持python2和python3,目前最新版本是3.0.0。

还需要配置一下信息:

字段 配置
RECAPTCHA_PUBLIC_KEY 必须 公钥
RECAPTCHA_PRIVATE_KEY 必须 私钥
RECAPTCHA_API_SERVER 可选 验证码 API 服务器
RECAPTCHA_PARAMETERS 可选 一个 JavaScript(api.js)参数的字典
RECAPTCHA_DATA_ATTRS 可选 一个数据属性项列表 https://developers.google.com/recaptcha/docs/display
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
app = Flask(__name__)
# csrf保护
app.config['SECRET_KEY'] = 'jfkdk73434kjfk3'
# 测试环境
app.config['ENV'] = 'testing'
# 必须 公钥
app.config['RECAPTCHA_PUBLIC_KEY'] = '6LeYIbsSAAAAACRPIllxA7wvXjIE411PfdB2gt2J'
# 必须 私钥
app.config['RECAPTCHA_PRIVATE_KEY'] = '6LeYIbsSAAAAAJezaIq3Ft_hSTo0YtyeFG-JgRtu'
# 可选 一个 JavaScript(api.js)参数的字典
app.config['RECAPTCHA_PARAMETERS'] = {'hl': 'zh', 'render': 'explicit'}
#可选 一个数据属性项列表
app.config['RECAPTCHA_DATA_ATTRS'] = {'theme': 'dark'}
# 初始化全局csrf保护
csrf = CSRFProtect(app=app)
# 初始化bootstrap
bootstrap = Bootstrap(app=app)
1. 安装
1
pip install pillow
2. utils.py工具类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import random
from PIL import Image, ImageFont, ImageDraw, ImageFilter

def get_random_color():
return (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))

def generate_image(length):
s = 'qwerQWERTYUIOtyuiopas123456dfghjklPASDFGHJKLZXCVBNMxcvbnm7890'
size = (130, 50)
# 创建画布
im = Image.new('RGB', size, color=get_random_color())
# 创建字体
font = ImageFont.truetype('font/方正正中黑简体.ttf', size=35)
# 创建ImageDraw对象
draw = ImageDraw.Draw(im)
# 绘制验证码
code = ''
for i in range(length):
c = random.choice(s)
code += c
draw.text((5 + random.randint(4, 7) + 25 * i, 1),
text=c,
fill=get_random_color(),
font=font)
# 绘制干扰线
for i in range(8):
x1 = random.randint(0, 130)
y1 = random.randint(0, 50 / 2)

x2 = random.randint(0, 130)
y2 = random.randint(50 / 2, 50)

draw.line(((x1, y1), (x2, y2)), fill=get_random_color())

im = im.filter(ImageFilter.EDGE_ENHANCE)
return im, code
if __name__ == '__main__':
generate_image(4)
页面user.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<p>
{{ uform.recaptcha.label }}:{{ uform.recaptcha }}
<img src="{{ url_for('get_image') }}" alt="" id="img">
</p>
<p>{% if uform.recaptcha.errors %}
{{ uform.recaptcha.errors.0 }}
{% endif %}
</p>

<script>
<!--点击图片刷新验证码-->
$('#img').click(function () {
console.log('-------')
$(this).attr('src', "{{ url_for('get_image') }}?ran=" + Math.random());
})
</script>

WTForms

基本了解

WTForms是一个Flask集成的框架,或者是说库。用于处理浏览器表单提交的数据。它在Flask-WTF 的基础上扩展并添加了一些随手即得的精巧的帮助函数,这些函数将会使在 Flask 里使用表单更加有趣。

用法:

1.field字段

WTForms支持HTML字段:

字段类型 说明
StringField 文本字段, 相当于type类型为text的input标签
TextAreaField 多行文本字段
PasswordField 密码文本字段
HiddenField 隐藏文本字段
DateField 文本字段, 值为datetime.date格式
DateTimeField 文本字段, 值为datetime.datetime格式
IntegerField 文本字段, 值为整数
DecimalField 文本字段, 值为decimal.Decimal
FloatField 文本字段, 值为浮点数
BooleanField 复选框, 值为True 和 False
RadioField 一组单选框
SelectField 下拉列表
SelectMultipleField 下拉列表, 可选择多个值
FileField 文件上传字段
SubmitField 表单提交按钮
FormFiled 把表单作为字段嵌入另一个表单
FieldList 子组指定类型的字段
2.Validators验证器

WTForms可以支持很多表单的验证函数:

验证函数 说明
Email 验证是电子邮件地址
EqualTo 比较两个字段的值; 常用于要求输入两次密钥进行确认的情况
IPAddress 验证IPv4网络地址
Length 验证输入字符串的长度
NumberRange 验证输入的值在数字范围内
Optional 无输入值时跳过其它验证函数
DataRequired 确保字段中有数据
Regexp 使用正则表达式验证输入值
URL 验证url
AnyOf 确保输入值在可选值列表中
NoneOf 确保输入值不在可选列表中

现在,我们将对联系表单中的name字段应用‘DataRequired’验证规则。

1
name = TextField("Name Of Student",[validators.Required("Please enter your name.")])

如果验证失败,表单对象的validate()函数将验证表单数据并抛出验证错误。Error消息将发送到模板。在HTML模板中,动态呈现error消息。

1
2
3
{% for message in form.name.errors %}
{{ message }}
{% endfor %}
3.自定义Validators验证器

第一种: in-line validator(内联验证器)
也就是自定义一个验证函数,在定义表单类的时候,在对应的字段中加入该函数进行认证。下面的my_length_check函数就是用于判name字段长度不能超过50.

1
2
3
4
5
6
def my_length_check(form, field):
if len(field.data) > 50:
raise ValidationError('Field must be less than 50 characters')

class MyForm(Form):
name = StringField('Name', [InputRequired(), my_length_check])

第二种:通用且可重用的验证函数,为常用的自定义Validators验证器

一般是以validate开头,加上下划线再加上对应的field字段(validate_filed),浏览器在提交表单数据时,会自动识别对应字段所有的验证器,然后执行验证器进行判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class RegistrationForm(FlaskForm):
email = StringField('Email', validators=[DataRequired(), Length(1, 60), Email()])
username = StringField('Username', validators=[DataRequired(), Length(1, 60),
Regexp('^[A-Za-z][A-Za-z0-9_.]*$', 0, 'username must have only letters, numbers dots or underscores')])
password = PasswordField('Password', validators=[DataRequired(), EqualTo('password2', message='password must match')])
password2 = PasswordField('Confirm password', validators=[DataRequired()])

def validate_email(self, field):
if User.objects.filter(email=field.data).count() > 0:
raise ValidationError('Email already registered')

def validate_username(self, field):
if User.objects.filter(username=field.data).count() > 0:
raise ValidationError('Username has exist')

第三种:比较高级的validators

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Length(object):
def __init__(self, min=-1, max=-1, message=None):
self.min = min
self.max = max
if not message:
message = u'Field must be between %i and %i characters long.' % (min, max)
self.message = message

def __call__(self, form, field):
l = field.data and len(field.data) or 0
if l < self.min or self.max != -1 and l > self.max:
raise ValidationError(self.message)

length = Length
4.Widget组件

下面可以以登录界面为实例:
login.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/usr/bin/env python
# -*- coding:utf-8 -*-
from flask import Flask, render_template, request, redirect
from wtforms import Form
from wtforms import validators
from wtforms import widgets

class LoginForm(Form):
name = simple.StringField(
label='用户名',
validators=[
validators.DataRequired(message='用户名不能为空.'),
],
widget=widgets.TextInput(),
render_kw={'class': 'form-control'}
)
pwd = simple.PasswordField(
label='密码',
validators=[
validators.DataRequired(message='密码不能为空.'),
],
widget=widgets.PasswordInput(),
render_kw={'class': 'form-control'}
)

总结

其实flask表单这一部分我们都是使用Flask-WTF与WTForms来进行实现,比如说做一下简单的系统登录界面、注册界面等等,可以利用它们来做一些小demo,你就可以深入理解它的原理并且可以强化flask的基础。

Flask 消息闪现

一个好的基于GUI的应用程序会向用户提供有关交互的反馈。例如,桌面应用程序使用对话框或消息框,JavaScript使用警报用于类似目的。

在Flask Web应用程序中生成这样的信息性消息很容易。Flask框架的闪现系统可以在一个视图中创建消息,并在名为next的视图函数中呈现它

Flask模块包含flash()方法。它将消息传递给下一个请求,该请求通常是一个模板。

1
flash(message, category)

其中,

  • message参数是要闪现的实际消息。
  • category参数是可选的。它可以是“error”,“info”或“warning”。

为了从会话中删除消息,模板调用get_flashed_messages()

1
get_flashed_messages(with_categories, category_filter)

两个参数都是可选的。如果接收到的消息具有类别,则第一个参数是元组。第二个参数仅用于显示特定消息。

以下闪现在模板中接收消息。

1
2
3
4
5
6
7
{% with messages = get_flashed_messages() %}
{% if messages %}
{% for message in messages %}
{{ message }}
{% endfor %}
{% endif %}
{% endwith %}

获取类型的闪现消息

view

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
# 验证是否是admin
username = request.form.get('username')
if username == 'admin':
flash('恭喜!验证成功啦!', 'info')
flash('哈哈哈', 'error')
flash(username, 'warning')
return redirect(url_for('index'))
else:
# app.logger.debug('这是一个debug测试')
# app.logger.error('这个是一个error测试')
app.logger.warning('这个是一个warning测试')
return render_template('login.html')

html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>测试</title>
<style>
.info {
color: deepskyblue;
}
.warning {
color: orange;
}
.error {
color: red;
}
</style>
</head>
<body>
{% with messages = get_flashed_messages(with_categories=True) %}
{% if messages %}
<ul>
{% for category, message in messages %}
<li class="{{ category }}">{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
{% endwith %}
</body>
</html>

Flask 日志

Python 自身提供了一个用于记录日志的标准库模块:logging

logging 模块

  • logging 模块定义的函数和类为应用程序和库的开发实现了一个灵活的事件日志系统
  • logging 模块是 Python 的一个标准库模块,由标准库模块提供日志记录 API 的关键好处是所有 Python 模块都可以使用这个日志记录功能。

logging 模块的日志级别

  • logging模块默认定义了以下几个日志等级,它允许开发人员自定义其他日志级别,但是这是不被推荐的,尤其是在开发供别人使用的库时,因为这会导致日志级别的混乱。

    • DEBUG 最详细的日志信息,典型应用场景是 问题诊断
    • INFO 信息详细程度仅次于DEBUG,通常只记录关键节点信息,用于确认一切都是按照我们预期的那样进行工作
    • WARNING 当某些不期望的事情发生时记录的信息(如,磁盘可用空间较低),但是此时应用程序还是正常运行的
    • ERROR 由于一个更严重的问题导致某些功能不能正常运行时记录的信息
    • FATAL/CRITICAL 整个系统即将/完全崩溃
  • 开发应用程序或部署开发环境时,可以使用 DEBUG 或 INFO 级别的日志获取尽可能详细的日志信息来进行开发或部署调试;

  • 应用上线或部署生产环境时,应该使用 WARNING 或 ERROR 或 CRITICAL 级别的日志来降低机器的I/O压力和提高获取错误日志信息的效率。

日志级别的指定通常都是在应用程序的配置文件中进行指定的。

logging 模块的使用方式介绍

  • loggers 提供应用程序代码直接使用的接口
  • handlers 用于将日志记录发送到指定的目的位置
  • filters 提供更细粒度的日志过滤功能,用于决定哪些日志记录将会被输出(其它的日志记录将会被忽略)
  • formatters 用于控制日志信息的最终输出格式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import logging

logger = logging.getLogger(__name__)
# 设置日志的记录等级
logger.setLevel(level=logging.INFO)
# 处理程序类到磁盘文件写入格式化的日志记录
handler = logging.FileHandler("log1.txt")
# 设置此处理程序的日志记录级别
handler.setLevel(logging.INFO)
# 创建日志记录的格式 时间 输入日志信息的文件名 日志等级 日志信息
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
# 为刚创建的日志记录器设置日志记录格式
handler.setFormatter(formatter)
# 为全局的日志工具对象(flask app使用的)添加日志记录器
logger.addHandler(handler)

logger.debug("Do something")
logger.info("Start print log")
logger.warning("Something maybe fail.")
logger.info("Finish")

使用logging提供的模块级别的函数记录日志

最简单的日志输出

先来试着分别输出一条不同日志级别的日志记录:

1
2
3
4
5
6
7
import logging

logging.debug("This is a debug log.")
logging.info("This is a info log.")
logging.warning("This is a warning log.")
logging.error("This is a error log.")
logging.critical("This is a critical log.")

也可以这样写:

1
2
3
4
5
logging.log(logging.DEBUG, "This is a debug log.")
logging.log(logging.INFO, "This is a info log.")
logging.log(logging.WARNING, "This is a warning log.")
logging.log(logging.ERROR, "This is a error log.")
logging.log(logging.CRITICAL, "This is a critical log.")

Flask-restful

rest api 是前后端分离最佳实践,是开发的一套标准或者说是一套规范,不是框架。

好处:
1、轻量,直接通过http,不需要额外的协议,通常有post/get/put/deletec操作。
2、面向资源,一目了然,具有自解释性
3、数据描述简单,一般通过json或者xml做数据通讯rest的概括

REST的名称”表现层状态转化”中,省略了主语。”表现层”其实指的是”资源”(Resources)的”表现层”。

所谓”资源”,就是网络上的一个实体,或者说是网络上的一个具体信息。它可以是一段文本、一张图片、一首歌曲、一种服务,总之就是一个具体的实在。你可以用一个URI(统一资源定位符)指向它,每种资源对应一个特定的URI。要获取这个资源,访问它的URI就可以,因此URI就成了每一个资源的地址或独一无二的识别符。

综合上面的解释,我们总结一下什么是RESTful架构:

(1)每一个URI代表一种资源;

(2)客户端和服务器之间,传递这种资源的某种表现层;

(3)客户端通过四个HTTP动词,对服务器端资源进行操作,实现”表现层状态转化”。

快速入门

步骤1. 安装扩展

1
pip  install flask-restful

步骤2. 在ext扩展包中__init__.py文件中定义对象

1
2
3
4
5
from flask_restful import Api
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()
api = Api()

步骤3. 在app包中__init__.py文件中,使用api对象与flask中app对象绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from flask import Flask

from apps.user.view import user_bp
from exts import db, api
from settings import DevelopmentConfig

def create_app():
app = Flask(__name__)
app.config.from_object(DevelopmentConfig)
# db对象与app对象绑定
db.init_app(app=app)
# api对象与app对象绑定
api.init_app(app=app)
# 注册蓝图
app.register_blueprint(user_bp)
print(app.url_map)
return app

步骤4. 定义模型对象

1
2
3
4
5
6
7
8
9
10
import datetime

from exts import db


class User(db.Model):
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
username = db.Column(db.String(15), nullable=False)
password = db.Column(db.String(12), nullable=False)
udatetime = db.Column(db.DateTime, default=datetime.datetime.now)

步骤5. 初始化数据库.生成表

在虚拟环境下执行命令

1
2
3
python app.py db init  # 创建新的迁移存储库,生成migrations文件夹
python app.py db migrate # 生成迁移版本文件
python app.py db upgrade # 同步到数据库,生成表

步骤6. 在view中定义类视图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
from flask import Blueprint
from flask_restful import Resource, marshal_with, fields

from apps.user.model import User
from exts import api

user_bp = Blueprint('user', __name__, url_prefix='/api')

# 默认情况下,在你的返回迭代中所有字段将会原样呈现。
# 尽管当你刚刚处理 Python 数据结构的时候,觉得这是一个伟大的工作,
# 但是当实际处理它们的时候,会觉得十分沮丧和枯燥。为了解决这个问题,
# Flask-RESTful 提供了 fields 模块和 marshal_with() 装饰器。
# 类似 Django ORM 和 WTForm,你可以使用 fields 模块来在你的响应中格式化结构。
user_fields = {
'id': fields.Integer,
'username': fields.String,
'password': fields.String,
'udatetime': fields.DateTime
}

# 定义类视图
class UserResource(Resource):
# 上面的例子接受一个 python 对象并准备将其序列化。
# marshal_with() 装饰器将会应用到由 resource_fields 描述的转换。
# 从对象中提取的唯一字段是 task。fields.Url 域是一个特殊的域,
# 它接受端点(endpoint)名称作为参数并且在响应中为该端点生成一个 URL。
# 许多你需要的字段类型都已经包含在内。
# get 请求的处理
@marshal_with(user_fields)
def get(self):
users = User.query.all()
# userList = []
# for user in users:
# userList.append(user.__dict__)
return users
# post
def post(self):
return {'msg': '------>post'}
# put
def put(self):
return {'msg': '------>put'}
# delete
def delete(self):
return {'msg': '------>delete'}

api.add_resource(UserResource, '/user')

步骤7. 类视图与资源绑定,及和类视图绑定

1
api.add_resource(UserResource, '/user')

请求参数解析

尽管 Flask 能够简单地访问请求数据(比如查询字符串或者 POST 表单编码的数据),验证表单数据仍然很痛苦。Flask-RESTful 内置了支持验证请求数据,它使用了一个类似 argparse 的库。

步骤1. 创建RequestParser对象

1
2
3
4
from flask.ext.restful import reqparse

parser = reqparse.RequestParser(bundle_errors=True) # 解析对象
- bundle_errors=True 错误处理为True,错误全部返回,默认为false,遇到错误返回

步骤2.给解析器添加参数

通过parser.add_argument('名字',type=类型,required=是否必须填写,help=错误的提示信息,location=表明获取的位置form就是post表单提交)

注意在type的位置可以添加一些正则的验证等。

inputs 模块提供了许多的常见的转换函数,比如 inputs.date()inputs.url()

1
2
3
4
5
6
parser.add_argument('username', type=str, required=True, help='必须输入用户名', location=['form'])
parser.add_argument('password', type=inputs.regex(r'^\d{6,12}$'), required=True, help='必须输入6~12位数字密码',location=['form'])
parser.add_argument('phone', type=inputs.regex(r'^1[356789]\d{9}$'), location=['form'], help='手机号码格式错误')
# 如果是多选框添加 action='append'
parser.add_argument('hobby', action='append') # ['篮球', '游戏', '旅游']
parser.add_argument('icon', type=FileStorage, location=['files'])

步骤3.在请求的函数中获取数据

可以在get,post,put等中获取数据,通过parser对象.parse_args()

1
2
3
4
args = parser.parse_args()
# args是一个字典底层的结构中,因此我们获取具体的数据时可以通过get
username = args.get('username')
password = args.get('password')

lr_parser = parser.copy()

lr_parser.add_argument(‘code’, type=inputs.regex(r’^\d{4}$’), help=’必须输入四位数字验证码’, required=True,location=’form’)

parser.copy():相当于类型中的继承

响应数据格式化

默认情况下,在你的返回迭代中所有字段将会原样呈现。如果使用restful接口的方式自定义对象返回时,需要转换成dist对象,即return data必须是符合json格式。为了解决这个问题,Flask-RESTful 提供了 fields 模块和 marshal_with() 装饰器结合使用。你可以使用 fields 模块来在你的响应中格式化结构。

需要定义字典,字典的格式就是给客户端看的格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
user_fields = {
'id': fields.Integer,
'username': fields.String(default='匿名'),
'pwd': fields.String(attribute='password'),
'udatetime': fields.DateTime(dt_format='rfc822')
}

# 定义类视图
class UserResource(Resource):
# marshal_with() 装饰器将会应用到由user_fields描述的转换
@marshal_with(user_fields)
def post(self):
# 获取数据
args = parser.parse_args()

username = args.get('username')
password = args.get('password')
phone = args.get('phone')
bobby = args.get('hobby')
print(bobby)
icon = args.get('icon')
print(icon)
# 创建user对象
user = User()
user.username = username
user.password = password
if icon:
upload_path = os.path.join(Config.UPLOAD_ICON_DIR, icon.filename)
icon.save(upload_path)
# 保存路径个
user.icon = os.path.join('upload/icon/', icon.filename)
if phone:
user.phone = phone
db.session.add(user)
db.session.commit()

return user

api.add_resource(UserResource, '/user', endpoint='all_user')

客户端(前端)能看到的是:id,username,pwd,udatetime这四个key,默认key的名字是跟model中的模型属性名一致,如果不想让前端看到命名,则可以修改。但是必须结合attribute='模型的字段名',如果不想让页面显示为空,还可以设置默认值default='匿名'

自定义fields和Url

  1. 必须继承Raw
  2. 重写方法format()
  3. 在fields模块中使用 'isDelete': IsDelete(attribute='isdelete')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class IsDelete(fields.Raw):
def format(self, value):
# print('------------------>', value)
return '删除' if value else '未删除'

user_fields_1 = {
'id': fields.Integer,
'username': fields.String(default='匿名'),
'uri': fields.Url('single_user', absolute=True) # 参数使用的就是endpoint的值
}

user_fields = {
'id': fields.Integer,
'username': fields.String(default='匿名'),
'pwd': fields.String(attribute='password'),
'isDelete': fields.Boolean(attribute='isdelete'),
'isDelete1': IsDelete(attribute='isdelete'),
'udatetime': fields.DateTime(dt_format='rfc822'),
}
# 定义类视图
class UserResource(Resource):
@marshal_with(user_fields1)
def get(self):
# 获取数据
args = parser.parse_args()

return user

class UserSimpleResource(Resource):
@marshal_with(user_fields) # user转成一个序列化对象,
def get(self, id):
user = User.query.get(id)
return user # 不是str,list,int,。。。


# 涉及endpoint的定义:
api.add_resource(UserSimpleResource, '/user/<int:id>', endpoint='single_user')

返回复杂的结构对象

返回的对象必须符合Json格式

形如:对象里面嵌套了列表对象,列表对象中又嵌套了一个列表对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
'aa':10,
'bb':[
{
'id':1,
'xxxs':[
{},{}
]
},
{

}
]
}

注意:如果直接返回不能有自定义的对象,如果有这种对象,需要:marchal(),marchal_with()帮助进行转换。

marchal()

marchal(对象,对象的fields格式) # 对象的fields格式是指字典的输出格式

marchal([对象,对象],对象的fields格式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
user_fields = {
'id': fields.Integer,
'username': fields.String(default='匿名'),
'pwd': fields.String(attribute='password'),
'isDelete': fields.Boolean(attribute='isdelete'),
'isDelete1': IsDelete(attribute='isdelete'),
'udatetime': fields.DateTime(dt_format='rfc822'),
'uri': fields.Url('single_user', absolute=True)
}



user_friend_fields = {
'username': fields.String,
'nums': fields.Integer,
'friends': fields.List(fields.Nested(user_fields))
}

class UserFriendResource(Resource):

@marshal_with(user_friend_fields)
def get(self, id):
friends = Friend.query.filter(Friend.uid == id).all()
user = User.query.get(id)

friend_list = []
for friend in friends:
u = User.query.get(friend.fid)
friend_list.append(u)

data = {
'username': user.username,
'nums': len(friends),
'friends': marshal(friend_list,user_fields) # [user,user,user]
}
return data

api.add_resource(UserResource, '/user', endpoint='all_user')
api.add_resource(UserSimpleResource, '/user/<int:id>', endpoint='single_user')
api.add_resource(UserFriendResource, '/friend/<int:id>', endpoint='user_friend')

@marchal_with()

@marchal_with(user_friend_fields) 作为装饰器修饰请求方法,在user_friend_fields字典对象中定义

'friends': fields.List(fields.Nested(user_fields)),在fields.Nested()把自定义对象在进行数据格式化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
user_fields = {
'id': fields.Integer,
'username': fields.String(default='匿名'),
'pwd': fields.String(attribute='password'),
'isDelete': fields.Boolean(attribute='isdelete'),
'isDelete1': IsDelete(attribute='isdelete'),
'udatetime': fields.DateTime(dt_format='rfc822'),
'uri': fields.Url('single_user', absolute=True)
}



user_friend_fields = {
'username': fields.String,
'nums': fields.Integer,
'friends': fields.List(fields.Nested(user_fields))
}

class UserFriendResource(Resource):

@marshal_with(user_friend_fields)
# 函数需要参数id,就是参数最终数据输出的格式
# 列如使用路径传参/user/1,及id=1
def get(self, id):
friends = Friend.query.filter(Friend.uid == id).all()
user = User.query.get(id)

friend_list = []
for friend in friends:
u = User.query.get(friend.fid)
friend_list.append(u)

data = {
'username': user.username,
'nums': len(friends),
'friends': friend_list # [user,user,user]
}
return data

api.add_resource(UserResource, '/user', endpoint='all_user')
api.add_resource(UserSimpleResource, '/user/<int:id>', endpoint='single_user')
api.add_resource(UserFriendResource, '/friend/<int:id>', endpoint='user_friend')

Flask-restful-API跨域问题

跨域问题来源于JavaScript的”同源策略”,即只有 协议+主机名+端口号 (如存在)相同,则允许相互访问。也就是说JavaScript只能访问和操作自己域下的资源,不能访问和操作其他域下的资源。

跨域问题是针对JS和ajax的,html本身没有跨域问题。

后端解决跨域问题

第一种解决方案

步骤1. 使用第三方扩展

1
pip install flask-cors

步骤2. 在扩展包ext__init__.py文件中创建cors对象

1
2
3
from flask_cors import CORS

cors= CORS()

步骤3. 与flask对象app进行绑定初始化

1
2
3
cors.init_app(app=app,supports_credentials=True)

# supports_credentials 证书认证

第二种解决方案

1
2
3
4
5
response = make_response()
response.headers['Access-Control-Allow-Origin']='*'
response.headers['Access-Control-Allow-Methods']='GET,POST'
response.headers['Access-Control-Allow-Headers']='x-request-with,Content-type'
return response

Flask-restful-API与蓝图的使用

步骤1.声明蓝图对象

1
user_bp = Blueprint('user', __name__)

步骤2.蓝图对象与Api进行绑定

1
api = Api(user_bp)

步骤3.注册蓝图,与flask对象进行绑定

1
app.register_blueprint(user_bp)

步骤4.声明资源视图

1
2
3
4
5
6
7
8
9
class xxxxApi(Resource):
def get(self):
pass
def post(self):
pass
def put(self):
pass
def delete(self):
pass

步骤5.资源与api进行绑定

1
api.add_resource(xxxxApi,'/xxxx')