近期在用aiohttp替换tornado重构一部分api,其中一些接口需要验证比较多的请求参数,之前同事开发时挺赶的,直接在函数里一个个地验证参数,光验证参数得写一百五六十行代码。我重构时,想到用表单验证来解决这个问题,让代码不至于那么臃肿。
官方文档
http://wtforms.readthedocs.io/en/latest/
示例程序
示例程序web部分使用的是aiohttp,当然换成其他的像flask、tornado都行的。
import json
import traceback
from multidict import MultiDict
from aiohttp import web
from wtforms import Form, StringField, validators, ValidationError
class ActionForm(Form):
action = StringField('action', [validators.Required()])
async def task_handler(request):
try:
post_data = await request.text()
post_data = json.loads(post_data)
_form = ActionForm(MultiDict(post_data))
if _form.validate():
print('>>>> {} validate success'.format(post_data))
else:
print('<<<< {}'.format(_form.errors))
return web.json_response({'message': 'form validate failed'}, status=400)
return web.json_response({'message': 'task_handler'})
except:
traceback.print_exc()
return web.json_response({'message': 'exception'})
app = web.Application()
app.router.add_post('/task', task_handler)
web.run_app(app, host='127.0.0.1', port=8080)
使用curl测试
curl -l -H "Content-type: application/json" -X POST -d '{"action": "START"}' 127.0.0.1:8080/task
>>> {"message": "task_handler"}
说明
- 定义了ActionForm,验证一个类型为String的字段action, validators.Required()表示该字段必须要有,若无则无法通过验证,validators.optional()表示该参数可选
- 若post_data中有其他参数未在ActionForm定义的话,不会影响form的验证,直接忽略
- 除了StringField外,还有其他如:BooleanField、DateField、IntegerField、PasswordField、FieldList、FormField等等,可以查看官方文档
- FieldList是一个列表字段,可以指定list中field的类型
- FormField类似一个字典字段,可以嵌套定义
- 初始化Form后,调用validate()进行验证,若通过验证则返回True,否则返回False,失败的情况加Form的errors返回所有所有验证失败的字段及失败信息
自定义验证函数 Custom validators
在使用过程中,会有需求对字段进行更细化的验证,比如:希望action字段的字符串值在['START', 'CANCEL']范围内,此时就需要编写自定义的验证函数,对字段进行追加验证。
增加一个validator,用于验证action字段在['START', 'CANCEL']范围内,若不在则抛出ValidationError异常:
def my_action_check(form, field):
if field.data not in ['START', 'CANCEL']:
raise ValidationError('action must in [START, CANCEL]')
在ActionForm的action字段的validator列表中加入my_action_check,wtForms会按照先后顺序调用所有的validator对字段进行验证(链式调用,某个环节失败则不继续验证)
class ActionForm(Form):
action = StringField('action', [validators.Required(), my_action_check])
- 此时调用接口时若传入action参数不为START或CANCEL,则表单无法通过验证。
- 验证函数的form字段即为当前的form对象,可用来获取其他字段的值进行联合验证
- 验证函数的field字段即为当前的字段对象,通过field.data来获取值
In-line Validators (内联?。。不知道怎么翻译)
以上的方式使得验证函数具备一定的通用性,这里还有另一种方式用于验证字段,在Form类中定义validate_fieldname格式的函数,则Form会自动调用来验证对应的字段,函数同样接收两个参数:form, field。
class ActionForm(Form):
action = StringField('action', [validators.Required()])
def validate_action(form, field):
if field.data not in ['START', 'CANCEL']:
raise ValidationError('action must in [START, CANCEL]')
以上两种验证方式效果时一样的
自定义字段 Custom Fields
有的场景下,会post复杂的json数据,wtForms自带的字段会感觉不太够用,此时可以自己定义一个字段,能灵活验证数据。如嫌FieldList或者FormField用起来麻烦,可以自己定义一个ListField、DictField来验证列表和字典字段。
自定义ListField,验证数组
class ListField(Field):
def process_formdata(self, valuelist):
try:
if valuelist[0] and isinstance(valuelist[0], list):
self.data = valuelist[0]
else:
raise ValidationError('list validate error')
except:
raise ValidationError('list validate error, exception')
使用curl测试
curl -l -H "Content-type: application/json" -X POST -d '{"action": "START", "users": ["Tracy", "Kobe", "KD"]}' 127.0.0.1:8080/task
>>> test python3 aiotest.py
======== Running on http://127.0.0.1:8080 ========
(Press CTRL+C to quit)
>>>> {'action': 'START', 'users': ['Tracy', 'Kobe', 'KD']} validate success
若users字段不传数组或数组为空则form无法通过验证
- 自定义ListField时,需重写process_formdata方法处理初始化时传入的值,值通过valuelist[0]获取,若验证通过则将值赋给self.data,那么这个字段就有值了,后续的validators就可以对此值进行验证
- validators.Required()实际做的事情就是判断字段的self.data是否为True
小结
wtForms还有一些其他用法在此就不再介绍了,翻阅文档吧。本文代码均使用python3.5运行。若有问题,欢迎交流。