表单验证

这里以注册功能为例,讲解表单验证功能。注册时需要提交邮箱、用户名、密码、确认密码 4 个字段的数据。首先在 templates 文件夹中创建一个 register.html 文件,然后输入以下代码。

image 2025 01 21 16 04 15 993

以上代码中,首先创建了一个 form 标签,然后设置 action 为 url_for('register'),也就是将 register 视图函数反转为 URL,在以后单击 “提交” 按钮时,会把所有 form 标签下输入框中的内容都提交给这个 URL(看下文 register 视图函数)。接着还设置了 method 为 POST,这意味着会以 POST 方式提交。然后在 form 标签下,分别添加了属性 name 为 username、email、password 以及 confirm_password 的 input 标签。最后添加了一个 type="submit" 的 input 标签,被渲染出来是一个按钮。

模板写好后,再用一个视图函数渲染,示例代码如下。

@app.route("/register", methods=['GET', 'POST'])
def register():
    if request.method == 'GET':
        return render_template("register.html")
    else:
        pass

执行以上代码后,在浏览器中访问 http://127.0.0.1:5000/register ,即可看到如图 6-1 所示的效果。

image 2025 01 21 16 07 50 732
Figure 1. 图6-1 注册页面

表单类编写

到目前为止,我们完成了前端模板代码的编写,用户可以在此页面输入信息进行注册了。但是在此页面中,对每个字段是有一定要求的,具体要求如下。

  • 用户名:为了防止重名,一般要求最少要输入 3 位以上字符。

  • 邮箱:格式必须以 @+ 域名结尾。

  • 密码:要求最少输入 6 位以上字符。

  • 确认密码:该字段内容必须和密码字段内容一致。

我们不能要求用户一次性就正确输入满足这些规则的内容,如果用户输入错误,应该在界面中及时给予提示。这个工作可以由前端通过 JavaScript 来完成,但是服务器端也要做好验证,因为对于有一定技术功底的用户来说,可以通过抓包的形式获取注册时的请求数据,然后通过代码或者工具来模拟注册,这就完全绕开了前端的 JavaScript 验证。服务器端的验证可以通过 WTForms 来实现。首先在项目根路径下创建一个 forms.py 文件,然后写入以下代码。

from wtforms import Form, StringField
from wtforms.validators import Length, Email, EqualTo

class RegisterForm(Form):
    username = StringField(validators=[Length(min=3, max=20, message="请输入正确长度的用户名!")])
    email = StringField(validators=[Email(message="请输入正确格式的邮箱!")])
    password = StringField(validators=[Length(min=6, max=20, message="请输入正确长度的密码!")])
    confirm_password = StringField(validators=[EqualTo("password", message="两次密码不一致!")])

上述代码中,先从wtforms中导入Form基类,所有的表单类都必须继承自Form基类。然后在RegisterForm中分别添加了username、email、password以及confirm_password这4个字段,这里字段的名称必须和HTML模板中表单元素的name的值一致。如在HTML模板中邮箱的input标签的name值为email,那么在RegisterForm中字段的名称也必须为email。

这4个属性现在都是字符串类型,因此使用StringField类型,除StringField外,还有以下类型的Field类,如表6-1所示。

image 2025 01 21 16 12 25 922
Figure 2. 表6-1 Field类的类型

每个字段都传递了 validators 参数,这个参数是可以存储多个验证器的集合。不同的字段应根据实际需要设置不同的验证器。

  • username:添加了 length 验证器,用来规定最短字符串长度为 3,最长字符串长度为 20,并且如果上传的值不在这个范围,会提示一个错误信息,错误信息的内容就是 message 指定的值。

  • email:添加了 email 验证器,email 验证器会自动验证上传的值是否满足邮箱的格式规则。如果不满足,同样也会提示 message 指定的错误信息。

  • password:同样用的是 length 验证器,指定字符长度为 6~20。

  • confirm_password:确认密码用的是 equal_to 验证器,验证是否和 password 的值一致。

除了以上指定的 length、email 和 equal_to 验证器,WTForms 还提供了以下验证器,如表6-2 所示。

image 2025 01 21 16 15 54 884
Figure 3. 表6-2 WTForms常用验证器

其中,input_required 和 data_required 验证器用户经常会混淆,input_required 只是验证字段是否有值,而 data_required 则是验证数据是否有效。如字段的类型是 IntegerField,但是传入的数据不能被转换为整型时,则会验证失败。

表6-2 中的验证器都是小写形式,这个是为了方便使用,它们原始的名称遵循驼峰命名法,如 length 的原始名称为 Length、input_required 的原始名称为 InputRequired 等。

视图函数中使用表单

表单写完后,就可以在视图函数中对数据进行验证了。这里以继续完善 register 视图函数为例,来讲解表单在视图函数中的使用。

image 2025 01 21 16 17 52 548

以上代码中,先使用 RegisterForm 创建了一个 form 对象,并且把 request.form 作为参数传给 RegisterForm,request.form 是一个类字典类型,以键-值对的形式保存了从浏览器中提交上来的表单数据。然后再调用 form.validate() 方法判断 RegisterForm 中定义的所有字段是否都验证通过,如果是,则通过 form.<字段名>.data 来获取对应字段的数据,这里是从 form 对象上获取数据而不是从 request.form 上获取数据的原因是,服务器获取从浏览器提交上来的数据,其本质上都是字符串类型,所以 request.form 上所有数据都是字符串类型,但是通过 form 对象获取的数据则是经过处理后的,如某个字段是 IntegerField,那么通过 form 对象获取到的则是整型。以上代码有个小细节,在视图中不需要获取 confirm_password 的值,原因是 confirm_password 字段存在的意义就是为了验证其值和 password 是否一致,如果表单验证通过,则意味着 confirm_password 与 password 是相等的,所以没有必要再重新获取一次了。在所有数据都获取到后,就可以把数据存储到数据库中,或者再做其他操作。

如果表单验证失败,则可以通过 form.errors 获取错误信息。form.errors 是字典类型,key 是字段名称,value 是错误信息的列表。如在注册表单中什么都不输入,直接单击 “提交” 按钮,则 form.errors 的值如下所示。

{'username': ['请输入正确长度的用户名!'], 'email': ['请输入正确格式的邮箱!'], 'password': ['请输入正确长度的密码!']}

所以在表单验证失败的情况下,首先通过循环 form.errors.values() 获取所有错误内容,并存储到 flash 中。然后在模板中把 flash 消息显示出来,register.html 修改后的代码如下。

image 2025 01 21 16 22 00 351

使用 flash 消息,必须先在 app 上配置 SECRET_KEY,或直接通过 app.secret_key 来设置秘钥。

如果在浏览器中访问 http://127.0.0.1:5000/register ,然后在表单不输入任何信息,直接单击 “提交” 按钮,那么网页将展现出如图 6-2 所示的效果。

image 2025 01 21 16 23 21 404
Figure 4. 图6-2 显示表单错误消息

自定义验证字段

虽然 WTForms 中提供了许多验证器,但有时候我们还是需要自定义验证逻辑。还是以 RegisterForm 为例,在验证 email 字段时,除了验证是否满足邮箱的格式规则,还需要验证邮箱是否已经被注册过,这时就必须查询数据库,判断邮箱是否存在。如果要自定义某个字段的验证逻辑,可以通过在表单类中自定义方法 validate_<字段名> 来实现,这里以验证 email 为例,示例代码如下。

from wtforms import Form, StringField, ValidationError

registed_email = ['aa@example.com', 'bb@example.com']

class RegisterForm(Form):
    # 其他字段定义...

    def validate_email(self, field):
        email = field.data
        if email in registed_email:
            raise ValidationError("邮箱已经被注册!")
        return True

这里为了模拟从数据库中判断邮箱是否已经被注册,定义了一个 registed_email 变量,代表已经被注册的邮箱,读者可自行结合 ORM 知识实现真实的数据库查找。接着定义了一个 validate_email(self,field) 方法,以后在视图函数中调用 form.validate() 方法时,RegisterForm 底层会自动调用 validate_email 方法,并且会传递一个 field 参数,这里因为验证的是 email 字段,所以这个 field 参数代表的是 email 字段,如果验证的是其他字段,则 field 会代表相应字段。然后通过 field.data 拿到对应的值,再进行逻辑判断,如果认为验证失败了,则可以抛出 wtforms.ValidationError 异常,并且指定一个错误消息,这个错误消息会出现在 form.errors 中,否则直接返回 True 即可。