Flask 快速上手
# 安装
# Python版本
推荐使用最新版本的Python,Flask支持Python3.9以上版本
# 依赖
当安装Flask时,以下配套软件会被自动安装:
Werkzeug: 用于实现WSGI,应用和服务之间的标准Python接口Jinja: 用于渲染页面的模板语言MarkupSafe: 与Jinja共用,在渲染页面时用于避免不可信的输入,防止注入攻击。ItsDangerous: 保证数据完整性的安全标志数据,用于保护Flask的session cookieClick: 是一个命令行应用的框架,用于提供flask命令,并允许添加自定义管理命令。Blinker: 提供对于信号的支持。
# 可选依赖
以下配套软件不会被自动安装,如果安装了,那么Flask会检测到这些软件:
python-dotenv: 当运行flask命令时为通过dotenv设置环境变量提供支持。Watchdog: 为开发服务器提供快速高效的重载。
# greenlet
可以选择使用gevent或eventlet来服务应用,这种情况下,greenlet>=1.0是必须的,当使用PyPy时,PyPy>=7.3.7是必须的。上述版本是指支持的最小版本,应当尽量使用最新的版本。
# 虚拟环境
建议在开发环境和生产环境下都使用虚拟环境来管理项目的依赖。随着Python项目越来越多,开发者会发现不同的项目会需要不同版本的Python库,同一个Python库的不通过版本可能不兼容。
虚拟环境可以为每一个项目安装独立的Python库,这样可以隔离不同项目之间的Python库,也可以隔离项目与操作系统之间的Python库。Python内置了用于创建虚拟环境的venv模块。
# 创建一个虚拟环境
- macOS/Linux
$> mkdir myproject
$> cd myproject
$> python3 -m venv .venv
2
3
- Windows
$> mkdir myproject
$> cd myproject
$> py -3 -m venv .venv
2
3
# 激活虚拟环境
在开始工作前,需要先激活响应的虚拟环境:
# macOS/Linux
$> . .venv/bin/activate
# Windows
$> .venv\Scripts\activate
2
3
4
激活后,终端提示符会显示当前虚拟环境的名称。
# 安装Flask
在已激活的虚拟环境中可以使用如下命令安装Flask:
$> pip install Flask
# 快速上手
# 一个最小的应用
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello_world():
return "<p>Hello, World!</p>"
2
3
4
5
6
7
解释:
- 首先导入
Flask类,该类的实例将会成为WSGI应用。 - 接着创建一个该类的实例,第一个参数是应用模块或者包的名称。
__name__是一个适用于大多数情况的快捷方式,有了这个参数,Flask才能知道在哪里可以找到模板和静态文件等。 - 然后适用
route()装饰器告诉Flask触发函数的URL. - 函数返回需要在用户浏览器中显示的信息,默认的内容类型是HTML,因此字符串中的HTML会被浏览器渲染。
把它保存为hello.py或其他类似的名称,但不要使用flask.py作为应用名称,这会与Flask本身发生冲突。可以使用flask命令或者python -m flask来运行这个应用,但需要使用--app选项告诉Flask哪里可以找到应用。
$> flask --app hello run
# 应用发现行为
作为一个捷径,如果文件名为app.py或wsgi.py,那么可以不需要--app。
这样就启动了一个非常简单的内建服务器,这个服务器用于测试足够,但是不可用于生产环境。
# 外部可见的服务器
运行服务器后,会发现只有自己的电脑可以使用服务,而网络中的其他电脑不行。缺省设置就是这样,因为在调试模式下该应用的用户可以执行电脑中的任意Python代码。如果关闭了调试器或信任网络中的用户,可以让服务器被公开访问,只要在命令行简单加上--host=0.0.0.0即可:
$> flask run --host=0.0.0.0 # 这告诉操作系统鉴定所有公开的IP
# 调试模式
flask run命令不只可以启动开发服务器,如果打开了调试模式,那么服务器会在修改应用代码之后自动重启,并且当请求过程发生错误时还会在浏览器中提供一个交互调试器。
调试器允许执行来自浏览器的任意Python代码,虽然由一个pin保护,但仍然存在巨大安全风险,不要在生产环境中运行开发服务器或调试器。
如果要打开调试模式,使用--debug选项。
$> flask run --host=0.0.0.0 --debug
# HTML转义
当返回HTML(Flask默认响应类型)时,为了防止注入攻击,所有用户提供的值在输出渲染前必须被转义。使用Jinja渲染的HTML模板会自动执行此操作。在下面展示的escape()可以手动转义。因为保持简洁的原因,在多数示例中它被省略了,但应该始终留心处理不可信的数据。
from markupsafe import escape
@app.route("/<name>")
def hello(name):
return f"Hello, {escape(name)}!"
2
3
4
5
如果一个用户想要提交名称为<script>alert("bad")</script>,那么宁可转义为文本,也好过在浏览器执行脚本。路由中的<name>从URL中捕获值并将其传递给视图函数。
# 路由
现代web应用都使用有意义的URL,这样有助于用户记忆。使用route()装饰器来把函数绑定到URL:
@app.route('/')
def index():
return 'Index Page'
@app.route('/hello')
def hello():
return 'Hello, World'
2
3
4
5
6
7
能做的不仅仅是这些,可以动态变化URL的某些部分,还可以为一个函数指定多个规则。
# 变量规则
通过把URL的一部分标记为<variable_name>就可以在URL中添加变量,标记的部分会作为关键字参数传递给函数。还可用过使用<converter:variable_name>,可以选择性的加上一个转换器,为变量指定规则,如:
from markupsafe import escape
@app.route('/usr/<username>')
def show_user_profile(username):
# show the user profile for that user
return f'User {escape(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 f'Post {post_id}'
@app.route('/path/<path:subpath>')
def show_subpath(subpath):
# show the subpath after /path/
return f'Subpath {escape(subpath)}'
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
转换器类型:
| 类型 | 说明 |
|---|---|
string | (缺省值)接收任何不包含斜杠的文本 |
int | 接收正整数 |
float | 接收正浮点数 |
path | 类似string,但可以包含斜杠 |
uuid | 接收UUID字符串 |
# 唯一的URL/重定向行为
以下两条规则的不同之处在于是否使用尾部的斜杠。
@app.route('/projects/')
def projects():
return 'The project page'
@app.route('/about')
def about():
return 'The about page'
2
3
4
5
6
7
projects的URL是中规中矩的,尾部有一个斜杠,如同一个文件夹。访问一个没有斜杠结尾的URL(/projects)时Flask会自动进行重定向,自动在尾部加上一个斜杠/projects/。
about的URL尾部没有斜杠,因此其行为表现与一个文件类似。如果访问这个URL时添加了尾部斜杠/about/,就会得到一个404错误。这样可以保持URL唯一,并有助于搜索引擎重复索引同一页面。
# URL构建
url_for()函数用于构建指定函数的URL。它把函数名称作为第一个参数。可以接受任意个关键字参数,每个关键字参数对应URL中的变量,未知变量将添加到URL中作为查询参数。
那么为什么不把URL写死在模板中,而是使用反转函数url_for()动态构建?
- 反转通常比硬编码URL的描述性更好。
- 可以只在一个地方改变URL,而不用导出乱找。
- URL创建会处理特殊字符的转义,比较直观。
- 生产的路径总是绝对路径,可以避免相对路径产生副作用。
- 如果应用是放在URL根路径之外的地方(如在
/mmyapplication中,不在/中),url_for()会妥善处理。
例如,使用test_request_context()方法来尝试使用url_for(),
from flask import Flask, url_for
app = Flask(__name__)
@app.route('/')
def index():
return 'index'
@app.route('/login')
def login():
return 'login'
@app.route('/user/<username>')
def profile(username):
return f'{username}\'s profile'
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'))
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# HTTP方法
Web应用使用不同的HTTP方法处理URL。当使用Flask时,应当熟悉HTTP方法,缺省一个路由只回应GET请求。可以使用route()装饰器的methods参数来处理不同的HTTP方法。
from flask import request
app = Flask(__name__)
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method = 'POST':
return do_the_login()
else:
return show_the_login_form()
2
3
4
5
6
7
8
9
10
上例把路由所有方法都放在一个函数中,当每个方法都使用一些共同的数据时,这样是有用的。当然也可以把不同方法所对应的视图分别放在独立的函数中。Flask为每个常用的HTTP方法提供了捷径,如get(), post()等。
@app.get('/login')
def login_get():
return show_the login_form()
@app.post('/login')
def login_post():
return do_the_login()
2
3
4
5
6
7
如果当前使用了GET方法,Flask会自动添加HEAD方法支持,并且同时还会按照HTTP RFC来处理HEAD请求,同样OPTIONS也会自动实现。
# 静态文件
动态的web应用也需要静态文件,一般是CSS和JavaScript文件。理想情况下服务器已经配置好了静态文件服务,但在开发过程中,Flask也能做好这项工作。只要在包或模块叛变创建一个名为static的文件夹就行。静态文件位于应用的/static中,使用特定的'static'端点就可以生成响应的URL:
url_for('static', filename='style.css') # 这个静态文件在文件系统中的位置应该是static/style.css
# 渲染模板
在Python内部生成HTML相当笨拙,因为必须自己负责HTML转义,以确保应用的安全。因此,Flask自动配置Jinja2模板引擎。
模板可被用于生成任何类型的文本文件,对于web应用来说,主要用于生成HTML页面,但也可以生成markdown、用于电子邮件的纯文本等。
使用render_template()方法可以渲染模板,只要提供模板名称和需要作为参数传递给模板的变量就行。如:
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/hello/')
@app.route('/hello/<name>')
def hello(name=None):
return render_template('hello.html', person=name)
2
3
4
5
6
7
8
Flask会在templates文件夹内寻找模板,因此,如果应用是一个模块,那么模板文件夹应该在模块旁边,如果是一个包,那么就应该在包里面:
- 情形一: 一个模块
/application.py
/templates
/hello.html
2
3
- 情形二: 一个包
/application
/__init__.py
/templates
/hello.html
2
3
4
可以充分使用Jinja2模板引擎的为例,如:
<!doctype html>
<title>Hello From Flask</title>
{% if person %}
<h1>Hello {{ person }}!</h1>
{% else %}
<h1>Hello, World!</h1>
{% endif %}
2
3
4
5
6
7
在模板内部可以像使用url_for()和get_flashed_messages()函数一样访问config、request、session和g对象。模板在继承使用的情况下尤其有用,模板继承可以使每个页面的特定元素(如页头、导航和页尾)保持一致。自动转义默认开启,如果person包含HTML,那么会被自动转义。如果信任某个变量,且知道它是安全的HTML,那么可以使用Markup类把它标记为安全的,或者在模板中使用| safe过滤器。下面是Markup类的基本使用方法:
from markupsafe import Markup
Markup('<string>Hello %s!</string>') % '<blink>hacker</blink>'
# Markup('<strong>Hello <blink>hacker</blink>!</strong>')
Markup.escape('<blink>hacker</blink>')
# Markup('<blink>hacker</blink>')
Markup('<em>Marked up</em> » HTML').striptags()
# 'Marked up >> HTML'
2
3
4
5
6
7
g对象是某个可以根据需要储存信息的东西。
# 操作请求数据
对于web应用来说对客户端向服务器发送的数据做出响应很重要。在Flask中由全局对象request来提供请求信息。那么既然这个对象是全局的,怎么保持线程安全呢?
# 本地环境
某些对象在Flask中是全局对象,但不是通常意义上的全局对象。这些对象实际上是特定环境下本地对象的代理。
设想现在处于处理线程的环境中,一个请求进来,服务器就需要生成一个新线程(或者其他什么名称的东西,这个底层的东西能够处理包括线程在内的并发系统)。当Flask开始其内部请求处理时会把当前线程作为活动环境,并把当前应用和WSGI环境绑定到这个环境(线程)。其以一种聪明的方式使得一个应用可以在不中断的情况下调用另一个应用。
这个只有在做单元测试时才有用。在测试时会遇到由于没有请求对象而导致依赖于请求的代码会突然崩溃。解决方案是自己创建一个请求对象并绑定到环境。最简单的单元测试解决方案是使用test_request_context()环境管理器。通过使用with语句可以绑定一个测试请求,以便交互。如:
from flask imort Flask, request
app = Flask(__name__)
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'
2
3
4
5
6
7
8
另一种方式是把整个WSGI环境传递给request_context()方法:
with app.request_context(environ):
assert request.method == 'POST'
2
# 请求对象
请求对象在API章节有详细说明,下面简单介绍: from flask import request
通过使用method属性可以操作当前请求方法,使用form属性处理表单数据(在POST或者PUT请求中传输的数据),如下:
@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)
2
3
4
5
6
7
8
9
10
11
当form属性中不存在获取的键时,会引发一个KeyError。如果不像捕获一个标准错误一样捕获KeyError,那么会显示HTTP 400 Bad Request错误页面。
要操作URL(如?key=value)中提交的参数可以使用args属性: searchword = request.args.get('key')
# 文件上传
用Flask处理文件上传很容易,只要确保在HTML表单中设置enctype="multipart/form-data"属性,否则浏览器不会传输文件。
已上传的文件被储存在内存或文件系统的临时位置,可以通过请求对象files属性来访问上传的文件。每个上传的文件都储存在这个字典型属性中,这个属性基本和标准Python file对象一样,另外多出一个用于把上传文件保存到服务器的文件系统中的save()方法:
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')
2
3
4
5
6
7
如果需要知道文件上传之前在客户端系统中的名称,可以使用filename属性。但这个值是可以伪造的,如果希望把客户端的文件名作为服务器上的文件名,可以通过Werkzeug提供的secure_filename()函数:
from werkzeug.utils import secure_filename
@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
if request.method == 'POST':
file = request.files['the_file']
file.save(f"/var/www/uploads/{secure_filename(file.filename)}")
2
3
4
5
6
7
# Cookies
访问cookies,可以使用cookies属性,可以使用响应对象的set_cookie方法来设置cookies。请求对象的cookies属性是一个包含了客户端传输的所有cookies字典。在Flask中,如果使用会话,就不要直接使用cookies,因为会话会更安全。
- 读取Cookies:
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
2
3
4
5
6
- 储存Cookies:
from flask import make_response
@app.route('/')
def index():
resp = make_response(render_template(...))
resp.set_cookie('username', 'the username')
return resp
2
3
4
5
6
7
注意,cookies设置在响应对象上,通常知识从视图函数返回字符串,Flask会把它们转换为响应对象,如果希望显示转换,可以使用make_response()函数,然后在修改它。
# 重定向和错误
使用redirect()函数可以重定向,使用abort()可以更早退出请求,并返回错误代码:
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()
2
3
4
5
6
7
8
9
10
上例是没有实际意义的,它让一个用户从索引页重定向到一个无法访问的页面(401表示禁止访问),但是其可以说明重定向和出错跳出是如何工作的。缺省情况下每种出错代码都会对应显示一个黑白的出错页面,使用errorHandler()装饰器可以定制出错页面:
@app.errorhandler(404)
def page_not_found(error):
return render_template('page_not_found.html'), 404
2
3
注意render_template()后面的404,表示页面对应的出错代码是404.
# 关于响应
视图函数的返回值会自动转换为一个响应对象,如果返回值是一个字符串,那么会被转换为一个包含作为响应体的字符串、一个200 OK代码和一个text/html类型的响应对象。如果返回值是一个字典或列表,那么会调用jsonify()生成一个而相应,遵循以下的规则:
- 如果视图返回的是一个响应对象,就直接返回
- 如果返回的是一个字符串,那么根据这个字符串和缺省参数生成一个用于返回的响应对象。
- 如果返回的是一个迭代器或者生成器,那么返回字符串或者字节,作为流响应对待。
- 如果返回一个字典或者列表,那么使用
jsonify()创建一个响应对象。 - 如果返回一个元组,那么元组中的项目可以提供额外的信息。元组中必须至少包含一个项目,且项目应当由
(response, status),(response, headers)或(response, status, headers)组成。status的值会重载状态代码,headers是一个由额外头部值组成的列表或字典。 - 如果都不是,那么Flask会假定返回值是一个有效的WSGK应用并把它转换为一个响应对象。
如果希望在视图内部掌控响应对象的结果,可以使用make_response()函数。如:
@app.errorhandler(404)
def not_found(error):
return render_template('error.html'), 404
2
3
可以使用make_response()包裹返回表达式,获得响应对象,并对该对象进行修改,然后再返回:
@app.errorhandler(404)
def not_found(error):
resp = make_response(render_template('error.html'), 404)
resp.headers['X-Something'] = 'A value'
return resp
2
3
4
5
# JSON格式的API
JSON格式的响应是最常见的,如果从视图返回一个dict或list,那么就会被转换为一个JSON响应.
@app.route('/me')
def me_api():
user = get_current_user()
return {
"username": user.username,
"theme": user.theme,
"image": url_for("user_image", filename=user.image)
}
2
3
4
5
6
7
8
如果dict还不能满足要求,还需要创建其他类型的JSON格式响应,可以使用jsonify()函数。该函数会序列化任何支持JSON数据类型:
@app.route('/users')
def users_api:
users = get_all_users()
return [user.to_json() for user in users]
2
3
4
这是一个向jsonify()函数传递数据的捷径,可以序列化任何支持JSON数据类型,但也意味着在字典和列表中的所有数据必须可以被序列化。对于复杂的数据类型,如数据库模型,需要使用序列化库先把数据转换为合法的JSON。
# 会话
除了请求对象之外还有一种称为session的对象,允许在不同请求之间储存信息,这个对象相当于用密钥签名加密的cookie,即用户可以查看cookie,但是如果没有密钥就无法修改它。因此,使用会话之前必须设置一个密钥,如:
from flask import session
# Set the secret key to some random bytes. Keep this really secret!
app.secret_key = b'_5#y2L"F4Q8z\n\xec]\'
@app.route('/')
def index():
if 'username' in session:
return f'Logged in as {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>
<p><input type=submit value=Login /></p>
</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'))
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
:::info 如何生成一个好的密钥 生成随机数的关键在于一个好的随机种子,因此一个好的密钥应当有足够的随机性。操作系统可以有多种方式基于密码随机生成器来生成随机数据。如:
$> python -c 'import secrets; print(secrets.token_hex())'
:::
基于cookie的会话的说明: Flask会取出会话对象中的值,把值序列化后储存到cookie中。在打开cookie的情况下,如果查找某个值,如果这个值在请求中没有持续储存,就不会得到一个清晰的出错信息。可以检查页面响应中的cookie的大小是否与网络浏览器所支持的大小一致。
# 消息闪现
一个好的应用和用户接口都有良好的反馈,Flask通过闪现系统来提供一个易用的反馈方式。闪现系统的基本工作原理是在请求结束时记录一个消息,提供且只提供给下一个请求使用。通常通过一个布局模块来展现闪现的消息。
flash()用于闪现一个消息,在模板中,使用get_flashed_messages()来操作消息。
# 日志
app.logger.debug('A value for debugging')
app.logger.warning('A warning occurred (%d apples)', 42)
app.logger.error('An error occurred')
2
3
logger是一个标准的日志Logger类。
# 集成WSGI中间件
如果想要在应用中添加一个WSGI中间件,可以用应用的wsgi_app属性来包装,如: 假设需要在Nginx后面使用ProxyFix中间件,那么可以:
from werkzeug.middleware.proxy_fix import ProxyFix
app.wsgi_app = ProxyFix(app.wsgi_app)
2
用app.wsgi_app包装而不是app,意味着app依旧指向Flask应用,而不是指向中间件,这样可以继续直接使用和配置app。