flask-wtforms
flask-wtforms组件详解
一、简介
在flask内部并没有提供全面的表单验证,所以当我们不借助第三方插件来处理时候代码会显得混乱,而官方推荐的一个表单验证插件就是wtforms。wtfroms是一个支持多种web框架的form组件,主要用于对用户请求数据的进行验证,其的验证流程与django中的form表单验证由些许类似,本文将介绍wtforms组件使用方法以及验证流程。
- Forms: 主要用于表单验证、字段定义、HTML生成,并把各种验证流程聚集在一起进行验证。
- Fields: 主要负责渲染(生成HTML)和数据转换。
- Validator:主要用于验证用户输入的数据的合法性。比如Length验证器可以用于验证输入数据的长度。
- Widgets:html插件,允许使用者在字段中通过该字典自定义html小部件。
- Meta:用于使用者自定义wtforms功能,例如csrf功能开启。
- Extensions:丰富的扩展库,可以与其他框架结合使用,例如django。
二、安装
pip3 install wtforms
简单forms登录验证
from flask import Flask,render_template,request from wtforms.fields import simple from wtforms import Form from wtforms import validators from wtforms import widgets app = Flask(__name__,template_folder='templates') class LoginForm(Form): ''' 创建form表单,name、pwd等是字段 ''' name = simple.StringField( label='用户名', widget=widgets.TextInput(), # 表单验证规则 validators=[ validators.DataRequired(message='用户名不能为空'), validators.Length(max=8,min=3,message='用户名长度必须大于%(max)d且小于%(min)d'), ], # 设置class属性 render_kw={'class':'form-control'} ) pwd = simple.PasswordField( label='密码', validators=[ validators.DataRequired(message='密码不能为空'), validators.Length(max=18,min=4,message='密码长度必须大于%(max)d且小于%(min)d'), validators.Regexp(regex='\d+',message='密码必须是数字'), ], widget=widgets.PasswordInput(), render_kw={'class':'form-control'} ) @app.route('/login',methods=['POST','GET']) def login(): if request.method == 'GET': form = LoginForm() return render_template('login.html',form=form) else: form = LoginForm(formdata=request.form) if form.validate(): # 对用户提交数据进行校验,form.data是校验完成后的数据字典 print('用户提交的数据通过格式验证,值为:%s'%form.data) return '登陆成功' print(form.name.label) # <label for="name">用户名</label> print(form.name) # <input class="form-control" id="name" name="name" required type="text" value="11"> print(form.name.errors[0]) # 用户名长度必须大于8且小于3 return render_template('login.html',form=form) if __name__ == '__main__': app.run(debug=True)
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>登陆</h1> <form method="post"> <p>{{ form.name.label }}{{ form.name }}{{ form.name.errors[0] }}</p> <p>{{ form.pwd.label }}{{ form.pwd }}{{ form.pwd.errors[0] }}</p> <input type="submit" value="提交"> </form> </body> </html>
Form类实例化参数:
- formdata:需要被验证的form表单数据。
- obj:当formdata参数为提供时候,可以使用对象,也就是会是有obj.字段的值进行验证或设置默认值。
- prefix: 字段前缀匹配,当传入该参数时,所有验证字段必须以这个开头(无太大意义)。
- data: 当formdata参数和obj参数都有时候,可以使用该参数传入字典格式的待验证数据或者生成html的默认值,列如:{'usernam':'admin’}。
- meta:用于覆盖当前已经定义的form类的meta配置,参数格式为字典。
自定义验证规则
from wtforms import validators from wtforms import Form from wtforms.fields import simple from wtforms import widgets class MyValidators(object): ''' 自定义验证规则 ''' def __init__(self,message): self.message = message def __call__(self, form, filed): print(filed.data,'用户输入的信息') if filed.data == 'admin': raise validators.ValidationError(self.message) class LoginForm(Form): ''' Form ''' name = simple.StringField( label='用户名', widget=widgets.TextInput(), # 自定义验证类 validators=[ MyValidators(message='用户名不能是admin'), ], # 设置属性 render_kw={'class':'form-control'} )
字段介绍
wtforms中的Field类主要用于数据验证和字段渲染(生成html),以下是比较常见的字段:
- StringField 字符串字段,生成input要求字符串
- PasswordField 密码字段,自动将输入转化为小黑点
- DateField 日期字段,格式要求为datetime.date一样
- IntergerField 整型字段,格式要求是整数
- FloatField 文本字段,值是浮点数
- BooleanField 复选框,值为True或者False
- RadioField 一组单选框
- SelectField 下拉列表,需要注意一下的是choices参数确定了下拉选项,但是和HTML中的<select> 标签一样。
- MultipleSelectField 多选字段,可选多个值的下拉列表
- ...
字段参数:
- label:字段别名,在页面中可以通过字段.label展示;
- validators:验证规则列表;
- filters:过氯器列表,用于对提交数据进行过滤;
- description:描述信息,通常用于生成帮助信息;
- id:表示在form类定义时候字段的位置,通常你不需要定义它,默认会按照定义的先后顺序排序。
- default:默认值
- widget:html插件,通过该插件可以覆盖默认的插件,更多通过用户自定义;
- render_kw:自定义html属性;
- choices:复选类型的选项 ;
Meta
Meta主要用于自定义wtforms的功能,大多都是配置选项,以下是配置参数:
csrf = True # 是否自动生成CSRF标签 csrf_field_name = 'csrf_token' # 生成CSRF标签name csrf_secret = 'adwadada' # 自动生成标签的值,加密用的csrf_secret csrf_context = lambda x: request.url # 自动生成标签的值,加密用的csrf_context csrf_class = MyCSRF # 生成和比较csrf标签 locales = False # 是否支持翻译 locales = ('zh', 'en') # 设置默认语言环境 cache_translations = True # 是否对本地化进行缓存 translations_cache = {} # 保存本地化缓存信息的字段
from flask import Flask, render_template, request from wtforms import Form from wtforms.csrf.core import CSRF from wtforms.fields import html5 from wtforms.fields import simple from hashlib import md5 app = Flask(__name__, template_folder='templates') app.debug = True class MyCSRF(CSRF): """ Generate a CSRF token based on the user's IP. I am probably not very secure, so don't use me. """ def setup_form(self, form): self.csrf_context = form.meta.csrf_context() self.csrf_secret = form.meta.csrf_secret return super(MyCSRF, self).setup_form(form) def generate_csrf_token(self, csrf_token): gid = self.csrf_secret + self.csrf_context token = md5(gid.encode('utf-8')).hexdigest() return token def validate_csrf_token(self, form, field): print(field.data, field.current_token) if field.data != field.current_token: raise ValueError('Invalid CSRF') class TestForm(Form): name = html5.EmailField(label='用户名') pwd = simple.StringField(label='密码') class Meta: # -- CSRF # 是否自动生成CSRF标签 csrf = True # 生成CSRF标签name csrf_field_name = 'csrf_token' # 自动生成标签的值,加密用的csrf_secret csrf_secret = 'xxxxxx' # 自动生成标签的值,加密用的csrf_context csrf_context = lambda x: request.url # 生成和比较csrf标签 csrf_class = MyCSRF # -- i18n # 是否支持本地化 # locales = False locales = ('zh', 'en') # 是否对本地化进行缓存 cache_translations = True # 保存本地化缓存信息的字段 translations_cache = {} @app.route('/index', methods=['GET', 'POST']) def index(): if request.method == 'GET': form = TestForm() else: # formdata:需要被验证的form表单数据 form = TestForm(formdata=request.form) if form.validate(): print(form) return render_template('index.html', form=form) if __name__ == '__main__': app.run()
三、实现原理
wtforms实现原理这里主要从三个方面进行说明:form类创建过程、实例化过程、验证过程。从整体看其实现原理实则就是将每个类别的功能(如Filed、validate、meta等)通过form进行组织、封装,在form类中调用每个类别对象的方法实现数据的验证和html的渲染。这里先总结下验证流程:
- for循环每个字段;
- 执行该字段的pre_validate钩子函数;
- 执行该字段参数的validators中的验证方法和validate_字段名钩子函数(如果有);
- 执行该字段的post_validate钩子函数;
- 完成当前字段的验证,循环下一个字段,接着走该字段的2、3、4流程,直到所有字段验证完成;
Form类创建过程
以示例中的RegisterForm为例,它继承了Form类:
class Form(with_metaclass(FormMeta, BaseForm)): Meta = DefaultMeta def __init__(self, formdata=None, obj=None, prefix='', data=None, meta=None, **kwargs): meta_obj = self._wtforms_meta() if meta is not None and isinstance(meta, dict): meta_obj.update_values(meta) super(Form, self).__init__(self._unbound_fields, meta=meta_obj, prefix=prefix) for name, field in iteritems(self._fields): # Set all the fields to attributes so that they obscure the class # attributes with the same names. setattr(self, name, field) self.process(formdata, obj, data=data, **kwargs) def __setitem__(self, name, value): raise TypeError('Fields may not be added to Form instances, only classes.') def __delitem__(self, name): del self._fields[name] setattr(self, name, None) def __delattr__(self, name): if name in self._fields: self.__delitem__(name) else: # This is done for idempotency, if we have a name which is a field, # we want to mask it by setting the value to None. unbound_field = getattr(self.__class__, name, None) if unbound_field is not None and hasattr(unbound_field, '_formfield'): setattr(self, name, None) else: super(Form, self).__delattr__(name) def validate(self): """ Validates the form by calling `validate` on each field, passing any extra `Form.validate_<fieldname>` validators to the field validator. """ extra = {} for name in self._fields: inline = getattr(self.__class__, 'validate_%s' % name, None) if inline is not None: extra[name] = [inline] return super(Form, self).validate(extra)
其中with_metaclass(FormMeta, BaseForm):
def with_metaclass(meta, base=object): return meta("NewBase", (base,), {})
定义的with_metaclass函数相当于:
class Newbase(BaseForm,metaclass=FormMeta): pass class Form(Newbase): pass
也就是说RegisterForm继承Form—》Form继承Newbase—》Newbase继承BaseForm,因此当解释器解释道class RegisterForm会执行FormMeta的__init__方法用于生成RegisterForm类:
class FormMeta(type): def __init__(cls, name, bases, attrs): type.__init__(cls, name, bases, attrs) cls._unbound_fields = None cls._wtforms_meta = None
由其__init__方法可以知道生成的RegisterForm中含有字段_unbound_fields和_wtforms_meta并且也包含了我们自己定义的验证字段(name、pwd...),并且这些字段保存了每个Field实例化的对象,以下拿name说明:
name = simple.StringField( label="用户名", validators=[ validators.DataRequired() ], widget=widgets.TextInput(), render_kw={"class":"form-control"}, default="wd" )
实例化StringField会先执行其__new__方法在执行__init__方法,而StringField继承了Field:
class Field(object): """ Field base class """ errors = tuple() process_errors = tuple() raw_data = None validators = tuple() widget = None _formfield = True _translations = DummyTranslations() do_not_call_in_templates = True # Allow Django 1.4 traversal def __new__(cls, *args, **kwargs): if '_form' in kwargs and '_name' in kwargs: return super(Field, cls).__new__(cls) else: return UnboundField(cls, *args, **kwargs) def __init__(self, label=None, validators=None, filters=tuple(), description='', id=None, default=None, widget=None, render_kw=None, _form=None, _name=None, _prefix='', _translations=None, _meta=None):
也就是这里会执行Field的__new__方法,在这里的__new__方法中,判断_form和_name是否在参数中,刚开始kwargs里面是label、validators这些参数,所以这里返回UnboundField(cls, *args, **kwargs),也就是这里的RegisterForm.name=UnboundField(),其他的字段也是类似,实际上这个对象是为了让我们定义的字段由顺序而存在的,如下:
class UnboundField(object): _formfield = True creation_counter = 0 def __init__(self, field_class, *args, **kwargs): UnboundField.creation_counter + 1 self.field_class = field_class self.args = args self.kwargs = kwargs self.creation_counter = UnboundField.creation_counter
实例化该对象时候,会对每个对象实例化的时候计数,第一个对象是1,下一个+1,并保存在每个对象的creation_counter中。最后的RegisterForm中就保存了{’name’:UnboundField(1,simple.StringField,参数),’pwd’:UnboundField(2,simple.StringField,参数)…}。
Form类实例化过程
同样在RegisterForm实例化时候先执行__new__方法在执行__init__方法,这里父类中没也重写__new__也就是看__init__方法:
class Form(with_metaclass(FormMeta, BaseForm)): Meta = DefaultMeta def __init__(self, formdata=None, obj=None, prefix='', data=None, meta=None, **kwargs): meta_obj = self._wtforms_meta() # 实例化meta if meta is not None and isinstance(meta, dict): # 判断meta是否存在且为字典 meta_obj.update_values(meta) # 覆盖原meta的配置 # 执行父类的构造方法 super(Form, self).__init__(self._unbound_fields, meta=meta_obj, prefix=prefix) for name, field in iteritems(self._fields): # Set all the fields to attributes so that they obscure the class # attributes with the same names. setattr(self, name, field) self.process(formdata, obj, data=data, **kwargs)
构造方法中先实例化默认的meta,在判断是否传递类meta参数,传递则更新原meta的配置,接着执行父类的构造方法,父类是BaseForm:
class BaseForm(object): """ Base Form Class. Provides core behaviour like field construction, validation, and data and error proxying. """ def __init__(self, fields, prefix='', meta=DefaultMeta()): if prefix and prefix[-1] not in '-_;:/.': prefix += '-' self.meta = meta self._prefix = prefix self._errors = None self._fields = OrderedDict() if hasattr(fields, 'items'): fields = fields.items() translations = self._get_translations() extra_fields = [] if meta.csrf: #判断csrf配置是否为true,用于生成csrf的input框 self._csrf = meta.build_csrf(self) extra_fields.extend(self._csrf.setup_form(self)) #循环RegisterForm中的字段,并对每个字段进行实例化 for name, unbound_field in itertools.chain(fields, extra_fields): options = dict(name=name, prefix=prefix, translations=translations) field = meta.bind_field(self, unbound_field, options) self._fields[name] = field
在这里的for循环中执行meta.bind_field方法对每个字段进行实例化,并以k,v的形式放入了self._fields属性中。并且实例化传递来参数_form和_name,也就是在执行BaseForm时候判断的两个属性,这里传递了就走正常的实例化过程。
def bind_field(self, form, unbound_field, options): """ bind_field allows potential customization of how fields are bound. The default implementation simply passes the options to :meth:`UnboundField.bind`. :param form: The form. :param unbound_field: The unbound field. :param options: A dictionary of options which are typically passed to the field. :return: A bound field """ return unbound_field.bind(form=form, **options) def bind(self, form, name, prefix='', translations=None, **kwargs): kw = dict( self.kwargs, _form=form, #传递_form _prefix=prefix, _name=name, # 传递_name _translations=translations, **kwargs ) return self.field_class(*self.args, **kw)
继续看Form类中的__init__方法,接着循环:
for name, field in iteritems(self._fields): # Set all the fields to attributes so that they obscure the class # attributes with the same names. setattr(self, name, field) self.process(formdata, obj, data=data, **kwargs)
此时的self._fields已经包含了每个实例化字段的对象,调用setattr为对象设置属性,为了方便获取字段,例如没有该语句获取字段时候通过RegisterForm()._fields[’name’],有了它直接通过RegisterForm().name获取,继续执行self.process(formdata, obj, data=data, **kwargs)方法,改方法用于验证的过程,因为此时的formdata、obj都是None,所以执行了该方法无影响。
验证流程
当form对用户提交的数据验证时候,同样以上述注册为例子,这次请求是post,同样会走form = RegisterForm(formdata=request.form),但是这次不同的是formdata已经有值,让我们来看看process方法:
def process(self, formdata=None, obj=None, data=None, **kwargs): formdata = self.meta.wrap_formdata(self, formdata) if data is not None: #判断data参数 # XXX we want to eventually process 'data' as a new entity. # Temporarily, this can simply be merged with kwargs. kwargs = dict(data, **kwargs),更新kwargs参数 for name, field, in iteritems(self._fields):#循环每个字段 if obj is not None and hasattr(obj, name):# 判断是否有obj参数 field.process(formdata, getattr(obj, name)) elif name in kwargs: field.process(formdata, kwargs[name]) else: field.process(formdata)
首先对用户提交的数据进行清洗变成k,v格式,接着判断data参数,如果不为空则将其值更新到kwargs中,然后循环self._fields(也就是我们定义的字段),并执行字段的process方法:
def process(self, formdata, data=unset_value): self.process_errors = [] if data is unset_value: try: data = self.default() except TypeError: data = self.default self.object_data = data try: self.process_data(data) except ValueError as e: self.process_errors.append(e.args[0]) if formdata is not None: if self.name in formdata: self.raw_data = formdata.getlist(self.name) else: self.raw_data = [] try: self.process_formdata(self.raw_data) except ValueError as e: self.process_errors.append(e.args[0]) try: for filter in self.filters: self.data = filter(self.data) except ValueError as e: self.process_errors.append(e.args[0]) def process_data(self, value): self.data = value
该方法作用是将用户的提交的数据存放到data属性中,接下来就是使用validate()方法开始验证:
def validate(self): """ Validates the form by calling `validate` on each field, passing any extra `Form.validate_<fieldname>` validators to the field validator. """ extra = {} for name in self._fields: # 循环每个field #寻找当前类中以validate_’字段名匹配的方法’,例如pwd字段就寻找validate_pwd,也就是钩子函数 inline = getattr(self.__class__, 'validate_%s' % name, None) if inline is not None: extra[name] = [inline] #把钩子函数放到extra字典中 return super(Form, self).validate(extra) #接着调用父类的validate方法
验证时候先获取所有每个字段定义的validate_+'字段名'匹配的方法,并保存在extra字典中,在执行父类的validate方法:
def validate(self, extra_validators=None): self._errors = None success = True for name, field in iteritems(self._fields): # 循环字段的名称和对象 if extra_validators is not None and name in extra_validators: # 判断该字段是否有钩子函数 extra = extra_validators[name] # 获取到钩子函数 else: extra = tuple() if not field.validate(self, extra): # 执行字段的validate方法 success = False return success
该方法主要用于和需要验证的字段进行匹配,然后在执行每个字段的validate方法:
def validate(self, form, extra_validators=tuple()): self.errors = list(self.process_errors) stop_validation = False # Call pre_validate try: self.pre_validate(form) # 先执行字段字段中的pre_validate方法,这是一个自定义钩子函数 except StopValidation as e: if e.args and e.args[0]: self.errors.append(e.args[0]) stop_validation = True except ValueError as e: self.errors.append(e.args[0]) # Run validators if not stop_validation: chain = itertools.chain(self.validators, extra_validators) # 拼接字段中的validator和validate_+'字段名'验证 stop_validation = self._run_validation_chain(form, chain) # 执行每一个验证规则,self.validators先执行 # Call post_validate try: self.post_validate(form, stop_validation) except ValueError as e: self.errors.append(e.args[0]) return len(self.errors) == 0
在该方法中,先会执行内部预留给用户自定义的字段的pre_validate方法,在将字段中的验证规则(validator也就是我们定义的validators=[validators.DataRequired()],)和钩子函数(validate_+'字段名')拼接在一起执行,注意这里的validator先执行而字段的钩子函数后执行,我们来看怎么执行的:
def _run_validation_chain(self, form, validators): for validator in validators: # 循环每个验证规则 try: validator(form, self) # 传入提交数据并执行,如果是对象执行__call__,如果是函数直接调用 except StopValidation as e: if e.args and e.args[0]: self.errors.append(e.args[0]) # 如果有错误,追加到整体错误中 return True except ValueError as e: self.errors.append(e.args[0]) return False
很明显就是循环每一个验证规则,并执行,有错误追加到整体错误中,接着我们回到validate方法中,接着又会执行post_validate,这也是内置钩子函数,允许用户自己定义,最后这个字段的数据验证完成了,而在开始的for循环,循环结束意味着整个验证过程结束。
def post_validate(self, form, validation_stopped): """ Override if you need to run any field-level validation tasks after normal validation. This shouldn't be needed in most cases. :param form: The form the field belongs to. :param validation_stopped: `True` if any validator raised StopValidation. """ pass