Tornado-Form表单验证
基本思路
用户提交表单后,验证开始。页面中会有多个域要求验证,如text input,files, checkbox。同时,根据验证字段的不同,验证方式会有很多种,例如对邮箱、IP地址、电话的验证标准就各不相同。那么就要设计不同的验证标准,并且在将所有待验证域验证完毕后,返回验证是否成功的结果。这就是基本的实现了。
我们还可以将验证机制做得更细致、更友好些。第一点,并不是所有域名都是必须输入,也会有一些空着也无妨的域。那就可以对不同域做出区分。第二点,用户输入出错和空着没填,我们也可以区分开来,并且在前端动态地提示用户。第三点,对于每个用户输入域,我们都是获取用户输入,并拿自己定义的标准比对,这个过程其实是可以复用的,那是不是可以写一个基类将这些过程抽象并封装,让每个域去继承这一基本流程呢?
文件结构
两个启动文件,对应的分别的是基本表单验证实现和完整的验证实现。
HTML文件
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Form验证</title> <style> .inputs_1{margin-left:36px;} .inputs_2{margin-left:50px;} .inputs_3{margin-left:18px;} </style> </head> <body> <form action="/index" method="post" enctype="multipart/form-data"> IP地址<input type="text" name="ip" class="inputs_1"> <span style="color:red;"> {{ display_error_info(error_message_dict, 'ip') }} </span><br> <p> <input type="checkbox" name="game" value="1">马里奥 <input type="checkbox" name="game" value="2">塞尔达 <input type="checkbox" name="game" value="3">星之卡比 </p> <span style="color:red;"> {{ display_error_info(error_message_dict, 'game') }} </span><br> <input type="file" name="files"/> <input type="file" name="files"/> <input type="submit" value="提交"> <span style="color:red;"> {{ display_error_info(error_message_dict, 'files') }} </span><br> </form> </body> </html>
验证的基本实现
import tornado.ioloop import tornado.web class MainForm(object): # MainForm类的字段就是要匹配的输入域,值是匹配模式 # 其中对checkbox和files两个域,暂时简单地用判断列表有无元素进行检测 def __init__(self): # 匹配IP地址这里折腾了很久,一致尝试使用命名分组,却一直无法成功, # 不懂怎么回事,不想再看正则这一部分了,先放一放 # 注意尖角号和美元符的作用:匹配字符串开头和结尾的位置 # match从头匹配,没有则None,search则是匹配到 self.ip = "^(25[0-5]|2[0-4][0-9]|[0-1]?[0-9]?[0-9])(\.(25[0-5]|2[0-4][0-9]|[0-1]?[0-9]?[0-9])){3}$" self.email = "^\w+@\w+\.\w+" self.phone = "1[3|7|5|8]\d{8}" self.game = [] self.files = [] def check_valid(self, handler): import re # 获取了对象的所有字段,形式是一个字典,键值分别是字段和字段的值 form_dict = self.__dict__ flag = True # 用户输入的值也储存在一个字典中 user_dict = {} # 这里我们迭代form_dict的所有元素 for key, pattern in form_dict.items(): if key == 'game': user_value = handler.get_arguments(key) is_valid = False if not user_value else True elif key == 'files': user_value = handler.request.files.get(key) is_valid = False if not user_value else True else: user_value = handler.get_argument(key) is_valid = re.match(pattern, user_value) if not is_valid: flag = False user_dict[key] = user_value return flag, user_dict class IndexHandler(tornado.web.RequestHandler): def get(self): self.render('index.html') def post(self): # 用户提交数据后,会生成一个MainForm()对象 _formVali = MainForm() flag, user_inputs = _formVali.check_valid(self) print(flag, user_inputs) settings = { "template_path": "views", # 配置html文件路径 "static_path": "statics", # 配置静态文件路径 } # 路由映射 application = tornado.web.Application([ (r"/index", IndexHandler), ], **settings) # 启动 if __name__ == "__main__": application.listen(8001) tornado.ioloop.IOLoop.instance().start()
类封装后的验证实现
import tornado.ioloop import tornado.web import re import myuimethod class IPField: REGULAR = "^(25[0-5]|2[0-4][0-9]|[0-1]?[0-9]?[0-9])(\.(25[0-5]|2[0-4][0-9]|[0-1]?[0-9]?[0-9])){3}$" def __init__(self, error_dict=None, required=True): # 封装自定义错误信息,格式为错误类型-错误信息 self.error_dict = {} if error_dict: self.error_dict.update(error_dict) self.required = required # 用于返回给check_valid函数,让其判断输入是否全部合法 self.is_valid = False # 封装了用户输入的值 self.value = None # 封装了具体的错误信息 self.error = None def validate(self, name, user_value): # 传入参数:域名、用户输入值 # 是否要求必须输入 if not self.required: self.is_valid = True self.value = user_value else: # 输入是否为空 if not user_value.strip(): # 是否自定义了域不可为空的错误信息 if self.error_dict.get("required", None): self.error = self.error_dict['required'] else: self.error = "%s is required." % name else: match_obj = re.match(IPField.REGULAR, user_value) # 是否匹配成功 if match_obj: self.is_valid = True self.value = match_obj.group() else: # 是否定义了非法输入的错误信息 if self.error_dict.get('valid', None): self.error = self.error_dict['valid'] else: self.error = "%s is invalid." % name class CheckboxField: def __init__(self, error_dict=None, required=True): self.error_dict = {} if error_dict: self.error_dict.update(error_dict) self.required = required self.is_valid = False self.value = None self.error = None def validate(self, name, user_value): """ :param name: 域名 game :param user_value: 用户勾选的内容 形如[1,2,3] :return: """ if not self.required: self.is_valid = True self.value = user_value else: if not user_value: if self.error_dict.get('required', None): self.error = self.error_dict['required'] else: self.error = '%s is required.' % name else: self.is_valid = True self.value = user_value class FileField: REGULAR = r'(\w+.jpg)|(\w+.jpeg)|(\w+.gif)|(\*+.png)' def __init__(self, error_dict=None, required=True): self.name = None self.error = None # 封装了符合规范的文件名 self.value = [] # 上传文件一项是否符合规范,默认为True,验证中一旦有不合法的地方就将其改为False self.is_valid = True self.required = required # 存储最终成功上传的文件路径 self.success_file_list = [] # 将错误信息封装在字典中 self.error_dict = {} # 如果自定义了错误信息,则更新默认字典 if error_dict: self.error_dict.update(error_dict) def validate(self, name, file_name_list): self.name = name # 根据文件上传是否必须,区分为两大场景 if not self.required: self.is_valid = True # 所有合法的文件列表,文件可能有1个没有上传,也有可能文件后缀或大小不符合要求 self.success_file_list = file_name_list else: # 要求上传文件却没有上传 if not file_name_list: self.is_valid = False if self.error_dict.get('required', None): self.error = self.error_dict['required'] else: self.error = 'Files not selected and uploaded.' else: for file_name in file_name_list: # 文件是否符合规范 if not re.match(FileField.REGULAR, file_name): self.is_valid = False if self.error_dict.get('error', None): self.error = self.error_dict['error'] else: self.error = '%s is invalid' % file_name else: self.value.append(file_name) def save(self, files_obj, path=''): import os for file_obj in files_obj: file_name = file_obj['filename'] full_file_name = os.path.join(path, file_name) print(full_file_name) # 文件名不为空你且文件名在合法文件列表中 if file_name and file_name in self.value: self.success_file_list.append(full_file_name) with open(full_file_name, 'wb') as f: f.write(file_obj['body']) class BaseForm: def check_valid(self, handler): flag = True error_message_dict = {} success_dict = {} # Form类继承自BaseForm类,self.__dict__内封装了派生类的所有字段 # key是字段名,regOBj则是对应的正则对象 for key, regObj in self.__dict__.items(): # key: 输入域 if type(regObj) == CheckboxField: user_value = handler.get_arguments(key) elif type(regObj) == FileField: # form表单内有文件上传时,一定要将enctype属性设置为multipart/form-data,否则服务端收不到文件数据 # files_obj是一个列表,元素是字典,有键filename和body,封装了文件名和文件内容 files_obj = handler.request.files.get(key) user_value = [] # 如果没有上传任何文件,files_obj将是NoneType无法迭代,迭代前需判断 if files_obj: for file in files_obj: user_value.append(file['filename']) else: user_value = handler.get_argument(key) # 将验证过程放入对应域的正则对象中 # 这样可以将不同的验证规则封装到不同的类中 regObj.validate(key, user_value) # 验证信息封装在regObj中 if regObj.is_valid: success_dict[key] = regObj.value regObj.save(files_obj) else: flag = False error_message_dict[key] = regObj.error return flag, success_dict, error_message_dict class IndexForm(BaseForm): def __init__(self): self.ip = IPField(required=True) self.game = CheckboxField(required=True) self.files = FileField(required=True) class IndexHandler(tornado.web.RequestHandler): def get(self): self.render('index.html', error_message_dict={}) def post(self): _formVali = IndexForm() flag, success_dict, error_message_dict = _formVali.check_valid(self) if flag: print('success', success_dict) else: print('error', error_message_dict) self.render('index.html', error_message_dict=error_message_dict) settings = { "template_path": "views", # 配置html文件路径 "static_path": "statics", # 配置静态文件路径 "ui_methods": myuimethod # 配置模板方法文件 } # 路由映射 application = tornado.web.Application([ (r"/index", IndexHandler), ], **settings) # 启动 if __name__ == "__main__": application.listen(8001) tornado.ioloop.IOLoop.instance().start()
def display_error_info(self, error_dict, key): if error_dict.get(key): return error_dict[key] else: return ''