文章详情页 您现在的位置是:网站首页>文章详情

schema-让数据验证更优雅

图片丢失 Jeyrce.Lu 发表于:2019年6月14日 22:34 分类:【Python 2431次阅读

    有一段时间没有更新了,我并不是一个高产的博主。但是假如写出没有意义的文章,那么不如不写。

用户的行为永远是不可信的,我们一定要对用户输入的数据进行校验,让数据符合我们开发者设计的数据格式,同样对于返回给用户的数据,也需要遵循一定的格式。schema就是一种优雅、简洁的方式。

数据格式的验证

实际上,数据格式验证方式是有很多的,比如我们可以利用正则表达式、isinstance等

# 多个if elif, else来判断数据的类型,格式
if xxx:
    yyy

elif ccc:
    xxx
    。。。
else:
    xxx

这样当然可以限定数据格式,但是非常臃肿、难看,并且验证逻辑和业务逻辑放在一起,耦合。因此我们在业务层之上抽象出一个验证层,它专门用来做业务逻辑中数据格式的验证,schema就是一种优雅的数据验证层。

事实上,验证数据有不少三方包做的不错,有的项目中叫做validate,作用和用法都是差不多的,这里只对schema进行一些介绍。

Schema的用法

pip install schema

schema的源码非常短小精悍,完全可以自己研究一遍,它提供了如下核心类,用来满足数据校验过程中的各种需要。

__all__ = [
    "Schema",                    # 核心类,传入一个规则
    "And",                      # 校验条件的`and`关系
    "Or",                       # 校验条件的`or`关系
    "Regex",                     # 用于传入一个正则
    "Optional",                   # 用于表示可选条件
    "Use",                      # 传入一个callable对象
    "Forbidden",                   # 用于禁止数据出现某个名字
    "Const",                     # 用于表示一个常量
    "SchemaError",                  # 数据验证过程中抛出的异常
    "SchemaWrongKeyError",
    "SchemaMissingKeyError",
    "SchemaForbiddenKeyError",
    "SchemaUnexpectedTypeError",
    "SchemaOnlyOneAllowedError",
]

以下所有结果我都在py3.7.3进行测试过,2.x以及3.x其他版本可能会略有不同

(零)传入具体的值:需要完全匹配

print(Schema(5).validate(6))    # 5 not match 6
print(Schema('x').validate('z'))    # 'x' not match 'z'

(一)验证数据类型

int, float, list等等builtin类型:可以得出结论:判断数据类型采用的是 isinstance(obj, type)这样的格式

type_schema = Schema(int)
print(type_schema.validate(3.5))
print(type_schema.validate(3))
print(type_schema.validate('3'))
print(type_schema.validate([1, 2, 3]))

(二)使用Use传入一个callable对象:fuction、内建类型int, float等、实现了__call__的objlambda表达式

  • 传入一个function


def check(obj):
    return obj.__class__.__name__
use_schema = Schema(Use(check))
print(use_schema.validate('sdsdsds'))    # str

def has_previous(obj):
    if hasattr(obj, 'previous'):
        return getattr(obj, 'previous')
    raise AttributeError('{} obj has no attribute {}'.format(obj, 'previous'))

 class PageStart(object):
    pass

 class PageEnd(object):
    @property
    def previous(self):
        return 0

print(Schema(Use(has_previous)).validate(PageStart()))    # traceback
print(Schema(Use(has_previous)).validate(PageEnd()))    # 0, 可以证明validate验证后得到的是数据传入callable的返回值


  • 传入一个类型: int, float, list等: 注意:这里要和类型判断区别,这里使用的是int(obj)这样的调用,而不是isinstance


from schema import Schema, Const, Use, Optional, Or, And, Forbidden

int_schema = Schema(Use(int))

print(int_schema.validate("3"))    # 3 , 这里证明了会进行 str -> int的一个隐式转换
print(int_schema.validate("3.5"))   # traceback
print(int_schema.validate(3))     # 3
print(int_schema.validate(3.3))    # 3 这里证明了会进行int -> float的隐式转换
print(int_schema.validate("sd"))   # traceback
print(int_schema.validate("d"))    # traceback 同样的,我联想到c语言中'a'==65的ascii编码,可以证明python中是不会对这一值进行比较的
float_schema = Schema(Use(float, "必须是一个浮点数"))
print(float_schema.validate("3"))      # 3.0
print(float_schema.validate("3.5"))    # 3.5  注意这里,上面的 '3.5' -> 3 会失败, 但是这里的 '3' -> 3.0 却可以成功
print(float_schema.validate(3))      # 3.0   这里也证明了 int -> float 和 'int' -> float转换
print(float_schema.validate(3.5))    # 3.5
# 可以发现str类型作为用途最广的类型,来者不拒,这也意味着 任意类型   - > str的转换
str_schema = Schema(Use(str))
print(str_schema.validate("3"))         # 3
print(str_schema.validate(3))           # 3
print(str_schema.validate([1, 2, 3]), type(str_schema.validate([1, 2, 3])))     # [1, 2, 3] <class 'str'>
print(str_schema.validate((1, 2, 3)), type(str_schema.validate((1, 2, 3))))     # (1, 2, 3) <class 'str'>
print(str_schema.validate({1, 2, 3}), type(str_schema.validate({1, 2, 3})))     # {1, 2, 3} <class 'str'>
print(str_schema.validate({'x': 1, 'y': 2}), type(str_schema.validate({'x': 1, 'y': 2})))   # {'x': 1, 'y': 2} <class 'str'>
# 那么不妨我们试一下py的显式转换, 果然再次验证了之前的猜想
print(str([1, 2, 3]), type(str([1, 2, 3])))                 # [1, 2, 3] <class 'str'>
print(str((1, 2, 3)), type(str((1, 2, 3))))                 # (1, 2, 3) <class 'str'>
print(str({1, 2, 3}), type(str({1, 2, 3})))                 # {1, 2, 3} <class 'str'>
print(str({'x': 1, 'y': 2}), type(str({'x': 1, 'y': 2})))   # {'x': 1, 'y': 2} <class 'str'>
# 复杂数据类型验证,在此以list为例,请自行验证tupple, set, dict
list_schema = Schema(Use(list))
print(list_schema.validate(1))                        # traceback
print(list_schema.validate('a'))                      # ['a']
print(list_schema.validate([1, 2, 3]))                # [1, 2, 3]
print(list_schema.validate((1, 2, 3)))                # [1, 2, 3]
print(list_schema.validate({1, 2, 3}))                # [1, 2, 3]
print(list_schema.validate({'x': 1, 'y': 2}))         # ['x', 'y']

  • 传入一个实现了__call__方法的实例:

class Call(object):
    def __call__(self, *args, **kwargs):
        return 'called'
print(Schema(Use(Call())).validate('x'))  # called, 实现了__call__方法的对象就可调用


  • 传入一个lambda表达式

print(Schema(Use(lambda obj: obj.__class__.__name__)).validate('xxx'))    # str
print(Schema(Use(lambda obj: isinstance(obj, dict))).validate({'x': 1}))    # True


(三)传入一个实现了validate方法的对象:实际上能够使用UseOrAnd等,其实就是因为他们实现了validate方法

class DictTemplate(object):
    """
    自定义一个字典验证器
    """

     def __init__(self, **kwargs):
        self.dic = kwargs

     def validate(self, data):
        if not isinstance(data, dict):
            raise TypeError()
        for k, v in self.dic.items():
            if k not in data:
                raise KeyError()
            if not isinstance(data[k], type(v)):
                raise ValueError()
        return data
print(DictTemplate(**{'x': 1, 'y': 'sdf', 'z': [1, 2, 3]}).validate({'x': '3', 'y': 'sss', 'z': []}))    # ValueError
print(DictTemplate(**{'x': 1, 'y': 'sdf', 'z': [1, 2, 3]}).validate({'x': 3, 'y': 'sss'}))          # KeyError
print(DictTemplate(**{'x': 1, 'y': 'sdf', 'z': [1, 2, 3]}).validate({'x': 3, 'y': 'sss', 'z': []}))    # {'x': 3, 'y': 'sss', 'z': []}

(四)传入一个容器对象:[], (), '{}'

print(Schema([float, int, str]).validate([3.3, 3, '333']))      # [3.3, 3, '333']
print(Schema((float, int, str)).validate((3.3, 3, [])))         # Error [] should be instance of 'str', 可以得到结论:`容器中每个数据必须为float或int或str,他们是或的关系`
print(Schema({float, int, str}).validate([3.3, 3, '333']))      # SchemaUnexpectedTypeError: [3.3, 3, '333'] should be instance of 'set', 可以得到结论:待验证数据必须和模板容器是一种类型


(五)传入一个字典:这在我们前后端传输数据时最为常用

print(Schema({'x': int, 'y': str, 'z': [1, 2, 3, 4]}).validate({'x': 2, 'y': 'ccc', 'z': [5, 6]}))  # error, 5, 6 not match 1,2,3,4
print(Schema({'x': int, 'y': str, 'z': [1, 2, 3, 4]}).validate({'x': 2, 'y': 'ccc'}))   # Missing key 'z'
print(Schema({'x': int, 'y': str, 'z': [1, 2, 3, 4]}).validate({'x': 2, 'y': 'ccc', 'z': [1, 2], 'o': 'extra key'}))   # wrong key 'o'
print(Schema({'x': '3', 'y': 6}).validate({'x': '3', 'y': 6}))   # {'x': '3', 'y': 6}
print(Schema({'x': '3', 'y': 6}).validate({'x': '4', 'y': 6}))   # '3' not match '4'  证明value给定一个值时必须完全一样才可匹配


要点总结:

  • 传入字典时先验证有没有key,多了key也不行,然后验证对应的value类型或者嵌套的限制是否匹配

  • 当模板字典的value给定一个值之后,数据必须完全匹配才可匹配

  • 字典内规则也是可以嵌套的,规则和简单验证一致

(六)模板字典升级篇:应用扩展类Const, Use, Optional, Or, And, Forbidden实现更加灵活的验证规则

  • Const 和直接传入定值作用完全相同

print(Schema({'x': Const('5')}).validate({'x': 6}))     # '5' not match 6 Const传进去啥出来的还是啥,就相当于直接给值
print(Schema({'x': '5'}).validate({'x': 5}))            # '5' not match 5 和上面的作用完全相同


  • Optional: 可选key, 但是一旦有对应的key,value也必须符合规则

print(Schema({'x': int, Optional('y'): str}).validate({'x': 5}))  # {'x': 5}
print(Schema({'x': int, Optional('y'): str}).validate({'x': 5, 'y': 6}))      # error 6 is not instance of str
print(Schema({'x': int, Optional('y'): str}).validate({'x': 5, 'y': 'yyy'}))  # {'x': 5, 'y': 'yyy'}


  • AndOr:多个条件关系

# x必须是字符串或浮点数,或者可以转换为整形
print(Schema({'x': Or(Use(int), str, float)}).validate({'x': '5'}))        # {'x': 5}
# x必须是字符串或可以转换为int
print(Schema({'x': Or(Use(int), str)}).validate({'x': 'xxx'}))            # {'x': 'xxx'}
print(Schema({'x': Or(Use(int), str)}).validate({'x': 5}))                # {'x': 5}
# x 必须是非空字符串
print(Schema({'x': And(str, lambda x: x != '')}).validate({'x': ''}))     # error
print(Schema({'x': And(str, lambda x: x != '')}).validate({'x': 'ccc'}))    # {'x': 'ccc'}


  • Forbidden:禁用某些key,优先级最高,并且当某些key被禁用时他的value类型无所谓

print(Schema({Forbidden('x', 'y'): int, 'z': list}).validate({'z': []}))    # {'z': []}
print(Schema({Forbidden('x', 'y'): int, 'z': list}).validate({'x': 5, 'z': []}))    # error
print(Schema({'x': int, Forbidden('x'): int, 'y': str}).validate({'x': 5, 'y': ''}))    # error forbidden优先级比较高
print(Schema({Forbidden('x'): int, Optional('x'): int, 'y': str}).validate({'x': 5, 'y': ''}))    # error forbidden优先级比较高


(七)Regex传入一个正则表达式

print(Schema(Regex(r'^135[a-zA-Z0-9]+$')).validate('135Ahjg'))      # 135Ahjg
print(Schema(Regex(r'^135[a-zA-Z0-9]+$')).validate('135444'))          # 135444
print(Schema(Regex(r'^135[a-zA-Z0-9]+$')).validate('135x'))             # 135x
print(Schema(Regex(r'^135[a-zA-Z0-9]+$')).validate('13sx'))         # error

(八)扩展篇:一些重要的参数使用

  • Schema可选参数:error, ignore_extra_keys, name

s = Schema({'x': int}, error='必须是整形', ignore_extra_keys=True)

print(s.validate({'x': 3, 'y': 5}))     # {'x': 3}
print(s.validate({'x': '3', 'y': 5}))     # SchemaError: 必须是整形
- error:当验证不通过时抛出的自定义异常
- ignore_extra_keys: 当多出key是是否忽略异常
- name:给schema自定义一个名字,抛出异常时作为前缀
  • Optional中可以设定默认值:当不传入对应的key时,则默认取用, 并且可以证明:设置默认值后类型就不起作用了,但是你传了对应的key那么value就必须遵循规则了

print(Schema({'x': str, Optional('y', default=7): int}).validate({'x': ''}))    # {'x': '', 'y': 7}
print(Schema({'x': str, Optional('y', default=7): str}).validate({'x': ''}))    # {'x': '', 'y': 7}
print(Schema({'x': str, Optional('y', default=7): str}).validate({'x': '', 'y': 5}))    # error


综合例子:

print(Schema({
    Optional('x', default='xxx'): str,
    Forbidden('y', 'z'): str,
    'age': And(int, lambda x: x > 0 and x < 120)
}, ignore_extra_keys=True).validate({'age': 23, 'title': 'xxx'}))   # {'age': 23, 'x': 'xxx'}

结语

schema确实是一种短小精悍数据验证层工具,在一些其他的框架中也有叫做Validate, Field的, 他们使用起来确实比写多个if else强多了。





版权声明 本文属于本站  原创作品,文章版权归本站及作者所有,请尊重作者的创作成果,转载、引用自觉附上本文永久地址: http://blog.lujianxin.com/x/art/yxcf6t3z211i

文章评论区

作者名片

图片丢失
  • 作者昵称:Jeyrce.Lu
  • 原创文章:61篇
  • 转载文章:3篇
  • 加入本站:1831天

站点信息

  • 运行天数:1832天
  • 累计访问:164169人次
  • 今日访问:0人次
  • 原创文章:69篇
  • 转载文章:4篇
  • 微信公众号:第一时间获取更新信息