• 在web程序中,表单是和用户交互最常见的方式之一,如登陆、发布文章、编辑设置等。
  • 表单处理包括创建表单、验证用户输入及错误提示、获取处理数据等。
  • 在Flask中可以手写HTML表单,也可以使用WTForms,WTForms是一个使用python编写的表单库,使得表单编写和处理非常轻松。

HTML表单

即使用html编写表单

  • 表单通过
    标签创建,表单中字段使用标签定义
  1. <form method="post">
  2. <label for="username">用户名</laber><br>
  3. <input type="text" name="username" placeholder="请输入用户名">
  4. <input type="submit" name="submit" value="提交">
  • WTForms支持在python使用类定义表单,然后直接通过类生成对应的html代码,这种方式更见方便且容易重用,因此推荐使用这种方式。

使用Flask-WTF处理表单

  • Flask-WTF集成了WTForms,可以在Flask中更方便的使用WTForms。Flask-WTF将表单数据解析、CSRF保护、文件上传等功能与FLask继承。
  • Flask-WTF默认为每个表单启用CSRF保护,会自动生成和验证CSRF令牌, 默认情况下使用程序密钥进行签名因此需要设置程序密钥.

安装: pip install flask-wtf

定义WTForms表单类

  • 使用WTForms创建表单类是,表单有Python类表示,这个类继承从WTForms导入的From基类。
    • 一个表单有若干个输入字段组成,这些字段分别用表单类的类属性来表示。每个字段属性通过实例化WTForms提供的字段类表示。
      • 字段属性名大小写敏感,不能以_和validate开头。
  1. from wtforms import Form, StringField, PasswordField, BooleanField, SubmitField
  2. from wtforms.validators import DataRequierd, Length
  3. class LoginForm(From):
  4. username = StringField('用户名', validators=[DataRequired()])
  5. password = Password('密码', validators=[DataRequired(), Length(8,128)])
  6. remeber = BooleanField('记住账号')
  7. sumbit = SumbitField('登陆')
  8. # 字段属性/字段属性名称:如username

字段类从wtforms包导入,常用的WTForms字段如下

字段类 说明 对的HTML表示
BooleanField 复选框,值被处理为True或False
DateField 文本字段,值会被处理为datetime.date对象
DateTimeField 文本字段,值会被处理为datetime.datetime对象
FileField 文件上传字段
FloatField
InterField
RadioField
SelectField
SelectMultipleField 多选下拉列表
SubmitField
StringField 文本字段
HiddenField
PasswordField
TextAreaField 多行文本字段

字段设置:
通过实例化字段类是传入参数,可以对字段进行设置。 常用参数如下

参数 说明
label 字段标签
render_kw 一个字典,用来设置对应HTML input标签的属性,如{‘placeholder’:’请输入用户名’}
validators 一个列表,包含一系列验证器,会在表单提交后被逐一调用验证表单数据。
default 字符串或可调用对象,用来为表单字段设置默认值。

验证器validator
验证器是用于验证字段数据的类,在实例化字段类时使用validators关键字类指定附加的验证器列表。
常用验证器:

验证器 说明
DataRequired(message=None) 验证数据是否有效
Email(message=None) 验证Email地址
EqualTo(fieldname, message=None) 验证两个字段值是否相同
InputRequired(message=None)
Length(min=-1,max=-1, message=None)
NubberRange(min=None, max=None, message=None)
Optional(strip_whitespace=True) 允许输入为空,并跳过其他验证
Regexp(regex, flags=0, message=None) 使用正则表达式验证输入值
URL(require_tld=True, message=None)
AnyOf(values, massage=None, values_formatter=None) 确保输入值再可选值列表中
NoneOf(valuse, message=None, values_formatter=None) 确保输入值不在可选值列表中
  • message用来传递自定义错误消息,如果没有设置则使用内置的英文错误消息。

使用Flask-WTF

  • 在使用Flask-WTF定义表单类时,仍然使用WTFForms提供的字段类和验证器,方式也完全相同。不同的是表单类要继承Flask-WTF提供的FlaskForm类。
    • FlaskForm类继承自WTForms的Form类并进行了一些设置以便于与Flask集成。
  • 因为程序中包含多个表单类,一般为了便于组织,会创建forms.py来存放所有的表单类。
  1. from flask_wtf import FlaskForm
  2. from wtforms import StringField, PasswordField
  3. from wtforms.validators import DataRequired, Length
  4. class LoginForm(FlaskForm):
  5. username = StringField('用户名', validators=[DataRequied()])
  6. submit = SubmitField('提交')
  • 配置键WTF_CSRF_ENABLED用来设置是否开启CSRF保护,默认weTrue, Flask-WTF会自动在实例化表单类是添加一个包括CSRF令牌值的隐藏字段,字段名为csrf_token

输出HTML代码

  • 实例化表单类,即可将实例属性转化成字符串或直接调用获取表单对应的HTML代码。
    • 字段的label元素的HTML代码可以通过form.字段名.label获取。
  1. form = LoginForm()
  2. form.username()
  3. form.username.label
  • 默认情况下WTForms输出的HTML代码只会包含id和name属性,属性值均为表单类中对应的字段属性名称。 如需添加额外的属性可通过 render_kw属性和 在调用字段时传入。
  1. # 1 在定义表单类时使用render_kw属性。
  2. username = StringField('用户名', render_kw={'placeholder'='请输入用户名'})
  3. # 2 在调用字段时传入(一般在模板中使用)
  4. form.username'style'='width:200px;', class_='bar' # class是python保留字段,这里用class_代替,渲染后会得到正确的值。

在模板中渲染表单

  • 在模板中渲染表单,需要把表单类实例传入模板
    • 先在视图函数实例化表单类, 然后在render_template()函数中使用关键字参数form将表单实例传入模板。
    • 在模板中,调用表单实例的属性即可获取对应的HTML代码。如果需要传入参数也可以添加括号
  • 模板表单渲染时,还需要调用form.csrf_token属性渲染Flask-WTF为表单类自动创建的CSRF令牌字段。form.csrf_token字段包含了自动生成的CSRF令牌值,在提交表单后会自动被验证,为了确保表单验证通过我们必须在模板的表单中手动渲染这个字段。
    • 也可以使用Flask-WTF提供的form.hidden_tag()方法,这个方法会依次渲染表单from中所有隐藏字段,csrf_token也会被渲染。
  1. from forms import LoginForm
  2. @app.route('/login')
  3. def login():
  4. form = LoginForm()
  5. return render_template('login.html', form:form)
  1. <form method="post">
  2. {{ form.username.label }}<br>{{ form.username }}
  3. <div class="form-group">
  4. {{ form.password.label }}
  5. {{ form.password(class='form-control') }}
  6. {{ form.csrf_token }}
  7. {{ form.submit }}

处理表单数据

提交表单数据大致下面几个步骤:

  • 客户端:
    • 表单提交
  • 服务端
    • 请求解析,获取表单数据
    • 对数据进行必要的转换
    • 验证数据、令牌; 如果验证失败则返回错误消息并在模板中显示
    • 通过验证需要保存数据库或进一步逻辑处理。

在Flask中,可以使用Flask-WTF 和WTForms简化这些步骤。

提交表单

  • 在html模板中, 当from标签声明的表单那中的类型为submit的提交字段被点击时,就会创建一个提交表单的HTTP请求,请求中包含表单个字段的数据,提交到action指定的URL
    • action属性用来指定表单提交到的目标URL,不设置时默认为当前URL。
    • 使用GET方法提交表单数据时,表单数据会以查询字符串的形式附加在请求的URL里。适用与长度不超过2000个字符且不包含铭感信息的表单。否则使用POST方法,使用POST方法时数据会保存在请求主体中。
属性 默认值 说明
action 当前URL,即页面对应的URL 表单提交时发送请求的目标URL
method get HTTP请求方法,支持GET和POST
enctype application/x-www-form-urlencoded 表单数据的编码类型。 当表单中包含文件上传字段时需要设置为: multipart/form-data。

验证表单数据

使用Flask-WTF验证并获取表单数据

1 客户端验证和服务器端验证
客户端验证:

  • 使用HTML5内置的属性验证如:;
  • 在思议表单渲染时,可以在定义表单时通过render_kw属性或渲染表单时传入: {{ form.username(required=’’) }}
  • 使用JS验证,可以手动编写js代码或使用各类js表单验证库如jQuery Validation Plugin或Bootstrap Validator等

服务器端验证-使用WTForms

  • WTForms验证表单字段的方式是在实例化表单类时传入表单数据,然后对表单实例调用validate()方法,会逐个对字段调用字段实例化时定义的验证器,然后返回结果的布尔值,如果验证失败会把错误消息存储到表单实例的errors属性对应的字典中.
  • 因为表单使用POST提交,如果单纯的使用WTForms,在实例化表单时需要把request.form传入表单类。 如果使用Flask-WTF,表单类继承的FlaskForm基类会默认从request.form获取表单数据,所以不需要手动传入。
  • 使用POST方法提交的表单,其数据会被Flask解析为一个字典,可以通过请求对象的form属性获取(request.form), 使用GET方法提交的表单数据通用会被解析为字典,通过请求对象的args属性获取(request.args)

3 在视图函数中验证表单

  • 因为视图会处理POST和GET两个请求,需要对不同请求执行不同代码。 所以处理流程:实例化表单,如果是GET请求就渲染模板,如果是POST请求就用validate()方法验证表单数据。
    • 请求的HTTP方法可以通过requesst.method属性获取。
    • 表单验证时会逐个验证表单字段,包括CSRF。
    • 请求且验证成功,Flask-WTF提供了validate_on_submit()方法合并这两个操作。
  1. from flask import request
  2. @app.login(methods=['POST', 'GET'])
  3. def login():
  4. form = LoginForm()
  5. if request.method=="POST" and form.validate(): #或者 if validate_on_subit():
  6. username = form.username.data
  7. flash('%s, 登陆成功') % username
  8. return redirect(url_for('index')) #重定向避免post为最后一个请求RPG(Post/Redirect/Get),避免在刷新时浏览器提示提示重新提交,
  9. return render_template('login.html', form=form)

在模板中渲染错误消息

  • 如果form.validate_on_submit()返回失败,说明验证失败,WTForms会把验证失败错误消息添加到表单类的errors属性中。这是一个匹配 表达那字段的类属性到对应错误消息列表的字典。 一般可通过字段名获取对应字段的错误消息列表,如form.字段名.errors。 可以在模板中使用for寻源显示错误消息。
  1. <form method="post">
  2. {{ form.csrf_token }}
  3. {{ form.username.label }}
  4. {{ form.username }}
  5. {% for error in form.username.errors %}
  6. {{ error }}
  7. {% endfor %}
  8. </form>
  • 在使用DataRequired和InputRequired验证器时,WTForms会在输出的HTML模板中添加required属性,所以会弹出浏览器内置的错误提示, 同时WTForms也会在表单字段的flags属性添加required标志(如 form.username.flags.required),可以在模板中通过这个标志值来判断是否在文本中添加一个* 号提示必填。

表单进阶实践

设置错误消息语言

  • WTForms内置了多种语言的错误消息,可以通过自定义表单基类实现
    • 首先将配置变量WTF_I18N_ENABLED设置为false,让Flask_WTF使用WTForms内置的错误消息翻译。
    • 然后再自定义表单基类中定义Meta类,在locales列表中加入中文的地区字符。
    • 在创建表单时继承这个基类。 即可将错误消息语言设置为中文。 或者在实例化表单时通过meta关键字传入locales值
      • locales是一个根据优先级排序的的地区字符串,在WTForms中简体中文是zh
  1. # 定义基类设置语言
  2. from flask import Flask
  3. from flask_wtf import FlaskForm
  4. app=Flask(__name__)
  5. app.config['WTF_I18N_ENABLED'] = False
  6. class MyBaseForm(FlaskForm)
  7. class Meta:
  8. localles = ['zh']
  9. # 1 继承这个基类的表单类错误消息会变成设置的语言
  10. class LoginForm(MyBaseForm):
  11. username = StringField('用户名', valiadattors=[DataRequired()])
  12. # 2 或者在实例化表单时设置locales值
  13. form = LoginForm('locales':['zh'])

使用宏渲染表单

  • 表单渲染包括input定义、label、错误消息。 可以使用宏来避免重复操作。

macros.html

  • 这个form_field宏接收表单实例的字段属性和附加的关键字属性, 返回包含label、input、错误消息。
    1. {% macros form_field(field) %}
    2. {{ field.label }}
    3. {{ field(**kwargs) }}
    4. {% if field.errors %}
    5. {% for error in field.errors %}
    6. {{ error }}
    7. {% endfor %}
    8. {% endif %}
    9. {% endmacros %}

login.html

  1. {% form 'macros.html' import form_field %}
  2. <form method="post">
  3. {{ form.csrf_token }}
  4. {{ form_field(form.username) }}
  5. </form>

自定义验证器

除了使用WTForms提供的验证器,我们还可自己定义验证器

行内验证器(in-line-validator)

  • 定义在表单类内,适用于针对这个表单验证特定字段
  • 方法是在表单类内定义方法来验证特殊字段, 方法名为valiadate_字段属性名,接收from和field两参数, 验证出错时抛出ValidationError,传入错误消息作为参数。 ```python from wtforms import StringField from wtforms.validators import ValidationError

class LoginForm(FlaskForm): username = StringField(‘用户名’)

  1. def validate_username(form, field):
  2. if username == 'admin':
  3. raise ValidationError('不可使用系统用户名')
  1. **<br />**<br />**
  2. **全局验证器**
  3. - 可重用的通用验证器:定义一个函数,然后和内置验证器一样调用。
  4. ```python
  5. from wtforms.validators import ValidationError
  6. def is_42(message=None):
  7. if message==None:
  8. message ='Must be 42'
  9. def _is_42(form, field):
  10. if field.date!= 42:
  11. raise ValidationError(message)
  12. return _is_42
  13. class LoginForm(FlaskForm):
  14. anser = IntergeField('The Number', validators=[is_42()])

文件上传

  • 在HTML中设置一个上传文件字段,只需要将input的type类型设置为file,即,会渲染成一个文件上传字段,单击按钮会打开文件选择窗口。
  • 在服务器端和普通数据一样获取数据、保存, 不过处于安全考虑需要验证文件类型、大小、过滤文件名称

定义上传表单

  • 使用Flask-WTF.file提供的FileField类,它继承即WTForms的FileField类并添加了对Flask的集成。
  1. from flask_wtf.file import FileField, FileRequired, FileAllow
  2. class UploadForm(FlaskForm):
  3. photo = FileField('上传文件', validators=[FileRequired(), FileAllow(['jpg','png','jpeg', 'gif'])])
  4. submit = SubmitField('提交')
  • 针对文件,可以使用flask_wtf.file提供的验证器进行验证
验证器 说明
FileRequired(message=None) 验证是否包含文件对象
FileAllowed(upload_set, message=None) 验证文件类型,upload_set参数用来传入允许的文件后缀名列表。
  • FileAllowed是在服务器端设置,在HTML中可以通过accept参数设置。
  • 除了文件类型,还需要对文件大小进行限制。可通过flask内置的配置变量MAX_CONNECT_LENGTH设置请求报文的最大长度,单位为字节。 app.config[‘MAX_CONNECT_LENGTH’] = 310241024。 超出最大限制后会返回413错误。

渲染上传表单

  • 渲染方式和其他字段相同。
  • 当表单中包含上传文件字段时,需要设置表单的enctype为”multipart/form-data”。 这回告诉浏览器将上传数据发送到服务器,否则指挥把文件名作为表单数据提交。
  1. <form method="post" enctype="multipart/form-data">
  2. {{ form.csrf_token }}
  3. {{ form_field(form.photo) }}
  4. {{ form.submit }}
  5. </form>

处理上传文件

  • 当上传文件字段的表单提交后, 上传文件在请求的files属性获取(request.files)。这个属性是Werkzeug提供的ImmutableMultiDict字典对象,存储字段的name键值和文件对象的映射,如:ImmutableMultiDict([(‘photo’, )])
    • 上传的文件会被Flask解析为Werkzeug中的FileStorage对象,手动处理时使用文件上传字段的name属性作为键获取对象如:reqeust.file.get(‘photo’)
  • Flask-WTF中,会自动帮我们获取对应文件对象,因此仍可以使用表单类属性的data属性获取上传文件。
    • 获取文件对象:from.photo.data获取上传文件的FileStorage对象
    • 处理文件名:
      • 使用原文件名
        • 需要确保文件名安全
      • 使用过滤后的文件名
        • 使用Werkzeug提供的secure_filename()对文件名进行过滤。传递文件名为参数,过滤掉危险字符(过滤掉文件所有非ASCII字符)。
        • secure_filename(‘avatar%%>>头像.jpg’) 返回avatar.jpg
      • 同一重命名
        • 更好的做法是使用统一的处理方式对所有文件进行重命名, random_filename是一个定义的处理函数
    • 文件保存路径
    • 保存文件: 对FileStorage对象调用save()方法即可保存,传入包含目标文件夹绝对路径和文件名在内的完整保存路径
    • 获取上传文件:创建一个视图函数来返回上传后的文件。 该视图与static视图类似,传入文件路径返回对应的静态文件。
      • 使用Flask提供的send_from_directory()函数获取文件,传入文件路径和文件名作为参数。

  1. import os
  2. app.config['UPLOAD_PATH'] = os.join.path(app.root_path, 'uploads')
  3. @app.route('/upload', methods=['GET', 'POST'])
  4. def upload():
  5. form = UploadForm()
  6. if form.validate_on_submit():
  7. f = from.photo.data
  8. filename = random_filename(f.filename)
  9. f.save(os.path.join(app.config['UPLOAD_PATH'], filename))
  10. ...
  11. return redirect(url_for('show_images'))
  12. return render_template('upload.html', form=form)
  13. def random_filename(filename):
  14. ext = os.path.splitext(filename)[1]
  15. new_filename = uuid.uuid4().hex + ext
  16. return new_filename
  17. @app.route('/uploads/<path:filename>')
  18. def get_file(filename):
  19. return send_from_directory(app.config['UPLOAD_PATH'], filename)

多文件上传

  • 1 HTML中,多文件上传需要在上传字段添加multiple标记: 。 这样就可以多选
  • 2 在定义表单,使用WTForms提供的MultipleFileField字段实现
  1. from wtforms import MultipleFileField
  2. class MultiUploadForm(FlaskForm):
  3. photo = MultipleFileField('上传多张图片', validators=[DataRequired()])
  4. submit = SubmitField()
  • 3 提交表单后在服务端处理程序中,对request.files调用getlist()方法并传入字段的name属性会返回所有包含上传文件对象的列表。
  1. from flask import request
  2. from flask_wtf.csrf import validate_csrf
  3. from wtforms import ValidationError
  4. @app.route('/multi-upload', methods=['GET', 'POST'])
  5. def multi_upload():
  6. from = MultiUploadForm()
  7. if request.method =='POST':
  8. try:
  9. validate_csrf(form.csrf_token.data)
  10. except ValidationError:
  11. flash('CSRF TOKEN Error')
  12. return redirect(url_for('multi_upload'))
  13. if 'photo' not in request.files:
  14. pass
  15. for f in request.files.getlist('photo'):
  16. if f and allowed_file(f.filename):
  17. filename = random_filename(f.filename)
  18. f.save(os.path.join(app.config['UPLOAD_PATH'], filename))
  19. filename.append(f.filename)
  20. else:
  21. pass
  22. pass
  23. return render_template('upload.html', form=form)
  24. # 文件验证
  25. app.config['ALLOWED_EXTENSIONS'] = ['png', 'jpg', 'jpeg', 'gif']
  26. def allowed_file(filename):
  27. return '.' in filename and filename.rsplit('.',1)[1].lower() in app.config['ALLOWED_EXTENSIONS']
  • 在请求方法为post时,对上传数据手动验证包含一下几步:
    • 1 验证csrf: 手动调用flask_wtf.csrf.validate_csrf验证CSRF令牌,传入表单字csrf_token隐藏字段的值。如果抛出wtforms.ValidationError异常则表示验证未通过。
    • 内容处理

使用Flask-CKEditor富文本编辑器

  • 1 安装: pip insfall flask-ckeditor
  • 2 实例化Flask-CEKditor提供的CKEditor类,传入实例程序。 ```python from flask import Flask from flask_ckeditor import CKEditor

app= Flask(name) ckeditor = CKEditor(app)

  1. **配置富文本编辑器**
  2. | 配置键 | 默认值 | 说明 |
  3. | --- | --- | --- |
  4. | CKEDITOR_SERVE_LOCAL | Flase | 设为True会使用内置的本地资源 |
  5. | CKEDITOR_PKG_TYPE | 'standard' | CKEditor包类型,可选值为basic,standard,full |
  6. | CKEDITOR_LANGUAGE | | 界面语言. 中文为zh,未设置自动探寻浏览器语言 |
  7. | CKEDITOR_HEIGHT | | 编辑器高 |
  8. | CKEDITOR_WIDTH | | 编辑器宽 |
  9. - CKEDITOR_SERVE_LOCALCKEDITOR_PKG_TYPE仅限使用flask-ckeditor提供的方法加载资源时有效,手动引入资源时忽略。
  10. - 如果使用本地资源可设置app.config['CKEDITOR_SERVE_LOCAL']=True
  11. **渲染富文本编辑器**
  12. - 富文本在html中通过文本域字段表示:<textarea></textarea>
  13. - 表单定义使用CKEditorField字段,它时Flask-CKEditor通过包装WTForms提供的TextAreaField字段类型实现的。CKEditorField字段的标签、验证器、默认值、渲染同其他字段相同。
  14. - 渲染CKEditor字段需要加载相应的JavaScript脚本。
  15. - 1 可以使用Flask-CEKditor在模板中提供的ckeditor.load()方法加载资源。 默认从CDN加载, CKEDITOR_SERVE_LOCAL设置为True时使用扩展内置的本地资源,内置的本地资源包含了几个常用的插件和语言包。
  16. - 2 CKEditor官网下载资源放到static目录下,然后再文本编辑器模板中加载目录下的ckeditor.js,替换到ckeditor.load()调用。
  17. - 如果使用配置变量设置了高、宽、语言或其他插件配置。需要在加载ckeditor资源后,使用ckeditor.config()方法加载配置,传入对应表单字段的name属性值
  18. ```python
  19. from flask_wtf import FlaskForm
  20. from wtforms.validators import DataRequired
  21. from flask-ckeditor import CKEditorField
  22. class RichTextForm(FlaskForm):
  23. body = CKEditorField('文章内容', validators=[DataRequired()])
  1. <form method"post">
  2. {{ form_field(form.body) }}
  3. ...
  4. </form>
  5. {{ ckeditor.load() }}
  6. {{ ckeditor.config(name='body') }}

单个表单多个提交按钮

  • 当表单通过POST请求提交时,Flask会把表单数据解析到request.form字典。如果表单中有多个提交按钮,只有被点击的那个提交字段才会出现在这个字典中。 在WTForms中会对数据做进一步处理,当对表单实例或特定字段属性调用data属性时,对于提交字段的值会被转换为布尔值,即被单击的提交字段的值为True、未被点击的为False.
  1. @app.route('/twobutton')
  2. def twobutton():
  3. form = TwoButtonForm()
  4. if form.validate_on_submit():
  5. if form.save.data == True:
  6. pass
  7. elif :
  8. form.publish.data == True
  9. pass

单个页面多个表单

单视图处理

  • 一个视图函数,处理表单渲染和表单处理, 在处理逻辑中根据不同表单按钮进行不同处理。

多视图处理

  • 分别创建表单渲染视图,渲染多个表单
  • 每个表单创建处理视图,分别处理对应表单的数据。