一、簡介
在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