Flask快速入门教程

Posted by Jack on 2020-03-24
Words 6.3k and Reading Time 24 Minutes
Viewed Times

一、Flask简介

Flask是一个使用Python编写的轻量级Web应用框架,基于Werkzeug WSGI(Python Web服务器网关接口(Python Web Server Gateway Interface,缩写为WSGI)是Python应用程序或框架和Web服务器之间的一种接口,已经被广泛接受, 它已基本达成它的可移植性方面的目标)工具箱Jinja2模板引擎。 Flask使用BSD授权。

Flask也被称为微框架(microframework),因为它使用简单的核心,用extension增加其他功能。“微”(micro) 并不表示你需要把整个Web应用塞进单个Python文件(虽然确实可以 ),也不意味着Flask在功能上有所欠缺。微框架中的“微”意味着 Flask 旨在保持核心 简单而易于扩展。默认情况下,Flask 不包含数据库抽象层、表单验证,或是其它任何已有多种库可以 胜任的功能。然而,Flask支持用扩展来给应用添加这些功能,如同是Flask本身实现的一样。众多的扩展提供了数据库集成、表单验证、上传处理、各种各样的开放认证技术等功能。

二、Flask安装

Flask 依赖两个外部库:WerkzeugJinja2 。 Werkzeug 是一个 WSGI(在Web应用和多种服务器之间的标准Python接口) 工具集。Jinja2 负责渲染模板。Flask需要Python 2.6或更高的版本支持,因此安装前应确保Python环境已经装好。

1. 通过virtualenv安装

在实际的开发过程中,往往拥有很多的项目,同时使用不同版本的Python工作的可能性也就很大,或者起码需要不同版本的Python库。这时常常会有库破坏向后兼容性,然而正经应用不采用外部库的可能微乎其微,从而在项目就出现了两个或更多依赖性冲突

这个时候virtualenv能够拯救世界!virtualenv为每个不同项目提供一份Python安装。它并没有真正安装多个Python副本,但是它确实提供了一种巧妙的方式来让各项目环境保持独立。接下来看看virtualenv是怎么工作的。

如果你在Mac OS系统或Linux系统下,下面两条命令可能会适用:

1
$ sudo easy_install virtualenv

或更好的:
1
$ sudo pip install virtualenv

上述的命令会在你的系统中安装virtualenv。它甚至可能会存在于包管理器中,如果你用的是 Ubuntu,可以尝试:
1
$ sudo apt-get install python-virtualenv

如果你用的是Windows系统,并且没有easy_install命令,那么你必须先安装这个命令。查阅 3. Windows下的pip和distribute了解如何安装。之后,运行上述的命令,但是要去掉 sudo 前缀。

virtualenv安装完毕后,你可以立即打开shell然后创建你自己的Python项目环境。比如,先创建一个项目文件夹,然后在其下创建一个venv文件夹。

1
2
3
4
5
$ mkdir myproject
$ cd myproject
$ virtualenv venv
New python executable in venv/bin/python
Installing distribute............done.

现在,无论何时你想在某个项目上工作,只需要激活相应的环境。在Mac OS系统和Linux系统上,执行如下操作:
1
$ . venv/bin/activate

下面的操作适用Windows系统:
1
$ venv\scripts\activate

无论通过哪种方式,你现在应该已经激活了virtualenv(注意你的shell提示符显示的是当前活动的环境)。现在你只需要键入以下的命令在virtualenv环境中安装Flask:
1
$ pip install Flask

2. 全局安装

全局安装是把Flask安装到系统Python环境中,在Mac OS系统和Linux系统中,只需要以root权限运行pip

1
$ sudo pip install Flask

如果是Windows系统,则需要以管理员方式运行命令提示符,然后执行下面的命令:
1
> pip install Flask

3. Windows下的pip和distribute

在Windows系统下,Python 3.x版本中已经自带了easy_install,只有Python 2.x版本需要手动安装下easy_install。最简单的方法是下载distribute_setup.py文件并运行它。运行这个文件最简单的方法就是打开你的下载文件夹并且双击这个文件。

下一步,把Python安装目录下的Scripts文件夹添加到PATH环境变量中,这样easy_install命令和其它Python脚本就加入到了命令行自动搜索的路径。具体做法是:右键单击桌面上的“我的电脑”图标,选择“属性”,然后单击“高级系统设置”,然后单击“环境变量”按钮,最后双击“系统变量”栏中的“PATH变量”,并加入Python安装目录中的Scripts文件夹。确保你用分号把它和现有的值分隔开。假设你使用Python 3.5且为默认目录,添加下面的值:

1
;C:\Python35\Scripts

如此,你就搞定了!打开命令提示符并执行 easy_install 测试它是否正常工作。如果你开启了Windows系统中的用户账户控制,它应该会提示你使用管理员权限。现在你有了 easy_install ,你可以用它来安装virtualenv:
1
> easy_install virtualenv

三、Flask快速入门

1. 一个最小的应用

一个最小的 Flask 应用看起来会是这样:

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

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

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

把它保存为hello.py(或是类似的),然后用Python解释器来运行。确保你的应用文件名不是flask.py,因为这将与Flask本身冲突。
1
2
$ python hello.py
* Running on http://127.0.0.1:5000/

现在访问http://127.0.0.1:5000/,你会看见Hello World问候。

2. 调试模式

虽然 run() 方法适用于启动本地的开发服务器,但是你每次修改代码后都要手动重启它。这样并不够优雅,而且Flask可以做到更好。如果你启用了调试支持,服务器会在代码修改后自动重新载入,并在发生错误时提供一个相当有用的调试器。有两种途径来启用调试模式,一种是直接在应用对象上设置:

1
2
app.debug = True
app.run()

另一种是作为 run 方法的一个参数传入:
1
app.run(debug=True)

两种方法的效果完全相同。

3. 路由

现代Web应用的URL十分优雅,易于人们辨识记忆,这一点对于那些面向使用低速网络连接移动设备访问的应用特别有用。如果可以不访问索引页,而是直接访问想要的那个页面,他们多半会笑逐颜开而再度光顾。

如上所见,route()装饰器把一个函数绑定到对应的URL上。来看一些基本的例子:

1
2
3
4
5
6
7
@app.route('/')
def index():
return 'Index Page'

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

但是,不仅如此!你可以构造含有动态部分的URL,也可以在一个函数上附着多个规则。

3.1 变量规则

要给URL添加变量部分,你可以把这些特殊的字段标记为<variable_name>,这个部分将会作为命名参数传递到你的函数。规则可以用<converter:variable_name>指定一个可选的转换器。这里有一些不错的例子:

1
2
3
4
5
6
7
8
9
@app.route('/user/<username>')
def show_user_profile(username):
# show the user profile for that user
return 'User %s' % username

@app.route('/post/<int:post_id>')
def show_post(post_id):
# show the post with the given id, the id is an integer
return 'Post %d' % post_id

转换器有下面几种:

int     接受整数
float   同 int ,但是接受浮点数
path    和默认的相似,但也接受斜线

3.2 构造URL

如果Flask能匹配URL,那么Flask可以生成它们吗?当然可以。你可以用url_for() 来给指定的函数构造URL。它接受函数名作为第一个参数,也接受对应URL规则的变量部分的命名参数。未知变量部分会添加到URL末尾作为查询参数。这里有一些例子:

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, url_for
>>> app = Flask(__name__)
>>> @app.route('/')
... def index(): pass
...
>>> @app.route('/login')
... def login(): pass
...
>>> @app.route('/user/<username>')
... def profile(username): pass
...
>>> with app.test_request_context():
... print url_for('index')
... print url_for('login')
... print url_for('login', next='/')
... print url_for('profile', username='John Doe')
...
/
/login
/login?next=/
/user/John%20Doe

(这里用到了test_request_context()方法,后面会解释。即使我们正在通过Python的shell进行交互,它依然会告诉Flask要表现为正在处理一个请求。)

为什么你要构建URL而非在模板中硬编码?这里有三个绝妙的理由:

  1. 反向构建通常比硬编码的描述性更好。更重要的是,它允许你一次性修改 URL, 而不是到处边找边改。
  2. URL构建会转义特殊字符和Unicode数据,免去你很多麻烦。
  3. 如果你的应用不位于URL的根路径(比如,在/myapplication下,而不是/),url_for()会妥善处理这个问题。

3.3 HTTP方法

HTTP(与Web应用会话的协议)有许多不同的访问URL方法。默认情况下,路由只回应GET请求,但是通过route()装饰器传递 methods 参数可以改变这个行为。这里有一些例子:

1
2
3
4
5
6
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
do_the_login()
else:
show_the_login_form()

如果存在 GET ,那么也会替你自动地添加 HEAD,无需干预。它会确保遵照 HTTP RFC (描述 HTTP 协议的文档)处理 HEAD 请求,所以你可以完全忽略这部分的 HTTP 规范。同样,自从 Flask 0.6 起, 也实现了 OPTIONS 的自动处理。

HTTP方法(也经常被叫做“谓词”)告知服务器,客户端想对请求的页面做些什么。下面的都是非常常见的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
GET
浏览器告知服务器:只获取页面上的信息并发给我。这是最常用的方法。
HEAD
浏览器告诉服务器:欲获取信息,但是只关心消息头。应用应像处理GET请求一样来处理它,但是不分发
实际内容。在Flask中你完全无需人工干预,底层的Werkzeug库已经替你打点好了。
POST
浏览器告诉服务器:想在URL上发布新信息。并且,服务器必须确保数据已存储且仅存储一次。这是HTML
表单通常发送数据到服务器的方法。
PUT
类似POST但是服务器可能触发了存储过程多次,多次覆盖掉旧值。你可能会问这有什么用,当然这是有
原因的。考虑到传输中连接可能会丢失,在这种情况下浏览器和服务器之间的系统可能安全地第二次接
收请求,而不破坏其它东西。因为POST它只触发一次,所以用POST是不可能的。
DELETE
删除给定位置的信息。
OPTIONS
给客户端提供一个敏捷的途径来弄清这个URL支持哪些HTTP方法。从Flask 0.6开始,实现了自动处理。

有趣的是,在HTML4和XHTML1中,表单只能以GET和POST方法提交到服务器。但是JavaScript和未来的HTML标准允许你使用其它所有的方法。此外,HTTP最近变得相当流行,浏览器不再是唯一的HTTP客户端。比如,许多版本控制系统就在使用HTTP。

4. 静态文件

动态web应用也会需要静态文件,通常是CSS和JavaScript文件。理想状况下,你已经配置好Web服务器来提供静态文件,但是在开发中,Flask也可以做到。只要在你的包中或是模块的所在目录中创建一个名为 static 的文件夹,在应用中使用 /static 即可访问。比如给静态文件生成URL,使用特殊的 ‘static’ 端点名:

1
url_for('static', filename='style.css')

这个文件应该存储在文件系统上的 static/style.css

5. 模板渲染

用Python生成HTML十分无趣,而且相当繁琐,因为你必须手动对HTML做转义来保证应用的安全。为此,Flask配备了Jinja2模板引擎。你可以使用render_template()方法来渲染模板。你需要做的一切就是将模板名和你想作为关键字的参数传入模板的变量。这里有一个展示如何渲染模板的简例:

1
2
3
4
5
6
from flask import render_template

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

Flask会在templates文件夹里寻找模板。所以,如果你的应用是个模块,这个文件夹应该与模块同级;如果它是一个包,那么这个文件夹作为包的子目录:

情况1:模块

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

情况2:包
1
2
3
4
/application
/__init__.py
/templates
/hello.html

关于模板,你可以发挥Jinja2模板的全部实例。更多信息请见Jinja2模板文档。这里有一个模板实例:
1
2
3
4
5
6
7
<!doctype html>
<title>Hello from Flask</title>
{% if name %}
<h1>Hello {{ name }}!</h1>
{% else %}
<h1>Hello World!</h1>
{% endif %}

在模板里,你也可以访问 requestsessiong 对象,以及 get_flashed_messages() 函数。模板继承让模板用起来相当顺手。如欲了解继承的工作机理,请查阅模板继承模式相关的文档。最起码,模板继承能使特定元素(比如页眉、导航栏和页脚)可以出现在所有的页面。

6. 访问请求数据

对于Web应用,与客户端发送给服务器的数据交互至关重要。在Flask中由全局的 request 对象来提供这些信息。如果你有一定的Python经验,你会好奇,为什么这个对象是全局的,为什么Flask还能保证线程安全。答案是环境作用域。

6.1 环境局部变量

Flask中的某些对象是全局对象,但却不是通常的那种。这些对象实际上是特定环境的局部对象的代理。虽然很拗口,但实际上很容易理解。

想象一下处理线程的环境。一个请求传入,Web服务器决定生成一个新线程(或者别的什么东西,只要这个底层的对象可以胜任并发系统,而不仅仅是线程)。当Flask开始它内部的请求处理时,它认定当前线程是活动的环境,并绑定当前的应用和WSGI环境到那个环境上(线程)。它的实现很巧妙,能保证一个应用调用另一个应用时不会出现问题。

所以,这对你来说意味着什么?除非你要做类似单元测试的东西,否则你基本上可以完全无视它。你会发现依赖于一段请求对象的代码,因没有请求对象无法正常运行。解决方案是,自行创建一个请求对象并且把它绑定到环境中。单元测试的最简单的解决方案是:用 test_request_context() 环境管理器。结合 with 声明,绑定一个测试请求,这样你才能与之交互。下面是一个例子:

1
2
3
4
5
6
7
from flask import request

with app.test_request_context('/hello', method='POST'):
# now you can do something with the request until the
# end of the with block, such as basic assertions:
assert request.path == '/hello'
assert request.method == 'POST'

另一种可能是:传递整个WSGI环境给 request_context() 方法:
1
2
3
4
from flask import request

with app.request_context(environ):
assert request.method == 'POST'

6.2 请求对象

你可以使用全局 request 对象访问进入的请求数据。 Flask处理进入的请求数据并允许你用这个全局对象访问它。如果你工作在多线程环境,Flask内部保证你总会在当前线程上获取正确的数据。首先从flask模块里导入它:

1
from flask import request

当前请求的HTTP方法可通过 method 属性来访问。通过 flask.request.form 属性来访问表单数据(POSTPUT 请求提交的数据)。这里有一个用到上面提到的那两个属性的完整实例:
1
2
3
4
5
6
7
8
9
10
11
12
@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'
# the code below is executed if the request method
# was GET or the credentials were invalid
return render_template('login.html', error=error)

当访问 form 属性中的不存在的键会发生什么?会抛出一个特殊的 KeyError 异常。你可以像捕获标准的 KeyError 一样来捕获它。 如果你不这么做,它会显示一个 HTTP 400 Bad Request 错误页面。所以,多数情况下你并不需要干预这个行为。

你可以通过 args 属性来访问URL中提交的参数( ?key=value ),比如:

1
searchword = request.args.get('q', '')

推荐大家都使用 get 来访问 URL参数或捕获 KeyError ,因为用户可能会修改URL,向他们展现一个 400 bad request 页面会影响用户体验。

6.3 文件上传

用 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))
...

6.4 Cookies

你可以通过 cookies 属性来访问Cookies,用响应对象的 set_cookie 方法来设置Cookies。请求对象的 cookies 属性是一个内容为客户端提交的所有Cookies的字典。如果你想使用会话,请不要直接使用Cookies,请参考会话一节。下面看一下使用Cookies的例子:

读取 cookies:

1
2
3
4
5
6
7
from flask import request

@app.route('/')
def index():
username = request.cookies.get('username')
# use cookies.get(key) instead of cookies[key] to not get a
# KeyError if the cookie is missing.

存储 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() 函数然后再进行修改。为此,也可以阅读关于响应 一节。

7. 重定向和错误

你可以用 redirect() 函数把用户重定向到其它地方。放弃请求并返回错误代码,用 abort() 函数。这里是一个它们如何使用的例子:

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

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

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

这是一个相当无意义的例子因为用户会从主页重定向到一个不能访问的页面(401意味着禁止访问),但是它展示了重定向是如何工作的。

默认情况下,错误代码会显示一个黑白的错误页面。如果你要定制错误页面,可以使用 errorhandler() 装饰器:

1
2
3
4
5
from flask import render_template

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

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

8. 关于响应

视图函数的返回值会被自动转换为一个响应对象。如果返回值是一个字符串,它被转换为该字符串为主体的、状态码为 200 OK 的、 MIME 类型是 text/html 的响应对象。Flask把返回值转换为响应对象的逻辑是这样:

1
2
3
4
5
6
1. 如果返回的是一个合法的响应对象,它会从视图直接返回。
2. 如果返回的是一个字符串,响应对象会用字符串数据和默认参数创建。
3. 如果返回的是一个元组,且元组中的元素可以提供额外的信息。这样的元组必须是 (response, status,
headers) 的形式,且至少包含一个元素。status值会覆盖状态代码,headers可以是一个列表或字典,
作为额外的消息标头值。
4. 如果上述条件均不满足, 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

9. 会话

除请求对象之外,还有一个 session 对象。它允许你在不同请求间存储特定用户的信息。它是在Cookies的基础上实现的,并且对Cookies进行密钥签名。这意味着用户可以查看你Cookie的内容,但却不能修改它,除非用户知道签名的密钥。要使用会话,你需要设置一个密钥。这里介绍会话如何工作:

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.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 action="" method="post">
<p><input type=text name=username>
<p><input type=submit value=Login>
</form>
'''

@app.route('/logout')
def logout():
# remove the username from the session if it's there
session.pop('username', None)
return redirect(url_for('index'))

# set the secret key. keep this really secret:
app.secret_key = 'A0Zr98j/3yX R~XHH!jmN]LWX/,?RT'

这里提到的 escape() 可以在你模板引擎外做转义(如同本例)。

使用基于 cookie 的会话需注意: Flask会将你放进会话对象的值序列化至Cookies。如果你发现某些值在请求之间并没有持久存在,然而确实已经启用了Cookies,但也没有得到明确的错误信息。这时,请检查你的页面响应中的Cookies的大小,并与Web浏览器所支持的大小对比。

10. 消息闪现

反馈,是良好的应用和用户界面的重要构成。如果用户得不到足够的反馈,他们很可能开始厌恶这个应用。Flask提供了消息闪现系统,可以简单地给用户反馈。消息闪现系统通常会在请求结束时记录信息,并在下一个(且仅在下一个)请求中访问记录的信息。展现这些消息通常结合要模板布局。

使用 flash() 方法可以闪现一条消息。要操作消息本身,请使用 get_flashed_messages() 函数,并且在模板中也可以使用。比如下面的例子:

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

app = Flask(__name__)
app.secret_key = 'some_secret'

@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)

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

11. 日志记录

有时候你会处于这样一种境地,你处理的数据本应该是正确的,但实际上不是。比如,你会有一些向服务器发送请求的客户端代码,但请求显然是畸形的。这可能是用户篡改了数据,或是客户端代码的粗制滥造。大多数情况下,正常地返回 400 Bad Request 就可以了,但是有时候不能这么做,并且要让代码继续运行。

你可能依然想要记录下,是什么不对劲。这时日志记录就派上了用场。从Flask 0.3开始,Flask就已经预置了日志系统。这里有一些调用日志记录的例子:

1
2
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的文档

12. 整合WSGI中间件

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

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


...

...

00:00
00:00