23 KiB
Flask 中的表单处理
最后更新于 2020 年 7 月 27 日
表单是任何 web 应用必不可少的一部分,但不幸的是,使用表单非常困难。这一切都从客户端开始,首先,您必须在客户端验证数据,然后在服务器端。如果这还不够,您还需要考虑所有的安全问题,如 CSRF、XSS、SQL 注入等等。总而言之,这是一个很大的工作量。幸运的是,我们有一个名为 WTForms 的优秀库来为我们做这项繁重的工作。在我们了解更多关于 WTForms 的知识之前,下一节将为您介绍如何在 Flask 中处理表单,而无需使用任何库或包。
表单处理-艰难之路
使用以下代码创建名为login.html的新模板:
flask _ app/模板/login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Login</title>
</head>
<body>
{% if message %}
<p>{{ message }}</p>
{% endif %}
<form action="" method="post">
<p>
<label for="username">Username</label>
<input type="text" name="username">
</p>
<p>
<label for="password">Password</label>
<input type="password" name="password">
</p>
<p>
<input type="submit">
</p>
</form>
</body>
</html>
接下来,在main2.py中的books()查看功能后添加以下代码。
Flask _app/main2.py
from flask import Flask, render_template, request
#...
@app.route('/login/', methods=['post', 'get'])
def login():
message = ''
if request.method == 'POST':
username = request.form.get('username') # access the data inside
password = request.form.get('password')
if username == 'root' and password == 'pass':
message = "Correct username and password"
else:
message = "Wrong username or password"
return render_template('login.html', message=message)
#...
注意传递给route()装饰器的methods参数。默认情况下,只有当request.method为 GET 或 HEAD 时,才会调用请求处理程序。这可以通过将允许的 HTTP 方法列表传递给methods关键字参数来更改。从现在开始,login()视图功能
将只在使用 GET、POST 或 HEAD 方法请求/login/时调用。尝试使用任何其他方法访问/login/网址将导致 HTTP 405 方法不允许错误。
在之前的课程中,我们已经讨论过request对象提供了关于当前 web 请求的信息。通过表单提交的数据存储在request对象的form属性中。request.form是一个像字典一样不可变的对象,被称为ImmutableMultiDict。
启动服务器,访问http://localhost:5000/log in/。你应该看到这样一个表格。
使用 GET 请求请求页面,因此跳过了login()视图功能中 if 块
内的代码。
提交表单时无需输入任何内容,您应该会看到如下页面:
这一次页面是使用 POST 方法提交的,所以 if 块中的代码被执行。在 if 主体中,我们访问用户名和密码,并相应地设置message变量的值。因为我们提交了一个空表单,所以会显示一条错误消息。
用正确的用户名和密码填写表格,然后点击回车。您应该会收到如下"Correct username and password"信息:
这就是我们在 Flask 中处理表单的方式。现在让我们把注意力转移到 WTForms 包上。
WTForms
WTForms 是一个用 Python 编写的强大的框架无关(框架无关)库。它允许我们生成 HTML 表单、验证表单、用数据预填充表单(对编辑有用)等等。除此之外,它还为 CSRF 提供保护。为了安装 WTForms,我们使用了 Flask-WTF。
Flask-WTF 是一个将 Flask 和 WTForms 集成在一起的 Flask 扩展。Flask-WTF 还提供了一些附加功能,如文件上传、reCAPTCHA、国际化(i18n)等。要安装 Flask-WTF,请输入以下命令。
(env) overiq@vm:~/flask_app$ pip install flask-wtf
创建表单类
我们首先将表单定义为 Python 类。每个表单类都必须扩展flask_wtf包的FlaskForm类。FlaskForm是一个包装器,包含一些围绕原始wtform.Form类的有用方法,这是创建表单的基类。在表单类中,我们将表单字段定义为类变量。通过创建与字段类型相关联的对象来定义表单字段。wtform包提供了几个类来表示表单域,如StringField、PasswordField、SelectField、TextAreaField、SubmitField等。
在flask_app字典中创建一个新文件forms.py,并添加以下代码。
Flask _app/forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField, TextAreaField
from wtforms.validators import DataRequired, Email
class ContactForm(FlaskForm):
name = StringField("Name: ", validators=[DataRequired()])
email = StringField("Email: ", validators=[Email()])
message = TextAreaField("Message", validators=[DataRequired()])
submit = SubmitField("Submit")
这里我们定义了一个表单类ContactForm,它包含四个表单域:name、email、message和submit。这些变量将用于渲染表单字段,以及在字段之间设置和检索数据。使用两个StringField创建表单,一个TextAreaField和一个SubmitField。每次我们创建一个字段对象,我们都会传递一些参数给它的构造函数。第一个参数是一个包含标签的字符串,当表单域被渲染时,它将显示在<label>标签中。第二个可选参数是作为关键字参数传递给构造函数的验证器列表。验证器是确定字段中的数据是否有效的函数或类。我们可以通过逗号(,)将多个验证器应用于一个字段。wtforms.validators模块提供了一些基本的验证器,但是我们也可以创建自己的验证器。在这个表单中,我们使用了两个内置验证器DataRequired和Email。
数据要求:保证用户必须在字段中输入一些数据。
邮件:检查输入的数据是否为有效的邮件地址。
字段中的数据将不会被接受,直到对其应用的所有验证器都得到满足。
**注意:**我们几乎没有触及表单域和验证器的表面,要查看完整列表,请访问https://wtforms.readthedocs.io/en/master/。
设置密钥
默认情况下,Flask-WTF 阻止所有形式的 CSRF 攻击。它通过在表单中隐藏的<input>元素中嵌入一个标记来实现这一点。然后使用令牌来验证请求的真实性。在 Flask-WTF 可以生成 csrf 令牌之前,我们必须添加一个密钥。打开main2.py并按如下方式设置密钥:
Flask _app/main2.py
#...
app.debug = True
app.config['SECRET_KEY'] = 'a really really really really long secret key'
manager = Manager(app)
#...
这里我们使用的是Flask对象的config属性。config属性就像字典一样工作,它用于放置 Flask 和 Flask 扩展的配置选项,但是如果您愿意,也可以放置您自己的配置。
密钥应该是一个很长很难猜测的字符串。SECRET_KEY的使用不仅仅局限于创建 CSRF 代币,它还被 Flask 和许多其他扩展使用。密钥应该保密。与其将密钥存储在应用中,不如将它存储在环境变量中。我们将在后面的章节中学习如何做到这一点。
控制台中的表单
通过输入以下命令打开 Python shell:
(env) overiq@vm:~/flask_app$ python main2.py shell
这将在应用上下文中启动 Python shell。
现在导入ContactForm类,并通过向其传递表单数据来实例化一个新的表单对象。
>>>
>>> from forms import ContactForm
>>> from werkzeug.datastructures import MultiDict
>>>
>>>
>>> form1 = ContactForm(MultiDict([('name', 'jerry'),('email', 'jerry@mail.com')]))
>>>
请注意,我们将表单数据作为MultiDict对象传递,因为wtforms.Form类的构造函数接受类型为MultiDict的参数。如果在实例化表单对象时未指定表单数据,并且表单是使用 POST 请求提交的,则wtforms.Form将使用来自request.form属性的数据。回想一下request.form返回一个类型为ImmutableMultiDict的对象,该对象与MultiDict对象相同,但不可变。
表单对象的validate()方法验证表单。成功后返回True,否则返回False。
>>>
>>> form1.validate()
False
>>>
我们的表单未能通过验证,因为我们在创建表单对象时没有向所需的message字段提供任何数据。我们可以使用表单对象的errors属性来访问表单错误:
>>>
>>> form1.errors
{'message': ['This field is required.'], 'csrf_token': ['The CSRF token is missing.']}
>>>
请注意,除了message字段的错误消息外,输出还包含缺失 csrf 令牌的错误消息。这是因为我们在表单数据中没有带有 csrf 令牌的实际 POST 请求。
我们可以在实例化表单类时通过传递csrf_enabled=False来关闭表单上的 CSRF 保护。这里有一个例子:
>>>
>>> form3 = ContactForm(MultiDict([('name', 'spike'),('email', 'spike@mail.com')]), csrf_enabled=False)
>>>
>>> form3.validate()
False
>>>
>>> form3.errors
{'message': ['This field is required.']}
>>>
>>>
不出所料,现在我们只得到丢失的message字段的错误。让我们创建另一个表单对象,但这次我们将向所有表单字段提供有效数据。
>>>
>>> form4 = ContactForm(MultiDict([('name', 'jerry'), ('email', 'jerry@mail.com'), ('message', "hello tom")]), csrf_enabled=False)
>>>
>>> form4.validate()
True
>>>
>>> form4.errors
{}
>>>
这次表单验证成功。
我们的下一个逻辑步骤将是渲染接下来讨论的表单。
渲染表单
有两种方法可以渲染表单域:
- 逐个渲染字段。
- 使用 for 循环渲染字段。
逐个渲染字段
在模板中,一旦我们访问了表单实例,我们就可以使用字段名来渲染字段、标签和错误,如下所示:
{# render the label tag associated with field #}
{{ form.field_name.label() }}
{# render the field itself #}
{{ form.field_name() }}
{# render the validation errors associated with the field #}
{% for error in form.field_name.errors %}
{{ error }}
{% endfor %}
让我们在控制台内部测试一下:
>>>
>>> from forms import ContactForm
>>> from jinja2 import Template
>>>
>>> form = ContactForm()
>>>
这里我们已经实例化了没有任何请求数据的表单对象,这通常是第一次使用 GET 请求显示表单的情况。
>>>
>>>
>>> Template("{{ form.name.label() }}").render(form=form)
'<label for="name">Name: </label>'
>>>
>>> Template("{{ form.name() }}").render(form=form)
'<input id="name" name="name" type="text" value="">'
>>>
>>>
>>> Template("{{ form.email.label() }}").render(form=form)
'<label for="email">Email: </label>'
>>>
>>> Template("{{ form.email() }}").render(form=form)
'<input id="email" name="email" type="text" value="">'
>>>
>>>
>>> Template("{{ form.message.label() }}").render(form=form)
'<label for="message">Message</label>'
>>>
>>> Template("{{ form.message() }}").render(form=form)
'<textarea id="message" name="message"></textarea>'
>>>
>>>
>>> Template("{{ form.submit() }}").render(form=form)
'<input id="submit" name="submit" type="submit" value="Submit">'
>>>
>>>
由于表单是第一次显示,因此它的任何字段都不会有任何验证错误。下面的代码演示了这一点:
>>>
>>>
>>> Template("{% for error in form.name.errors %}{{ error }}{% endfor %}").render(form=form)
''
>>>
>>>
>>> Template("{% for error in form.email.errors %}{{ error }}{% endfor %}").render(form=form)
''
>>>
>>>
>>> Template("{% for error in form.message.errors %}{{ error }}{% endfor %}").render(form=form)
''
>>>
>>>
您可以使用form.errors来访问与表单相关的所有验证错误,而不是显示每个字段的验证错误。forms.errors常用于在表单顶部显示验证错误。
>>>
>>> Template("{% for error in form.errors %}{{ error }}{% endfor %}").render(form=form)
''
>>>
在渲染字段和标签时,我们还可以提供额外的关键字参数,这些参数将作为键值对注入到 HTML 中。例如:
>>>
>>> Template('{{ form.name(class="input", id="simple-input") }}').render(form=form)
'<input class="input" id="simple-input" name="name" type="text" value="">'
>>>
>>>
>>> Template('{{ form.name.label(class="lbl") }}').render(form=form)
'<label class="lbl" for="name">Name: </label>'
>>>
>>>
现在假设我们的表单已经提交。这次让我们尝试渲染字段,看看会发生什么。
>>>
>>> from werkzeug.datastructures import MultiDict
>>>
>>> form = ContactForm(MultiDict([('name', 'spike'),('email', 'spike@mail.com')]))
>>>
>>> form.validate()
False
>>>
>>>
>>> Template("{{ form.name() }}").render(form=form)
'<input id="name" name="name" type="text" value="spike">'
>>>
>>>
>>> Template("{{ form.email() }}").render(form=form)
'<input id="email" name="email" type="text" value="spike@mail.com">'
>>>
>>>
>>> Template("{{ form.message() }}").render(form=form)
'<textarea id="message" name="message"></textarea>'
>>>
>>>
请注意,name和email字段的value属性已填充数据。然而,message字段的<textarea>元素是空的,因为我们没有向它提供任何数据。我们可以访问message字段的验证错误,如下所示:
>>>
>>> Template("{% for error in form.message.errors %}{{ error }}{% endfor %}").render(form=form)
'This field is required.'
>>>
或者,您也可以使用form.errors一次遍历所有验证错误。
>>>
>>> s ="""\
... {% for field_name in form.errors %}\
... {% for error in form.errors[field_name] %}\
... <li>{{ field_name }}: {{ error }}</li>
... {% endfor %}\
... {% endfor %}\
... """
>>>
>>> Template(s).render(form=form)
'<li>csrf_token: The CSRF token is missing.</li>\n
<li>message: This field is required.</li>\n'
>>>
>>>
请注意,由于提交的请求没有 csrf 令牌,因此出现了 csrf 令牌丢失错误。我们可以像正常场一样渲染 csrf 场,如下所示:
>>>
>>> Template("{{ form.csrf_token() }}").render(form=form)
'<input id="csrf_token" name="csrf_token" type="hidden" value="IjZjOTBkOWM4ZmQ0M
GMzZTY3NDc3ZTNiZDIxZTFjNDAzMGU1YzEwOTYi.DQlFlA.GQ-PrxsCJkQfoJ5k6i5YfZMzC7k">'
>>>
如果您有相当多的表单域,逐个渲染域可能会很麻烦。对于这种情况,可以使用 For 循环来渲染字段。
使用循环渲染字段
下面的 shell 会话演示了如何使用 for 循环渲染字段。
>>>
>>> s = """\
... <div>
... {{ form.csrf_token }}
... </div>
... {% for field in form if field.name != 'csrf_token' %}
... <div>
... {{ field.label() }}
... {{ field() }}
... {% for error in field.errors %}
... <div class="error">{{ error }}</div>
... {% endfor %}
... </div>
... {% endfor %}
... """
>>>
>>>
>>> print(Template(s).render(form=form))
<div>
<input id="csrf_token" name="csrf_token" type="hidden" value="IjZjOTBkOW
M4ZmQ0MGMzZTY3NDc3ZTNiZDIxZTFjNDAzMGU1YzEwOTYi.DQlFlA.GQ-PrxsCJkQfoJ5k6i5YfZMzC7
k">
</div>
<div>
<label for="name">Name: </label>
<input id="name" name="name" type="text" value="spike">
</div>
<div>
<label for="email">Email: </label>
<input id="email" name="email" type="text" value="spike@mail.com">
</div>
<div>
<label for="message">Message</label>
<textarea id="message" name="message"></textarea>
<div class="error">This field is required.</div>
</div>
<div>
<label for="submit">Submit</label>
<input id="submit" name="submit" type="submit" value="Submit">
</div>
>>>
>>>
需要注意的是,无论使用哪种方法,都必须手动添加<form>标签来包装表单字段。
现在我们知道如何创建、验证和渲染表单。让我们利用这些知识创造一些真实的形式。
首先用以下代码创建一个新模板contact.html:
flask _ app/templates/contact . html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="" method="post">
{{ form.csrf_token() }}
{% for field in form if field.name != "csrf_token" %}
<p>{{ field.label() }}</p>
<p>{{ field }}
{% for error in field.errors %}
{{ error }}
{% endfor %}
</p>
{% endfor %}
</form>
</body>
</html>
拼图中唯一缺少的部分是我们接下来将创建的视图功能。
提交表格
打开main2.py,在login()查看功能后添加以下代码。
Flask _app/main2.py
from flask import Flask, render_template, request, redirect, url_for
from flask_script import Manager, Command, Shell
from forms import ContactForm
#...
@app.route('/contact/', methods=['get', 'post'])
def contact():
form = ContactForm()
if form.validate_on_submit():
name = form.name.data
email = form.email.data
message = form.message.data
print(name)
print(email)
print(message)
# db logic goes here
print("\nData received. Now redirecting ...")
return redirect(url_for('contact'))
return render_template('contact.html', form=form)
#...
在第 7 行,我们正在创建一个表单对象。在第 8 行,我们正在检查validate_on_submit()方法的返回值,以执行 if 语句体中的一些代码。
为什么我们使用validate_on_submit()而不是validate(),就像我们在控制台中做的那样?
validate()方法只是检查表单数据是否有效,并不检查请求是否使用 POST 方法提交。这意味着如果我们使用validate()方法,那么对/contact/的 GET 请求将触发表单验证,用户将在表单中看到验证错误。一般来说,只有在使用 POST 请求提交数据时,我们才会触发验证例程。当使用开机自检请求提交表单并且数据有效时,validate_on_submit()方法返回True。否则False。validate_on_submit()方法内部调用validate()方法。此外,请注意,我们在实例化表单对象时没有传递任何数据,因为当使用 POST 请求提交表单时,WTForms 会从request.form属性中读取表单数据。
表单类中定义的表单字段成为表单对象的属性。要访问字段数据,我们使用表单字段的data属性:
form.name.data # access the data in the name field.
form.email.data # access the data in the email field.
要一次访问所有表单数据,请使用表单对象的data属性:
form.data # access all the form data
当您使用 GET 请求访问 URL /contact/时,validate_on_submit()方法返回False,跳过 if 主体内的代码,向用户显示一个空的 HTML 表单。
当使用 POST 请求提交表单时,validate_on_submit()返回True,假设数据有效。if 主体内的print()调用打印用户输入的数据,redirect()功能将用户重定向到/contact/页面。另一方面,如果validate_on_submit()返回False,则跳过 if 主体内部语句的执行,并显示带有验证错误的表单。
启动服务器,如果还没有运行,访问http://localhost:5000/contact/。您应该会看到这样的联系人表单:
无需输入任何内容,点击提交,您将看到如下验证错误:
在名称和消息字段中输入一些数据,在电子邮件字段中输入无效数据,然后再次提交表单。
请注意,所有字段仍然包含来自上一个请求的数据。
在电子邮件字段中输入有效的电子邮件,然后点击提交。这一次我们的验证将会成功,在运行服务器的 shell 中,您应该会看到如下输出:
Spike
spike@gmail.com
A Message
Data received. Now redirecting ...
在 shell 中显示提交的数据后,查看功能会再次将用户重定向到/contact/ URL。此时,您应该会看到一个没有任何验证错误的空表单,就像您第一次使用 GET 请求访问/contact/网址一样。
成功提交表单后,向用户显示一些反馈是一个很好的做法。在 Flask 中,我们使用 flash 消息创建这样的反馈,这将在下面讨论。
Flash 消息
Flash 消息是另一种依赖于密钥的功能。密钥是必要的,因为在幕后,闪存消息存储在会话中。我们将在 Flask 中的课程中深入学习什么是会话以及如何使用它们。既然我们已经设置了密匙,我们就准备出发了。
要闪烁消息,我们使用flask包中的flash()功能。flash()函数接受两个参数,消息到 flash 和一个可选类别。类别表示消息类型,如成功、错误、警告等。该类别可在模板中用于确定要显示的警报消息的类型。
在contact()视图功能中打开main2.py并在redirect()调用前添加flash("Message Received", "success"),如下所示:
Flask _app/main2.py
from flask import Flask, render_template, request, redirect, url_for, flash
#...
# db logic goes here
print("\nData received. Now redirecting ...")
flash("Message Received", "success")
return redirect(url_for('contact'))
return render_template('contact.html', form=form)
flash()功能设置的消息仅适用于后续请求,然后将被删除。
我们现在正在设置 flash 消息,为了显示它,我们也必须修改我们的模板。
打开contact.html,修改文件如下:
flask _ app/templates/contact . html
<body>
{% for category, message in get_flashed_messages(with_categories=true) %}
<p class="{{ category }}">{{ message }}</p>
{% endfor %}
<form action="" method="post">
Jinja 提供了一个名为get_flashed_messages()的函数,该函数返回一个没有类别的未决 flash 消息列表。拨打get_flashed_messages()时,要获得带类别通行证with_categories=True的闪光信息。当with_categories设置为真时,get_flashed_messages()返回形式为(category, message)的元组列表。
进行这些更改后,再次访问http://localhost:5000/contact/。填写表格并点击提交。这一次,您应该会在表单顶部看到一条成功消息,如下所示:






