在flask內部並無提供全面的表單驗證,因此當咱們不借助第三方插件來處理時候代碼會顯得混亂,而官方推薦的一個表單驗證插件就是wtforms。wtfroms是一個支持多種web框架的form組件,主要用於對用戶請求數據的進行驗證,其的驗證流程與django中的form表單驗證由些許相似,本文將介紹wtforms組件使用方法以及驗證流程。 html
pip3 install wtforms
簡單登錄驗證html5
app:python
#!/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.htmlweb
<!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類實例化參數:django
#定義 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),如下是比較常見的字段:flask
字段參數:緩存
示例:session
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.htmlapp
<!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主要用於自定義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的渲染。這裏先總結下驗證流程:
以示例中的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,參數)…}。
一樣在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