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
简单登陆验证
app:
#!/usr/bin/env python3 # -*- coding:utf-8 -*- # author:wd 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 = simple.stringfield( label="用户名", widget=widgets.textinput(), validators=[ validators.datarequired(message="用户名不能为空"), validators.length(max=8,min=3,message="用户名长度必须大于%(max)d且小于%(min)d") ], render_kw={"class":"form-control"} #设置属性生成的html属性 ) 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=["get","post"]) 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 "登录成功" else: print(form.errors,"错误信息") return render_template("login.html",form=form) if __name__ == '__main__': app.run(debug=true)
login.html
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>title</title> </head> <body> <h1>登录</h1> <form method="post"> <!--<input type="text" name="name">--> <p>{{form.name.label}} {{form.name}} {{form.name.errors[0] }}</p> <!--<input type="password" name="pwd">--> <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配置,参数格式为字典。
自定义验证规则
#定义 class myvalidators(object): '''自定义验证规则''' def __init__(self,message): self.message = message def __call__(self, form, field): print(field.data,"用户输入的信息") if field.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:复选类型的选项 ;
示例:
from flask import flask,render_template,redirect,request from wtforms import form from wtforms.fields import core from wtforms.fields import html5 from wtforms.fields import simple from wtforms import validators from wtforms import widgets app = flask(__name__,template_folder="templates") app.debug = true =======================simple=========================== class registerform(form): name = simple.stringfield( label="用户名", validators=[ validators.datarequired() ], widget=widgets.textinput(), render_kw={"class":"form-control"}, default="wd" ) pwd = simple.passwordfield( label="密码", validators=[ validators.datarequired(message="密码不能为空") ] ) pwd_confim = simple.passwordfield( label="重复密码", validators=[ validators.datarequired(message='重复密码不能为空.'), validators.equalto('pwd',message="两次密码不一致") ], widget=widgets.passwordinput(), render_kw={'class': 'form-control'} ) ========================html5============================ email = html5.emailfield( #注意这里用的是html5.emailfield label='邮箱', validators=[ validators.datarequired(message='邮箱不能为空.'), validators.email(message='邮箱格式错误') ], widget=widgets.textinput(input_type='email'), render_kw={'class': 'form-control'} ) ===================以下是用core来调用的======================= gender = core.radiofield( label="性别", choices=( (1,"男"), (1,"女"), ), coerce=int #限制是int类型的 ) city = core.selectfield( label="城市", choices=( ("bj","北京"), ("sh","上海"), ) ) hobby = core.selectmultiplefield( label='爱好', choices=( (1, '篮球'), (2, '足球'), ), coerce=int ) favor = core.selectmultiplefield( label="喜好", choices=( (1, '篮球'), (2, '足球'), ), widget = widgets.listwidget(prefix_label=false), option_widget = widgets.checkboxinput(), coerce = int, default = [1, 2] ) def __init__(self,*args,**kwargs): #这里的self是一个registerform对象 '''重写__init__方法''' super(registerform,self).__init__(*args, **kwargs) #继承父类的init方法 self.favor.choices =((1, '篮球'), (2, '足球'), (3, '羽毛球')) #把registerform这个类里面的favor重新赋值,实现动态改变复选框中的选项 def validate_pwd_confim(self,field,): ''' 自定义pwd_config字段规则,例:与pwd字段是否一致 :param field: :return: ''' # 最开始初始化时,self.data中已经有所有的值 if field.data != self.data['pwd']: # raise validators.validationerror("密码不一致") # 继续后续验证 raise validators.stopvalidation("密码不一致") # 不再继续后续验证 @app.route('/register',methods=["get","post"]) def register(): if request.method=="get": form = registerform(data={'gender': 1}) #默认是1, return render_template("register.html",form=form) else: form = registerform(formdata=request.form) if form.validate(): #判断是否验证成功 print('用户提交数据通过格式验证,提交的值为:', form.data) #所有的正确信息 else: print(form.errors) #所有的错误信息 return render_template('register.html', form=form) if __name__ == '__main__': app.run()
register.html
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>title</title> </head> <body> <h1>用户注册</h1> <form method="post" novalidate style="padding:0 50px"> {% for item in form %} <p>{{item.label}}: {{item}} {{item.errors[0] }}</p> {% endfor %} <input type="submit" value="提交"> </form> </body> </html>
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 = {} # 保存本地化缓存信息的字段
示例:
#!/usr/bin/env python # -*- coding:utf-8 -*- from flask import flask, render_template, request, redirect, session from wtforms import form from wtforms.csrf.core import csrf from wtforms.fields import core from wtforms.fields import html5 from wtforms.fields import simple from wtforms import validators from wtforms import widgets 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: 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,), {})
这几段代码就等价于:
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
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