flask框架学习

flask运行及调试

WSGI介绍

1
https://blog.csdn.net/on_1y/article/details/18803563

一个最小的应用

一个最小的应用看起来像这样,新建 hello.py 文件,并向其中写入如下代码:

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

@app.route('/')
def hello_world():
return 'Hello, World!'

那么这段代码做了什么?

首先我们导入了类 Flask。这个类的实例化将会是我们的 WSGI 应用。
接着,我们创建一个该类的实例。第一个参数是应用模块或包的名称,这样 Flask 才会知道去哪里寻找模板、静态文件等等。如果你使用的是单一的模块(就如本例),第一个参数应该使用

1
2
3
4
5
我们使用装饰器route()告诉 Flask 哪个URL才能触发我们的函数。
定义一个函数,该函数名也是用来给特定函数生成 URLs,并且返回我们想要显示在用户浏览器上的信息。
使用 Python 解释器运行这个文件,注意这个文件不能取名为flask.py,因为这会与 Flask 本身冲突。

运行这个应用既可以使用 flask 命令行也可以使用 Python 的 -m 调用 flask,在运行之前你需要设置 FLASK_APP 的环境变量来告诉终端需要运行哪个应用,在终端执行如下命令:

export FLASK_APP=hello.py
flask run

1
2
3
4
5
6
7

现在使用浏览器浏览http://127.0.0.1:5000/,将会看到页面上的 Hello, World!。


当你运行服务器,你会注意到它只能从你自己的计算机上访问,网络中其它任何的地方都不能访问。这是因为默认情况下是 debug 模式,只有应用中的一个用户可以执行你计算机上的任意 Python 代码。

如果你关闭 debug 或者信任你所在网络上的用户,可以让你的服务器对外公开可用,只需要在命令行中添加参数 --host=0.0.0.0:

flask run –host=0.0.0.0

1
2
3
4
5
6
7
8
这让你的操作系统去监听所有公开的 IP。

调试模式
使用 flask 命令行可以非常方便的启动一个本地开发服务器,但是每次修改代码后你都需要手动重启服务器。通过前面的启动后输出显示可以发现 Environment 为 production,同时调试模式未开启 Debug mode: off。

这样做并不好,Flask 能做得更好。如果启用了调试支持,在代码修改后服务器能够自动重载,并且如果发生错误,它会提供一个有用的调试器。

为了让所有的开发者特征可用(包括调试模式),在运行服务器之前可以设置 FLASK_ENV 环境变量为 development:

$ export FLASK_ENV=development
$ export FLASK_DEBUG=1
$ flask run

1
2
3
4
5
6
7
8
9
10
11
12
13
上述命令做了以下几件事:

使调试器(debugger)可用
启动了代码改变自动的热加载
在 flask 应用中开启了 debug 模式
注意
尽管交互式调试器(debugger)不能在分叉(forking)环境下工作(这使得它几乎不可能在生产服务器上使用),它依然允许执行任意代码。这使它成为一个巨大的安全风险,因此它绝对不能用于生产环境。

运行中的调试器的截图,从截图可以看出在页面上有终端可以执行交互式命令:


# 路由
route 装饰器是用于把一个函数绑定到一个 URL 上。

from flask import Flask
app = Flask(name)

如果访问 /,返回 Index Page
@app.route(‘/‘)
def index():
return ‘Index Page’

如果访问 /hello,返回 Hello, World!
@app.route(‘/hello’)
def hello():
return ‘Hello, World!’

1
2
3
4
访问地址 http://127.0.0.1:5000,浏览器页面会显示 Index Page;
如果访问地址 http://127.0.0.1:5000/hello,浏览器页面会显示 Hello, World!。这样就实现了通过访问不同的 URL 地址从而响应不同的页面。
为了给 URL 增加变量的部分,你需要把一些特定的字段标记成<variable_name>。这些特定的字段将作为参数传入到你的函数中。当然也可以指定一个可选的转换器通过规则<converter:variable_name>将变量值转换为特定的数据类型。
/home/shiyanlou/Code/hello.py 文件中添加如下的代码:

@app.route(‘/user/‘)
def show_user_profile(username):
显示用户名
return ‘User {}’.format(username)

@app.route(‘/post/int:post_id‘)
def show_post(post_id):
显示提交整型的用户”id”的结果,注意”int”是将输入的字符串形式转换为整型数据
return ‘Post {}’.format(post_id)

@app.route(‘/path/path:subpath‘)
def show_subpath(subpath):
显示 /path/ 之后的路径名
return ‘Subpath {}’.format(subpath)

1
2
3
4
5
6
7
当访问 http://127.0.0.1:5000/user/shiyanlou 时,页面显示为 User shiyanlou。

当访问 http://127.0.0.1:5000/post/3 时,页面显示为 Post 3。用户在浏览器地址栏上输入的都是字符串,但是在传递给 show_post 函数处理时已经被转换为了整型。

当访问 http://127.0.0.1:5000/path/file/A/a.txt 时,页面显示为 Subpath file/A/a.txt

唯一 URLs / 重定向行为

@app.route(‘/projects/‘)
def projects():
return ‘The project page’

@app.route(‘/about’)
def about():
return ‘The about page

1
2
3
4
5
6
7
8
9
10
11
12
13
虽然它们看起来确实相似,但它们结尾斜线的使用在 URL 定义中不同。

第一种情况中,规范的 URL 指向 projects 尾端有一个斜线/。这种感觉很像在文件系统中的文件夹。访问一个结尾不带斜线的 URL 会被 Flask 重定向到带斜线的规范 URL 去。
当访问 http://127.0.0.1:5000/projects/ 时,页面会显示 The project page。

然而,第二种情况的 URL 结尾不带斜线,类似 UNIX-like 系统下的文件的路径名。此时如果访问结尾带斜线的 URL 会产生一个404 “Not Found”错误。
当访问 http://127.0.0.1:5000/about 时,页面会显示 The about page;但是当访问 http://127.0.0.1:5000/about/ 时,页面就会报错 Not Found。

当用户访问页面忘记结尾斜线时,这个行为允许关联的 URL 继续工作,并且与 Apache 和其它的服务器的行为一致,反之则不行,因此在代码的 URL 设置时斜线只可多写不可少写;另外,URL 会保持唯一,有助于避免搜索引擎索引同一个页面两次。

## HTTP方法

默认情况下,路由只会响应 GET 请求,但是能够通过给 route() 装饰器提供 methods 参数来改变。这里是一个例子:

@app.route(‘/login’, methods=[‘GET’, ‘POST’])
def login():
if request.method == ‘POST’:
do_the_login() 如果是 POST 方法就执行登录操作
else:
show_the_login_form() 如果是 GET 方法就展示登录表单

1
2
3
4
5
6
7
8
9
如果使用 GET 方法,HEAD 方法将会自动添加进来。你不必处理它们。也能确保 HEAD 请求会按照 HTTP RFC (文档在 HTTP 协议里面描述) 要求来处理,因此你完全可以忽略这部分 HTTP 规范。
同样地,自从 Flask 0.6 后,OPTIONS 方法也能自动为你处理。

## 练习

请开发一个小应用,URL 地址输入http://127.0.0.1:5000/xxx(其中 xxx 表示你的名字),访问页面会显示 xxx。

请完成一个应用,当 URLhttp://127.0.0.1:5000/sum/a/b时,其中a和b都是数字,服务器返回它们的和。
练习题 1 的答案

from flask import Flask
app = Flask(name)

@app.route(‘/‘)
def get_name(username):
return username

1
2


from flask import Flask
app = Flask(name)

@app.route(‘/sum/int:a/int:b‘)
def get_sum(a,b):
return ‘{0} + {1} = {2}’.format(a,b,a+b)

1
2
3
4
5
6
7
8
9
10
11
12
13
# 静态文件及渲染模板
## 静态文件
动态的 web 应用同样需要静态文件。CSS 和 JavaScript 文件通常来源于此。
理想情况下,你的 web 服务器已经配置好为它们服务,然而在开发过程中 Flask 就能够做到。只要在你的包中或模块旁边创建一个名为static 的文件夹,在应用中使用 /static 即可访问。

给静态文件生成 URL ,使用特殊的 static 端点名
url_for('static', filename='style.css')
这个文件是应该存储在文件系统上的static/style.css。
在 Python 中生成 HTML 并不好玩,实际上是相当繁琐的,因为你必须自行做好 HTML 转义以保持应用程序的安全。由于这个原因,Flask 自动为你配置好 Jinja2 模板。

你可以使用方法 render_template() 来渲染模板。所有你需要做的就是提供模板的名称以及你想要作为关键字参数传入模板的变量。

这里有个渲染模板的简单例子,在 /home/shiyanlou/Code 目录下新建 hello.py 文件,并向其中添加如下代码:

from flask import Flask, render_template
app = Flask(name)

@app.route(‘/hello/‘)
@app.route(‘/hello/‘)
def hello(name=None): 默认 name 为 None
return render_template(‘hello.html’, name=name) 将 name 参数传递到模板变量中

1
2
3
Flask 将会在 templates 文件夹中寻找模板。因此如果你的应用是个模块,这个文件夹在模块的旁边,如果它是一个包,那么这个文件夹在你的包里面:

比如,应用是模块(本系列实验的应用结构都是模块型):

/application.py
/templates
/hello.html

1
2
3
4
```
/application.py
/templates
/hello.html

对于模板,你可以使用 Jinja2 模板的全部能力。详细信息查看官方的 Jinja2 Template Documentation 。

在 /home/shiyanlou/Code 目录下新建 templates 文件夹并在其中新建 hello.html 文件:
然后向 hello.html 模板文件中添加如下代码:

1
2
3
4
5
6
7
<!doctype html>
<title>Hello from Flask</title>
&#123;% if name %&#125; <!-- 如果 name 不为空则将 name 渲染出来 -->
<h1>Hello &#123;&#123; name &#125;&#125;!</h1>
&#123;% else %&#125; <!-- 如果 name 为空则打印 Hello World! -->
<h1>Hello World!</h1>
&#123;% endif %&#125;

按照前面的方法运行应用程序,当访问 http://127.0.0.1:5000/hello/ 时页面显示 Hello World!;
当访问 http://127.0.0.1:5000/hello/shiyanlou 时页面显示 Hello shiyanlou!。
在模板中你也可以使用request,session和g对象,也能使用函数get_flashed_messages() 。

模板继承是十分有用的。如果想要知道模板继承如何工作的话,请阅读文档模板继承。基本的模板继承使得某些特定元素(如标题、导航和页脚)在每一页成为可能。

自动转义默认是开启的,因此如name包含 HTML,它将会自动转义。如果你信任一个变量,并且你知道它是安全的(例如一个模块把 wiki 标记转换到 HTML ),你可以用Markup类或|safe过滤器在模板中标记它是安全的。 在 Jinja 2 文档中,你会见到更多例子。

练习:
请创建一个模板和CSS文件,并在模板引入CSS文件,当访问网站首页时显示一个绿色的Hello ShiYanLou字样。关于HTML以及CSS的学习请参考在线教程
文件目录结构如下所示:

1
2
3
4
5
/hello.py
/templates
/hello.html
/static
/hello.css

hello.py 文件中的代码如下所示:

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

@app.route('/')
def get_hello():
return render_template('hello.html')

templates/hello.html 存放的是前端代码,如下所示:

1
2
3
4
5
6
7
8
9
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<link rel="stylesheet" type="text/css" href="&#123;&#123; url_for('static', filename='hello.css') &#125;&#125;">
</head>
<body>
<h1>Hello ShiYanLou</h1>
</body>
</html>

static/hello.css 存放的是 css 代码,如下所示:

1
2
3
4
h1&#123;
color: green;
text-align: center;
&#125;

接收请求数据

首先你需要从 flask 模块中导入request:
from flask import request
当前请求的方法可以用method属性来访问。你可以用form属性来访问表单数据 (数据在 POST 或者PUT中传输)。
这里是上面提及到的两种属性的完整的例子:

1
2
3
4
5
6
7
8
9
10
11
@app.route('/login', methods=['POST', 'GET'])
def login():
error = None
if request.method == 'POST':
if valid_login(request.form['username'],
request.form['password']):
return log_the_user_in(request.form['username'])
else:
error = 'Invalid username/password'
当请求形式为“GET”或者认证失败则执行以下代码
return render_template('login.html', error=error)

如果在form属性中不存在上述键值会发生些什么?在这种情况下会触发一个特别的 KeyError。你可以像捕获标准的KeyError一样来捕获它,如果你不这样去做,会显示一个HTTP 400 Bad Request错误页面。所以很多情况下你不需要处理这个问题。

你可以用args属性来接收在URL ( ?key=value )中提交的参数:
searchword = request.args.get(‘key’, ‘’)
我们推荐使用get来访问 URL 参数或捕获KeyError,因为用户可能会修改 URL,向他们显示一个400 bad request页面不是用户友好的。

文件上传

你能够很容易地用 Flask 处理文件上传。只要确保在你的 HTML 表单中不要忘记设置属性enctype=”multipart/form-data”,否则浏览器将不会传送文件。

上传的文件是存储在内存或者文件系统上一个临时位置。你可以通过请求对象中files属性访问这些文件。每个上传的文件都会存储在这个属性字典里。它表现得像一个标准的 Python file对象,但是它同样具有save()方法,该方法允许你存储文件在服务器的文件系统上。

下面是一个简单的例子用来演示提交文件到服务器上:

1
2
3
4
5
6
7
8
from flask import request

@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
if request.method == 'POST':
f = request.files['the_file']
f.save('/var/www/uploads/uploaded_file.txt')
...

如果你想要知道在上传到你的应用之前在客户端的文件名称,你可以访问filename属性。
但请记住永远不要信任这个值,因为这个值可以伪造。如果你想要使用客户端的文件名来在服务器上存储文件,把它传递到Werkzeug提供给你的secure_filename()函数:

1
2
3
4
5
6
7
8
9
from flask import request
from werkzeug import secure_filename

@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
if request.method == 'POST':
f = request.files['the_file']
f.save('/var/www/uploads/' + secure_filename(f.filename))
...

cookies

你可以用 cookies 属性来访问 Cookies 。你能够用响应对象的 set_cookie 来设置 cookies。请求对象中的 cookies 属性是一个客户端发送所有的 cookies 的字典。

如果你要使用会话(sessions),请不要直接使用 cookies,相反,请用 Flask 中的会话,Flask 已经在cookies 上增加了一些安全细节;关于更多 seesions 和 cookies 的区别与联系,请参见施杨出品的博客。

读取 cookies:

1
2
3
4
5
6
7
from flask import request

@app.route('/')
def index():
username = request.cookies.get('username')
注意这里引用cookies字典的键值对是使用cookies.get(key)
而不是cookies[key],这是防止该字典不存在时报错"keyerror"

存储 cookies:

1
2
3
4
5
6
7
from flask import make_response

@app.route('/')
def index():
resp = make_response(render_template(...))
resp.set_cookie('username', 'the username')
return resp

注意cookies是在响应对象中被设置。由于通常只是从视图函数返回字符串,Flask 会将其转换为响应对象。如果你要显式地这么做,可以使用 make_response() 函数接着修改它。

有时候你可能要在响应对象不存在的地方设置cookie。利用延迟请求回调模式使得这种情况成为可能。

本节讲解了 flask 的请求,如果想在没有请求的情况下获取上下文,可以使用test_request_context()或者request_context(),从request对象的form中可以获取表单的数据,args中可以获取 URL 中的参数,files可以获取上传的文件,cookies可以操作cookie。

练习

请实现一个上传图片到服务器的功能。具体要求如下:

在 /home/shiyanlou/Code 目录下新建 upload_file.py 文件并在其中写入本练习的代码。
要求上传的文件保存位置为 /home/shiyanlou/Code 目录下。
当访问首页 http://127.0.0.1 时出现表单可以上传文件,
当上传成功后在浏览器可以看到 <上传的文件名> upload successed!

参考代码
在 /home/shiyanlou/Code/upload_file.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
import os
from flask import Flask, request
from werkzeug import secure_filename 获取上传文件的文件名

UPLOAD_FOLDER = '/home/shiyanlou/Code' 上传路径
ALLOWED_EXTENSIONS = set(['txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif']) 允许上传的文件类型

app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER

def allowed_file(filename): 验证上传的文件名是否符合要求,文件名必须带点并且符合允许上传的文件类型要求,两者都满足则返回 true
return '.' in filename and \
filename.rsplit('.', 1)[1] in ALLOWED_EXTENSIONS

@app.route('/', methods=['GET', 'POST'])
def upload_file():
if request.method == 'POST': 如果是 POST 请求方式
file = request.files['file'] 获取上传的文件
if file and allowed_file(file.filename): 如果文件存在并且符合要求则为 true
filename = secure_filename(file.filename) 获取上传文件的文件名
file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename)) 保存文件
return '&#123;&#125; upload successed!'.format(filename) 返回保存成功的信息
使用 GET 方式请求页面时或是上传文件失败时返回上传文件的表单页面
return '''
<!doctype html>
<title>Upload new File</title>
<h1>Upload new File</h1>
<form action="" method=post enctype=multipart/form-data>
<p><input type=file name=file>
<input type=submit value=Upload>
</form>
'''

重定向、响应、会话和扩展

重定向和错误

你能够用redirect()函数重定向用户到其它地方。能够用abort()函数提前中断一个请求并带有一个错误代码。

下面是一个演示它们如何工作的例子,在 /home/shiyanlou/Code/ 目录下新建 hello.py 文件并向其中写入如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
from flask import Flask
from flask import abort, redirect, url_for

app = Flask(__name__)

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

@app.route('/login')
def login():
abort(401)
this_is_never_executed()

按照之前的方式运行应用,这是一个相当无意义的例子因为用户会从主页/重定向到一个不能访问的页面/login( 401 意味着禁止访问),但是它说明了重定向如何工作。

默认情况下,每个错误代码会显示一个黑白错误页面。比如上面的页面会显示 401 Unauthorized。
如果你想定制错误页面,可以使用errorhandler()装饰器,向 /home/shiyanlou/Code/hello.py 文件中添加如下代码:

1
2
3
4
5
from flask import render_template

@app.errorhandler(401)
def page_not_found(error):
return render_template('page_not_found.html'), 404

注意到 404 是在render_template()调用之后。告诉 Flask 该页的错误代码应是 404 ,即没有找到。默认的 200 被假定为:一切正常。

在 /home/shiyanlou/Code 目录下新建 templates 文件夹,并在其中新建 page_not_found.html 文件。
向 page_not_found.html 文件中添加如下代码:

1
<h1>page not found, this is an error page.</h1>

响应

一个视图函数的返回值会被自动转换为一个响应对象。如果返回值是一个字符串,它被转换成一个响应主体是该字符串,错误代码为 200 OK ,媒体类型为text/html的响应对象。Flask 把返回值转换成响应对象的逻辑如下:

如果返回的是一个合法的响应对象,它会直接从视图返回。
如果返回的是一个字符串,响应对象会用字符串数据和默认参数创建。
如果返回的是一个元组而且元组中元素能够提供额外的信息。这样的元组必须是(response, status, headers) 形式且至少含有其中的一个元素。status值将会覆盖状态代码,headers可以是一个列表或额外的消息头值字典。
如果上述条件均不满足,Flask 会假设返回值是一个合法的 WSGI 应用程序,并转换为一个请求对象。
如果你想要获取在视图中得到的响应对象,你可以用函数make_response()。

想象你有这样一个视图:

1
2
3
@app.errorhandler(404)
def not_found(error):
return render_template('error.html'), 404

你只需要用make_response()封装返回表达式,获取结果对象并修改,然后返回它:

1
2
3
4
5
@app.errorhandler(404)
def not_found(error):
resp = make_response(render_template('error.html'), 404)
resp.headers['X-Something'] = 'A value'
return resp

会话

除了请求对象,还有第二个称为session对象允许你在不同请求间存储特定用户的信息。这是在 cookies 的基础上实现的,并且在 cookies 中使用加密的签名。这意味着用户可以查看 cookie 的内容,但是不能修改它,除非知道签名的密钥。

要使用会话,你需要设置一个密钥。这里介绍会话如何工作,在 /home/shiyanlou/Code 目录下新建 test.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
from flask import Flask, session, redirect, url_for, escape, request

app = Flask(__name__)

设置密钥,保证会话安全
app.secret_key = '_5#y2L"F4Q8z\n\xec]/'

@app.route('/')
def index():
if 'username' in session:
return 'Logged in as %s' % escape(session['username'])
return 'You are not logged in'

@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
session['username'] = request.form['username']
return redirect(url_for('index'))
return '''
<form method="post">
<p><input type=text name=username>
<p><input type=submit value=Login>
</form>
'''

@app.route('/logout')
def logout():
如果用户名存在,则从会话中移除该用户名
session.pop('username', None)
return redirect(url_for('index'))

这里提到的escape()可以在你不使用模板引擎的时候做转义(如同本例)。其中,login函数中返回的网页源代码可以单独存储在templates文件夹中作为模板文件html,然后使用return render_template()更方便。

按照前面的方式运行程序:

当访问首页 http://127.0.0.1:5000/ 时会显示 You are not logged in;

当访问登录页面 http://127.0.0.1:5000/login 时会出现一个输入框,在输入框中输入用户名 shiyanlou,然后点击 Login 按钮,这时 URL 会重定向到首页上,首页显示 Logged in as shiyanlou;

最后再访问登出页面 http://127.0.0.1:5000/logout,这时从 session 中移除了用户名,URL 重定向到首页显示 You are not logged in;

怎样产生一个好的密钥:

随机的问题在于很难判断什么是真随机。一个密钥应该足够随机。你的操作系统可以基于一个密码随机生成器来生成漂亮的随机值,这个值可以用来做密钥:

1
2
$ python3 -c 'import os; print(os.urandom(16))'
b'm \xf8>]?\x86\xcf/y\x0e\xc5\xc7j\xc5/'

把这个值复制粘贴到你的代码,你就搞定了密钥。

使用基于 cookie 的会话需注意: Flask 会将你放进会话(session)对象的值序列化到 cookie 。如果你试图寻找一个跨请求不能存留的值,cookies 确实是启用的,并且你不会获得明确的错误信息,检查你页面请求中 cookie 的大小,并与 web 浏览器所支持的大小对比。

消息

好的应用和用户界面全部是关于反馈。如果用户得不到足够的反馈,他们可能会变得讨厌这个应用。Flask 提供了一个真正的简单的方式来通过消息闪现系统给用户反馈。消息闪现系统基本上使得在请求结束时记录信息并在下一个 (且仅在下一个)请求中访问。通常结合模板布局来显示消息。

使用flash()方法来闪现一个消息,使用get_flashed_messages()能够获取消息,get_flashed_messages()也能用于模板中。

下面来看一个简单的例子,在 /home/shiyanlou/Code 目录下新建 flashTest.py 文件,并向其中写入如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from flask import Flask, flash, redirect, render_template, \
request, url_for

app = Flask(__name__)
app.secret_key = b'_5#y2L"F4Q8z\n\xec]/'

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

@app.route('/login', methods=['GET', 'POST'])
def login():
error = None
if request.method == 'POST':
if request.form['username'] != 'admin' or \
request.form['password'] != 'secret':
error = 'Invalid credentials'
else:
flash('You were successfully logged in')
return redirect(url_for('index'))
return render_template('login.html', error=error)

然后在 /home/shiyanlou/Code/templates 目录下新建 base.html 页面,其中写入基本的模板代码,代码主要是从后端获取 flash 消息以及错误信息。

1
2
3
4
5
6
7
8
9
10
11
12
<!doctype html>
<title>My Application</title>
&#123;% with messages = get_flashed_messages() %&#125;
&#123;% if messages %&#125;
<ul class=flashes>
&#123;% for message in messages %&#125;
<li>&#123;&#123; message &#125;&#125;</li>
&#123;% endfor %&#125;
</ul>
&#123;% endif %&#125;
&#123;% endwith %&#125;
&#123;% block body %&#125;&#123;% endblock %&#125;

在该目录下新建 index.html 页面,这个页面继承于 base.html 页面:

1
2
3
4
5
&#123;% extends "base.html" %&#125;
&#123;% block body %&#125;
<h1>Overview</h1>
<p>Do you want to <a href="&#123;&#123; url_for('login') &#125;&#125;">log in?</a>
&#123;% endblock %&#125;

在该目录下新建 login.html 页面,这个页面也继承于 base.html 页面:.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
&#123;% extends "base.html" %&#125;
&#123;% block body %&#125;
<h1>Login</h1>
&#123;% if error %&#125;
<p class=error><strong>Error:</strong> &#123;&#123; error &#125;&#125;
&#123;% endif %&#125;
<form method=post>
<dl>
<dt>Username:
<dd><input type=text name=username value="&#123;&#123;
request.form.username &#125;&#125;">
<dt>Password:
<dd><input type=password name=password>
</dl>
<p><input type=submit value=Login>
</form>
&#123;% endblock %&#125;

按照前面的方式运行程序:

当访问首页 http://127.0.0.1:5000,会提示 Do you want to log in?,点击链接跳转到登录页面。

在登录页面 http://127.0.0.1:5000/login,输入用户名和密码,如果输入错误的信息比如两个都为 shiyanlou,点击 Login,就会出现错误提示 Error: Invalid credentials。如果用户名输入 admin、密码输入 secret,点击 Login,就会跳转到首页,同时在首页会显示 flash 消息 You were successfully logged in。

日志

有时候你会遇到一种情况:理论上来说你处理的数据应该是正确的,然而实际上并不正确的状况。比如你可能有一些客户端代码,代码向服务器发送一个 HTTP 请求但是显然它是错误的。这可能是由于用户篡改数据,或客户端代码失败。大部分时候针对这一情况返回400 Bad Request就可以了,但是有时候不能这样做,代码必须继续工作。

你也有可能想要记录一些发生的不正常事情。这时候日志就派上用处。从 Flask 0.3 开始日志记录是预先配置好的。

这里有一些日志调用的例子:

app.logger.debug(‘A value for debugging’)
app.logger.warning(‘A warning occurred (%d apples)’, 42)
app.logger.error(‘An error occurred’)
附带的 logger 是一个标准的日志类 Logger ,因此更多的信息请查阅官方文档 logging documentation。

整合 WSGI 中间件
如果你想给你的应用添加 WSGI 中间件,你可以封装内部 WSGI 应用。例如如果你想使用 Werkzeug 包中的某个中间件来应付 lighttpd 中的 bugs,你可以这样做:

from werkzeug.contrib.fixers import LighttpdCGIRootFix
app.wsgi_app = LighttpdCGIRootFix(app.wsgi_app)

练习

请实现一个完整的用户登录功能:

当访问地址 http://127.0.0.1:5000/login ,出现登录页面,可以使用用户名和密码填写登录表单。
如果用户名和密码都为shiyanlou,那么就把用户名数据放到session中,把地址重定向到首页显示Hello shiyanlou,同时闪现消息 you were logged in。
如果用户名和密码不对,依然把地址重定向到首页显示hello world,同时闪现消息 username or password invalid。
参考答案
文件目录结构如下所示:

1
2
3
4
/app.py
/templates
/index.html
/login.html

在 app.py 文件中添加如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from flask import Flask, request
from flask import flash, redirect, url_for, render_template, session

app = Flask(__name__)
app.secret_key = b'_5#y2L"F4Q8z\n\xec]/'

@app.route('/<name>')
def index(name):
return render_template('index.html',name=name)

@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
if request.form['username'] != 'shiyanlou' or request.form['password'] != 'shiyanlou':
flash('username or password invalid')
return redirect(url_for('index', name='world'))
else:
session['username'] = request.form['username']
name = request.form['username']
flash('you were logged in')
return redirect(url_for('index', name=name))
return render_template('login.html')

在 templates/index.html 文件中添加如下代码:

1
2
3
4
5
6
7
8
<!doctype html>
<title>Index</title>
<div>
&#123;% for message in get_flashed_messages() %&#125;
<div class=flash>&#123;&#123; message &#125;&#125;</div>
&#123;% endfor %&#125;
<h1>hello &#123;&#123;name&#125;&#125;</h1>
</div>

在 templates/login.html 文件中添加如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
<!doctype html>
<title>Login</title>
<div>
<form action="&#123;&#123; url_for('login') &#125;&#125;" method=post>
<dl>
<dt>Username:
<dd><input type=text name=username>
<dt>Password:
<dd><input type=password name=password>
<dd><input type=submit value=Login>
</dl>
</form>
</div>

Flask项目实战之博客开发(一)

实验源码:
wget http://labfile.oss.aliyuncs.com/courses/29/flaskr.zip
unzip flaskr.zip
实验代码建议统一存放在 /home/shiyanlou/Code 目录下。
这里我们将博客应用起名为flaskr,也可以取一个不那么 web 2.0 的名字。基本上我们想要它做如下的事情:

根据配置文件中的认证允许用户登录以及注销。仅仅支持一个用户。
当用户登录后,他们可以添加新的条目,这些条目是由纯文本的标题和 HTML 的正文构成。因为我们信任用户这里的 HTML 是安全的。
页面倒序显示所有条目(新的条目在前),并且用户登入后可以在此添加新条目。
我们将在这个应用中直接使用 SQLite 3 因为它足够应付这种规模的应用。对更大的应用使用 SQLAlchemy 是十分有意义的,它以一种更智能方式处理数据库连接,允许你一次连接多个不同的关系数据库。你也可以考虑流行的 NoSQL 数据库,前提是你的数据更适合它们。

这是最终应用的一个截图:

在开始之前,让我们为这个应用创建需要的文件夹:

1
2
3
/flaskr
/static
/templates

flaskr:项目总文件夹。其中会存放数据库代码文件 schema.sql 和 主代码文件 flaskr.py。
static:项目的资源文件。用于存放 css 和 javascript 文件。
templates:项目前端页面文件。用于存放前端页面模板。

数据库模式

首先我们要创建数据库模式。对于这个应用仅一张表就足够了,而且我们只想支持 SQLite ,所以很简单。在 Code/flaskr 目录下新建 schema.sql 文件并向其中写入如下代码:

1
2
3
4
5
6
drop table if exists entries;
create table entries (
id integer primary key autoincrement,
title string not null,
text string not null
);

这个模式由一个称为entries的单表构成,在这个表中每行包含一个id,一个title和一个text。id是一个自增的整数而且是主键,其余两个为非空的字符串。

应用代码

现在我们已经有了数据库模式了,可以创建应用的模块了。

在 Code/flaskr 目录下新建 flaskr.py 文件。对于小应用,直接把配置放在主模块里,正如我们现在要做的一样,这是可行的。然而一个更干净的解决方案就是单独创建.ini或者.py文件接着加载或者导入里面的值。

在 flaskr.py 文件中写入如下代码:

1
2
3
4
5
6
7
8
9
10
11
# 导入所有的模块
import sqlite3
from flask import Flask, request, session, g, redirect, url_for, abort, render_template, flash

# 配置文件
DATABASE = '/tmp/flaskr.db'
ENV = 'development'
DEBUG = True
SECRET_KEY = 'development key'
USERNAME = 'admin'
PASSWORD = 'default'

,修改 flaskr.py 文件在其中配置初始化:

创建应用

1
2
app = Flask(__name__)
app.config.from_object(__name__)

from_object()将会寻找给定的对象(如果它是一个字符串,则会导入它),搜寻里面定义的全部大写的变量。在我们的这种情况中,配置文件就是我们上面写的几行代码。 你也可以将他们分别存储到多个文件。

通常从配置文件中加载配置是一个好的主意。这时可以使用from_envvar()来实现,可以用它替换上面的from_object():

app.config.from_envvar(‘FLASKR_SETTINGS’, silent=True)
这种方法我们可以设置一个名为FLASKR_SETTINGS的环境变量来设定一个配置文件载入后是否覆盖默认值。静默开关silent=True告诉 Flask 不去关心这个环境变量键值是否存在。

SECRET_KEY是为了保持客户端的会话安全。明智地选择该键,使得它难以猜测,最好是尽可能复杂。

DEBUG 调试标志启用或禁用交互式调试。决不让调试模式在生产系统中启动,因为它将允许用户在服务器上执行代码!

我们还添加了一个轻松地连接到指定数据库的方法,这个方法用于在请求时打开一个连接,并且在交互式 Python shell 和脚本中也能使用,这将方便后面的使用。

在 Code/flaskr.py 文件中继续添加如下代码:

def connect_db():
return sqlite3.connect(app.config[‘DATABASE’])
最后如果我们想要把这个文件当做独立应用来运行,只需在服务器启动文件也就是 Code/flaskr.py 文件的末尾添加这一行:

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

如此我们便可以顺利开始运行这个应用,通过使用如下命令:

1
2
3
4
5
6
7
8
$ python3 flaskr.py
* Serving Flask app "flaskr" (lazy loading)
* Environment: development
* Debug mode: on
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
* Restarting with stat
* Debugger is active!
* Debugger PIN: 289-075-564

你将会看到一个信息,信息提示你服务器启动的地址,这个地址你能够访问到的。

当你在浏览器中访问服务器获得一个 404 Not Found 的错误时,是因为我们还没有任何视图。我们之后再来关注这些。首先应该让数据库工作起来。

Flask项目实战之博客开发(二)

创建数据库

如前面所述,Flaskr 是一个数据库驱动的应用程序,准确地来说,Flaskr 是一个使用关系数据库系统的应用程序。这样的系统需要一个模式告诉它们如何存储信息。因此在首次启动服务器之前,创建数据库模式是很重要的。

可以通过管道把 schema.sql 作为 sqlite 3 命令的输入来创建这个模式,命令如下:

1
sqlite3 /tmp/flaskr.db < schema.sql

这种方法的缺点是需要安装 sqlite 3 命令,而且你必须提供数据库的路径,否则将报错。由于实验楼环境中没有安装 sqllite3 所以会得到报错command not found: sqlite3。

添加一个函数来对数据库进行初始化是个不错的想法。如果你想要这么做,首先必须从contextlib包中导入contextlib.closing()函数。在 Code/flaskr.py 文件中添加如下代码:

1
from contextlib import closing

接着我们可以创建一个称为 init_db 函数,该函数用来初始化数据库。为此我们可以使用之前定义的 connect_db 函数。 只要在 Code/flaskr.py 文件中的 connect_db 函数下添加这样的函数:

1
2
3
4
5
def init_db():
with closing(connect_db()) as db:
with app.open_resource('schema.sql') as f:
db.cursor().executescript(f.read().decode())
db.commit()

closing()函数允许我们在with块中保持数据库连接可用。应用对象的open_resource()方法在其方框外也支持这个功能,因此可以在with块中直接使用。这个函数从当前位置(你的flaskr 文件夹)中打开一个文件,并且允许你读取它。我们在这里用它在数据库连接上执行一个脚本。

当我们连接到数据库时会得到一个数据库连接对象(这里命名它为 db),这个对象提供给我们一个数据库指针。指针上有一个可以执行完整脚本的方法。最后我们不显式地提交更改, SQLite 3 或者其它事务数据库不会这么做。

现在可以在 Python shell 里创建数据库,导入并调用刚才的函数:

1
2
3
python3
>>> from flaskr import init_db
>>> init_db()

注意:

如果你后面得到一个表不能找到的异常,请检查你是否调用了 init_db 函数以及你的表名是否正确 (例如: singular vs. plural)。

请求数据库连接

现在我们知道了怎样建立数据库连接以及在脚本中使用这些连接,但是我们如何能优雅地在请求中这么做?

所有我们的函数中都需要数据库连接,因此在请求之前初始化它们,在请求结束后自动关闭它们就很有意义。

Flask 允许我们使用before_request(),after_request()和 teardown_request()装饰器来实现这个功能,在 Code/flaskr.py 文件中添加如下代码:

1
2
3
4
5
6
7
@app.before_request
def before_request():
g.db = connect_db()

@app.teardown_request
def teardown_request(exception):
g.db.close()

使用before_request()装饰器的函数会在请求之前被调用而且不带参数。使用after_request()装饰器的函数会在请求之后被调用且传入将要发给客户端的响应。

它们必须返回那个响应对象或是不同的响应对象。但当异常抛出时,它们不一定会被执行,这时可以使用teardown_request()装饰器。它装饰的函数将在响应构造后执行,并不允许修改请求,返回的值会被忽略。如果在请求已经被处理的时候抛出异常,它会被传递到每个函数,否则会传入一个None。

我们把当前的数据库连接保存在 Flask 提供的 g 特殊对象中。这个对象只能保存一次请求的信息,并且在每个函数里都可用。不要用其它对象来保存信息,因为在多线程环境下将不可行。特殊的对象 g 在后台有一些神奇的机制来保证它在做正确的事情。

Flask项目实战之博客开发(三)

视图

显示条目

这个视图显示所有存储在数据库中的条目。它监听着应用的根地址以及将会从数据库中查询标题和内容。id值最大的条目(最新的条目)将在最前面。从游标返回的行是按select语句中声明的列组织的元组。对于像我们这样的小应用是足够的,但是你可能要把它们转换成字典,如果你对如何转换成字典感兴趣的话,请查阅简化查询例子。

视图函数将会把条目作为字典传入show_entries.html 模板并返回渲染结果。

在 flaskr/flaskr.py 文件中添加如下代码:

1
2
3
4
5
@app.route('/')
def show_entries():
cur = g.db.execute('select title, text from entries order by id desc') 查询语句
entries = [dict(title=row[0], text=row[1]) for row in cur.fetchall()] 将查询结果转换为字典
return render_template('show_entries.html', entries=entries)

添加条目

这个视图允许登录的用户添加新的条目。它只回应POST请求,实际的表单是显示在show_entries页面。如果正常工作的话,我们用flash()向下一个请求闪现一条信息并且跳转回show_entries页面。

在 flaskr/flaskr.py 文件中添加如下代码:

@app.route(‘/add’, methods=[‘POST’])
def add_entry():
if not session.get(‘logged_in’):
abort(401)
g.db.execute(‘insert into entries (title, text) values (?, ?)’,[request.form[‘title’], request.form[‘text’]]) # 向数据库中插入数据
g.db.commit() # 更新数据
flash(‘New entry was successfully posted’) # 闪现一条消息
return redirect(url_for(‘show_entries’))
注意我们这里检查用户登录情况(logged_in键存在会话中,并且为True)。向 SQL 语句中传递参数,需要在语句中使用问号?来代替参数,并把参数放在列表中进行传递。不要使用字符串格式化的方式直接把参数传入 SQL 语句中,这样可能会有潜在的 SQL 注入风险。

登入和注销

这些函数是用于用户登录以及注销。登录时依据在配置中的值检查用户名和密码并且在会话中设置logged_in键值。如果用户成功登录,logged_in键值被设置成True,并跳转回show_entries页面。此外,会有消息闪现来提示用户登录成功。

如果发生错误,模板会通知,并提示重新登录。在 flaskr/flaskr.py 文件中添加如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
@app.route('/login', methods=['GET', 'POST'])
def login():
error = None
if request.method == 'POST':
if request.form['username'] != app.config['USERNAME']: 如果用户名不符合
error = 'Invalid username'
elif request.form['password'] != app.config['PASSWORD']: 如果密码不符合
error = 'Invalid password'
else:
session['logged_in'] = True 成功登录,在 session 中添加一个 logged_in 值为 True
flash('You were logged in') 闪现一条消息
return redirect(url_for('show_entries')) 重定向到首页
return render_template('login.html', error=error) 如果没有成功登录则返回登录页面以及错误信息

另一方面,注销函数从会话中移除了logged_in键值。这里我们使用一个大绝招:
如果你使用字典的pop()方法并传入第二个参数(默认),这个方法会从字典中删除这个键,如果这个键不存在则什么都不做。这很有用,因为我们不需要检查用户是否已经登入。

1
2
3
4
5
@app.route('/logout')
def logout():
session.pop('logged_in', None) 移除 logged_in 键
flash('You were logged out') 闪现消息
return redirect(url_for('show_entries')) 重定向到首页

模板

现在我们应该开始编写模板。如果我们现在请求 URLs ,将会得到一个 Flask 无法找到模板的异常。模板使用 Jinja2 语言以及默认开启自动转义。这就意味着除非你使用Markup标记或在模板中使用|safe过滤器,否则 Jinja2 会确保特殊字符比如<或>被转义成等价的XML实体。

我们使用模板继承使得在网站的所有页面中重用布局成为可能。

我们的模板统一存放在Code/flaskr/templates文件夹中。

layout.html

这个模板包含 HTML 主体结构,标题和一个登录链接(或者当用户已登录则提供注销)。如果有闪现信息的话它也将显示闪现信息。{% block body %} 块能够被子模板中的同样名字(body)的块替代。

session字典在模板中同样可用的,你能用它检查用户是否登录。注意在 Jinja 中你可以访问不存在的对象/字典属性或成员,如同下面的代码,即便 logged_in 键不存在,仍然可以正常工作。

在 flaskr/templates 目录下新建 layout.html 文件并写入如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!doctype html>
<title>Flaskr</title>
<link rel=stylesheet type=text/css href="&#123;&#123; url_for('static', filename='style.css') &#125;&#125;">
<div class=page>
<h1>Flaskr</h1>
<div class=metanav>
&#123;% if not session.logged_in %&#125;
<a href="&#123;&#123; url_for('login') &#125;&#125;">log in</a>
&#123;% else %&#125;
<a href="&#123;&#123; url_for('logout') &#125;&#125;">log out</a>
&#123;% endif %&#125;
</div>
&#123;% for message in get_flashed_messages() %&#125;
<div class=flash>&#123;&#123; message &#125;&#125;</div>
&#123;% endfor %&#125;
&#123;% block body %&#125;&#123;% endblock %&#125;
</div>

show_entries.html

这个模板继承了上面的layout.html模板用来显示信息。注意for遍历了所有我们用render_template()函数传入的信息。我们同样告诉表单提交到add_entry函数通过使用 HTTP 的POST方法。

在 flaskr/templates 目录下新建 show_entries.html 文件并写入如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
&#123;% extends "layout.html" %&#125;
&#123;% block body %&#125;
&#123;% if session.logged_in %&#125;
<form action="&#123;&#123; url_for('add_entry') &#125;&#125;" method=post class=add-entry>
<dl>
<dt>Title:
<dd><input type=text size=30 name=title>
<dt>Text:
<dd><textarea name=text rows=5 cols=40></textarea>
<dd><input type=submit value=Share>
</dl>
</form>
&#123;% endif %&#125;
<ul class=entries>
&#123;% for entry in entries %&#125;
<li><h2>&#123;&#123; entry.title &#125;&#125;</h2>&#123;&#123; entry.text|safe &#125;&#125;
&#123;% else %&#125;
<li><em>Unbelievable. No entries here so far</em>
&#123;% endfor %&#125;
</ul>
&#123;% endblock %&#125;

login.html

最后是登录模板,基本上只显示一个允许用户登录的表单。

在 flaskr/templates 目录下新建 login.html 文件并写入如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
&#123;% extends "layout.html" %&#125;
&#123;% block body %&#125;
<h2>Login</h2>
&#123;% if error %&#125;<p class=error><strong>Error:</strong> &#123;&#123; error &#125;&#125;&#123;% endif %&#125;
<form action="&#123;&#123; url_for('login') &#125;&#125;" method=post>
<dl>
<dt>Username:
<dd><input type=text name=username>
<dt>Password:
<dd><input type=password name=password>
<dd><input type=submit value=Login>
</dl>
</form>
&#123;% endblock %&#125;

添加样式

现在其它一切都正常工作,是时候给应用添加些样式。

在 Code/flaskr/static 目录下新建 style.css 样式文件并写入如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
body            &#123; font-family: sans-serif; background: #eee; &#125;
a, h1, h2 &#123; color: #377BA8; &#125;
h1, h2 &#123; font-family: 'Georgia', serif; margin: 0; &#125;
h1 &#123; border-bottom: 2px solid #eee; &#125;
h2 &#123; font-size: 1.2em; &#125;

.page &#123; margin: 2em auto; width: 35em; border: 5px solid #ccc;
padding: 0.8em; background: white; &#125;
.entries &#123; list-style: none; margin: 0; padding: 0; &#125;
.entries li &#123; margin: 0.8em 1.2em; &#125;
.entries li h2 &#123; margin-left: -1em; &#125;
.add-entry &#123; font-size: 0.9em; border-bottom: 1px solid #ccc; &#125;
.add-entry dl &#123; font-weight: bold; &#125;
.metanav &#123; text-align: right; font-size: 0.8em; padding: 0.3em;
margin-bottom: 1em; background: #fafafa; &#125;
.flash &#123; background: #CEE5F5; padding: 0.5em;
border: 1px solid #AACBE2; &#125;
.error &#123; background: #F0D6D6; padding: 0.5em; &#125;

运行应用

这样我们就完成了简单博客应用的代码编写,现在在终端执行如下命令运行程序:(在这之前注意要配置数据库,如果没有配置可以参考上一个实验的步骤)

1
python3 flaskr.py

访问首页 http://127.0.0.1:5000 ,效果如下: