flask-wtforms

flask-wtforms组件详解

一、简介

       在flask内部并没有提供全面的表单验证,所以当我们不借助第三方插件来处理时候代码会显得混乱,而官方推荐的一个表单验证插件就是wtforms。wtfroms是一个支持多种web框架的form组件,主要用于对用户请求数据的进行验证,其的验证流程与django中的form表单验证由些许类似,本文将介绍wtforms组件使用方法以及验证流程。  

wtforms依照功能类别来说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)
app
<!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>
login.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的渲染。这里先总结下验证流程:

  1. for循环每个字段;
  2. 执行该字段的pre_validate钩子函数;
  3. 执行该字段参数的validators中的验证方法和validate_字段名钩子函数(如果有);
  4. 执行该字段的post_validate钩子函数;
  5. 完成当前字段的验证,循环下一个字段,接着走该字段的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)
Form类

 

其中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):
Filed类

也就是这里会执行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
UnboundField类

实例化该对象时候,会对每个对象实例化的时候计数,第一个对象是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)
Form类

 

构造方法中先实例化默认的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
BaseForm类

 

在这里的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)
bind_field方法

 

继续看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      
_run_validation_chain方法

 

很明显就是循环每一个验证规则,并执行,有错误追加到整体错误中,接着我们回到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

 

posted @ 2019-06-24 14:40  aidenzdly  阅读(134)  评论(0编辑  收藏  举报