flask 入门笔记


flask 入门笔记

一. 安装 flask

关于 flask 的背景不多谈了,首先要注意 flask 的安装问题,由于 python3 与 flask 的部分模块并不能很好的兼容,所以推荐使用 python 2.7 编写 flask 的程序。
其次是 python 虚拟化的问题,由于 flask 的模块版本众多,如果将他们全部装在本机的话会相当混乱,而且当某些模块更新之后可能会发生各种各样的问题,所以推荐使用 virtualenv 创建一个独立的 python 环境,防止在开发过程中出现一些错误。
具体的安装步骤按照官网来就可以了,一般不会出什么问题。

1
2
3
4
5
6
sudo pip install virtualenv
mkdir myproject
cd myproject
virtualenv venv
. venv/bin/activate
pip install Flask

二. 第一个 flask 程序

首先要选择一个顺手的 IDE ,推荐 pycharm 因为它的智能提示非常好用。
在使用 pycharm 新建项目的时候,要注意切换 python 解释器路径为刚刚创建的虚拟化 python 路径。

第一个 flask 程序

1
2
3
4
5
6
7
8
9
10
11
12
13
# coding=utf-8
from flask import Flask # 导入 flask

app = Flask(__name__) # 实例化 flask,其中可以使用括号内的参数指定资源所在的路径


@app.route("/") # 使用装饰器确定路由
def index(): # 确定路由之后实现具体的函数
return "Hello World!"


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

首先在代码中导入 flask,然后实例化 flask,在实例化时,括号内的参数可以设置很多属性,大部分用来指定各种资源的路径。
接着需要使用装饰器告诉 flask 具体的路由信息,装饰器后面紧跟着对应的函数,比如上面的代码,当访问 “/“ 时,就会运行 index 函数。
最后在主函数中使用 run 函数,将 flask 实例运行在一个简易的服务器上面,如果运行这个脚本,会在终端中打印出一个本地网址,用浏览器访问即可看到效果。

另外,设置路由的时候要注意后面是否要跟着 ‘/‘,例如

1
2
3
@app.route("/login")

@app.route("/login/")

这两种设置方式存在差异,按照第一种方式设置时,如果用户访问了 http://xxx/login/ 网页会返回 404 错误,但是如果按照第二种方式设置,如果用户访问了 http://xxx/login 会重定向到 http://xxx/login/。

调试模式

如果没有启用调试模式,当修改程序的代码之后,需要手动重启程序,这样比较麻烦,因此,我们可以启用调试模式,在调试模式下,如果程序的代码发生变动,会自动使变动生效,不需要再手动重启程序了。不过需要注意的是,当新增、修改的代码出现错误时,程序还是会自动关闭。
启用调试模式的方式

1
app.run(debug=True)

调试模式不能在生产环境中启用!

路由请求限定方式

现在我们的程序可以使用 GET 请求访问,但是如果换成 POST 或者其他请求,就会返回 405 错误,如果想要网页支持其他请求方式的话,需要手动增加。
在装饰器中增加 methods 参数即可

1
2
# 让页面接受 GET 和 POST 请求
@app.route("/", methods=['GET', 'POST'])

可变的 URL

一般的网页中肯定存在可变的 URL,那么在 flask 中怎么实现呢?
很简单,在设置路由的时候使用 <可变参数名> 即可。
一个简单的例子

1
2
3
@app.route("/search/<id>")
def search(id):
return "ID: %s" % id

这样,在访问路由 /search 时,就可以在后面添加可变的参数了。
如果想要限制用户的输入必须是数字的话,可以在尖括号中添加变量前缀,例如

1
@app.route("/search/<int:id>")

若用户输入的是除了数字以外的内容,会返回 404 错误。
常用的变量前缀有 float、int、path

构造 URL

硬编码的 url 虽然简单,但是有时并不方便,我们可以使用 url_for 构建 url。例如

1
2
3
4
5
6
7
@app.route("/order/<int:id>", methods=['GET', 'POST'])    
def func(id):
return "Your ID is %d" % id

@app.route("/conurl") # 构造 url
def conurl():
return url_for("func", id="9999")

当访问 http://xxx/conurl 时,会返回 /order/9999

静态文件

web 页面经常需要使用 CSS、js 等静态文件实现各种排版、特效,在 flask 开发中,静态文件通常存放在 static 目录下,这样,可以使用

1
url_for("static", filename="xxx")

方便的生成静态文件的 url。

模板

flask 的两大核心之一,flask 使用 JinJa2 作为模板渲染引擎,具体的使用方法要参照 JinJa2 的手册。
使用模板引擎的好处是开发者不需要自己实现模板引擎,包括例如安全问题、兼容问题等,我们只需要使用已经成熟的开发框架即可达到理想的效果。

数据库

web 开发中一个非常重要的概念,没有数据库的程序能实现的功能非常局限。
flask 中可以使用 flask_SQLAlchemy 模块实现数据库功能,它将具体的 SQL 语句封装在底层,用户可以操作更为抽象的 python 对象,使得 web 开发变得十分简单高效。

三. 一个非常简单的 flask 实例

在本实例中,我编写了一个非常简单的 flask 程序,它模拟了一个图书管理系统,用户可以输入作者和书名,并添加到现有的数据库中。

首先要明确开发的流程,简单的描述为下面的步骤

1
2
3
4
5
6
7
1. 导入各种模块,新建对应页面的模板,做好准备。
2. 新建数据库,编写数据库模型,确定好各个字段以及它们的关系。
3. 实例化数据库,在终端中新建数据库。
4. 编写 WTF 表单类。
5. 在模板中实现 WTF 表单。
6. 为程序添加对应的逻辑。
7. 添加数据,测试功能

我们需要的模块有 flask 模块,sqlalchemy 模块,WTF 模块。

首先使用 pycharm 新建一个 flask 项目,这会自动生成一些代码,将所需要的模块都导入进去(SQL、WTF 等模块需要使用 pip 手动安装):

1
2
3
4
5
6
7
8
# coding=utf-8
from flask import Flask, render_template, request, flash
from flask_sqlalchemy import SQLAlchemy # 导入 sql 模块
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired

app = Flask(__name__)

这些就是接下来会用到的模块,接着在 template 目录下新建一个模板,例如名字是 library.html
这样准备工作就做完了。

下面需要编写数据库有关的代码,在编写具体代码之前,需要对整个项目的配置进行修改,添加如下代码:

1
2
3
app.config['SQLALCHEMY_DATABASE_URI'] = "mysql://root:mysql@127.0.0.1:3306/flask_books"
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.secret_key = "CATALPA"

第一项指出了 mysql 数据库的地址,注意数据库的用户名和密码不要填错,3306这个端口号可以省略。
第二项将 sqlalchemy 的动态跟踪修改设置为关闭,一般不推荐开启,会影响数据库的性能,并且这个选项会在未来的版本中移除。
第三项设置了 secret_key,在后面 flash 消息时会用到。
编写数据库代码,首先要实例化数据库,仅需要一句代码就可以搞定

1
db = SQLAlchemy(app)

接下来是创建数据库模型,任何数据库模型都需要继承自 db.Model 这个基类,类内部的代码需要指明表名,并且设置各种字段,和关系引用(如果有)。在本例中,数据库中仅需要两张表,一张是作者表,另一张则是书籍表,并且两者存在关系引用。以作者的数据库模型为例

1
2
3
4
5
6
7
8
9
10
11
class Author(db.Model):
# 表名
__tablename__ = "authors"
# 两个字段
id = db.Column(db.Integer, primary_key=True) # 主键
name = db.Column(db.String(16), unique=True) # 作者名要唯一
# 添加关系引用,books 是给自己用的,而反向引用是给另外一个模型用的
books = db.relationship('Book', backref='author')

def __repr__(self): # 在调用类时打印有关消息
return "%s" % self.name

需要特别解释一下关系引用,,有些情况下,我们需要通过一张表的某个字段找到另一张表的字段,如果没有关系引用,可能要查询很多次,降低的数据库的性能,但是又不能直接将两张表合成一张,这时就需要使用关系引用了,它就相当于给表添加了属性,但是又没有实际向表中增加了数据。
例如原来的数据库模型大致如下
image
如果在 books 表中想要查到某书的作者,则需要先获取 author_id 然后到 authors 表中再查询作者的姓名,当添加了关系引用之后表就会变成这样
image
但是红色部分实际上并没有真实的存储在表中,它类似 C 语言的指针,指向另一张表。现在再去查找效率就很高了。
代码中的关系引用实现的就是这种效果,使用 db.relationship() 声明一个关系引用,第一个参数指向需要引用的表,backref 表示反向引用,就是另一张表反过来引用当前表,它的参数表示另一张表中的字段名。

Book 的数据库模型如下

1
2
3
4
5
6
7
8
9
class Book(db.Model):
__tablename__ = "books"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(16), unique=False)
# 设置 author_id 为外键,指向 authors 模型
author_id = db.Column(db.Integer, db.ForeignKey('authors.id'))

def __repr__(self):
return "%s" % self.name

其他都很类似,需要注意 author_id 设置为外键,原因是它实际上就是 authoirs 的 id 字段。

代码中的数据库已经实现完毕,下面需要打开系统的终端,在 mysql 中创建这个数据库

1
2
3
mysql -u<用户名> -p<密码>  注意两个选项和参数中间没有空格
create database flask_books charset=utf8
use flask_books;

这样数据库就创建完成了。

接下来是创建 WTF 表单,flask_WTF 也是 flask 的一个模块,它集成了 web 程序需要使用的表单,使用这种成熟模块的好处是提高开发效率,并且一些安全、运行速度等问题不再需要我们去考虑了。

首先编写表单类,设置表单包括几个元素。

1
2
3
4
5
# 自定义表单类
class SearchForm(FlaskForm):
name = StringField(u"作者:", validators=[DataRequired()])
book_name = StringField(u"书籍:", validators=[DataRequired()])
submit = SubmitField(u"添加")

所有 WTF 表单类都需要继承自 FlaskForm 基类,我们声明了三个表单元素,第一个是作者的名字,第二个是书名,第三个是提交按钮,前两个都是字符串类型的数据,所以使用 StringField,第一个参数是表单的名字,未来会显示到网页上面,第二个参数接受一个函数列表,可以在其中添加表单的验证信息,例如这里就要求表单不能为空。

定义好表单类之后,我们先定义一下视图函数,和上面一样,使用 @app.route() 设置路由,然后编写视图函数

1
2
3
4
@app.route("/library", methods=['GET', 'POST'])
def library():
form = SearchForm()
return render_template("library.html", form=form)

首先实例化了 WTF 表单,然后将表单作为参数传递给模板进行渲染。在模板端要编写下面的代码

1
2
3
4
5
6
7
<form method="post" style="text-align: center">
{{ form.csrf_token() }} {# 声明 csrf token #}
{{ form.name.label }}{{ form.name }}<br> {# 作者名表单 #}
{{ form.book_name.label }}{{ form.book_name }}<br> {# 书名表单 #}
{{ form.submit }}<br> {# 提交按钮 #}

</form>

使用 html 的 form 标签声明一个表单,并且设置居中,然后设置各个表单。

为程序添加具体的逻辑:基本的设置已经完成了,下面开始为程序添加逻辑实现目标功能,首先是处理 WTF 表单的逻辑,我们希望用户输入一个作者名字以及书名,然后将它添加到现有的数据库中,不同作者可以有相同的书名,但是同一个作者不能有两本书名相同,如果有新的作者,就把新的作者也添加到数据库中。
显然要和数据库打交道了,首先我们要获取用户的输入,在提交表单时,浏览器会采用 post 请求,所以先来判断一下用户的请求是否为 post,如果是,就使用 api 获取用户在表单中的输入

1
2
3
4
if request.method == 'POST':
if form.validate_on_submit(): # 一句话对于表单进行有效性验证(DataRequired)
author_name = form.name.data # 获取用户输入
book_name = form.book_name.data

然后从数据库中查询作者是否已经存在,如果存在,再判断书籍是否存在

1
2
3
4
5
author = Author.query.filter_by(name=author_name).first()
if author:
book = Book.query.filter_by(name=book_name, author_id=author.id).first()
if book:
flash(u"书籍已存在!")

flash 是消息闪现,它的作用是向模板传递一个即时的消息,如果想在模板中打印这条消息,需要使用一个循环

1
2
3
{% for message in get_flashed_messages() %}
{{ message }}<br>
{% endfor %}

如果书籍不存在,就把用户的输入存入数据库中

1
2
3
4
else:
book_x = Book(name=book_name, author_id=author.id)
db.session.add(book_x)
db.session.commit()

当想要向数据库存储数据时,需要使用 db.session.add() 先添加模型实例,然后 db.session.commit() 向数据库提交修改。

另外,如果作者不存在,那么就新建一个作者

1
2
3
4
5
6
7
else:
author_x = Author(name=author_name)
db.session.add(author_x)
db.session.commit()
book_y = Book(name=book_name, author_id=author_x.id)
db.session.add(book_y)
db.session.commit()

整理这个函数如下

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
@app.route("/library", methods=['GET', 'POST'])
def library():
# 从数据库中查出所有的作者信息
authors = Author.query.all()
form = SearchForm()
if request.method == 'POST':
if form.validate_on_submit():
author_name = form.name.data
book_name = form.book_name.data
author = Author.query.filter_by(name=author_name).first()
if author:
book = Book.query.filter_by(name=book_name, author_id=author.id).first()
if book:
flash(u"书籍已存在!")
else:
book_x = Book(name=book_name, author_id=author.id)
db.session.add(book_x)
db.session.commit()
else:
author_x = Author(name=author_name)
db.session.add(author_x)
db.session.commit()
book_y = Book(name=book_name, author_id=author_x.id)
db.session.add(book_y)
db.session.commit()

else:
flash(u"参数错误!")
return render_template("library.html", authors=authors, form=form)

最后让我们添加一些数据,在主函数里面写代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if __name__ == '__main__':
db.drop_all() # 运行程序之后,先清空数据库
db.create_all() # 然后重新创建数据库,这两步操作防止数据库冗余
author1 = Author(name="CataLpa")
author2 = Author(name="Admin")
author3 = Author(name="Jack")
db.session.add_all([author1, author2, author3])
db.session.commit()

book1 = Book(name="加密与解密", author_id=author1.id)
book2 = Book(name="Flask入门经典", author_id=author2.id)
book3 = Book(name="漏洞战争", author_id=author3.id)
book4 = Book(name="C#入门经典", author_id=author3.id)
db.session.add_all([book1, book2, book3, book4])
db.session.commit()
app.run(host="0.0.0.0", debug=True)

最后启动这个小程序,访问本机的 5000 端口

image

添加一本书、新建一个作者试试:

image

四. 小总结

用 flask 的基础知识实现了一个很简单的小程序,在学习 flask 的过程中了解到了许多 web 程序的设计方法。

着重学习了数据库和表单的有关操作,还有许多东西需要学习,例如如何实现用户的注册和登录页面,如何保持用户的会话,包括前端的设计(这个程序实在是太丑了…)安全性问题等等。

五.代码

flask_book.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
from flask import Flask, render_template, request, flash
from flask_sqlalchemy import SQLAlchemy # 导入 sql 模块
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired

app = Flask(__name__)
# 配置数据库: 数据库的地址/关闭动态跟踪修改
app.config['SQLALCHEMY_DATABASE_URI'] = "mysql://root:19991220@127.0.0.1:3306/flask_books"
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.secret_key = "CATALPA"
# 第一步: 创建数据库 --> 需要在终端中手动创建数据库
db = SQLAlchemy(app)


# 第二步: 创建数据库模型,模型需要继承自 db.Model
class Author(db.Model):
# 表名
__tablename__ = "authors"
# 两个字段
id = db.Column(db.Integer, primary_key=True) # 主键
name = db.Column(db.String(16), unique=True)
# 添加关系引用,books 是给自己用的,而反向引用是给另外一个模型用的
books = db.relationship('Book', backref='author')

def __repr__(self):
return "%s" % self.name


class Book(db.Model):
__tablename__ = "books"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(16), unique=False)
# 设置 author_id 为外键,指向 authors 模型
author_id = db.Column(db.Integer, db.ForeignKey('authors.id'))

def __repr__(self):
return "%s" % self.name


# 自定义表单类
class SearchForm(FlaskForm):
name = StringField(u"作者:", validators=[DataRequired()])
book_name = StringField(u"书籍:", validators=[DataRequired()])
submit = SubmitField(u"添加")


@app.route("/library", methods=['GET', 'POST'])
def library():
# 从数据库中查出所有的作者信息
authors = Author.query.all()
form = SearchForm()
if request.method == 'POST':
if form.validate_on_submit():
author_name = form.name.data
book_name = form.book_name.data
author = Author.query.filter_by(name=author_name).first()
if author:
book = Book.query.filter_by(name=book_name, author_id=author.id).first()
if book:
flash(u"书籍已存在!")
else:
book_x = Book(name=book_name, author_id=author.id)
db.session.add(book_x)
db.session.commit()
else:
author_x = Author(name=author_name)
db.session.add(author_x)
db.session.commit()
book_y = Book(name=book_name, author_id=author_x.id)
db.session.add(book_y)
db.session.commit()

else:
flash(u"参数错误!")
return render_template("library.html", authors=authors, form=form)


if __name__ == '__main__':
db.drop_all()
db.create_all()
author1 = Author(name="CataLpa")
author2 = Author(name="Admin")
author3 = Author(name="Jack")
db.session.add_all([author1, author2, author3])
db.session.commit()

book1 = Book(name="加密与解密", author_id=author1.id)
book2 = Book(name="Flask入门经典", author_id=author2.id)
book3 = Book(name="漏洞战争", author_id=author3.id)
book4 = Book(name="C#入门经典", author_id=author3.id)
db.session.add_all([book1, book2, book3, book4])
db.session.commit()
app.run(host="0.0.0.0", debug=True)

library.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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Library</title>
<style>
h1 {text-align: center; background-color: aqua;}
</style>
</head>
<body style="background-color: darkcyan">

<h1>Welcome!</h1>

<hr>
<form method="post" style="text-align: center">
{{ form.csrf_token() }}
{{ form.name.label }}{{ form.name }}<br>
{{ form.book_name.label }}{{ form.book_name }}<br>
{{ form.submit }}<br>
{% for message in get_flashed_messages() %}
{{ message }}<br>
{% endfor %}

</form>

<p>
{% for author in authors %}
<li>{{ author }}</li>
<ul>
{% for book in author.books %}
<li>{{ book }}</li>
{% endfor %}

</ul>
{% endfor %}
</p>

</body>
</html>