Python中如何实现数据校验并自定义validator轮子

最近抽空造了一个数据校验的轮子 python-validator

在开发 web 应用时,经常需要校验前端传入的数据。如果使用 Django,那么可以使用自带的 forms 进行数据校验。

python-validator 的灵感也是来源于 Django 的 forms (类似 ORM 的方式定义数据结构),不过 python-validator 更加纯粹,只是数据校验,另外也支持使用 dict 定义数据结构,相比于使用类更加方便。

欢迎大家提建议。觉得不错麻烦给个 star 吧!

下面是简单的介绍:

python-validator 是一个类似于 Django ORM 的数据校验库,适用与任何需要进行数据校验的应用,比较常见的是 Web 后端校验前端的输入数据。

特性

  • 支持 python2 和 python3。

  • 使用类描述数据结构,数据字段一目了然。另外也支持使用字典定义数据结构。

  • 可以自动生成用于测试的 mocking data。

  • 可以打印出清晰的数据结构。

  • 易于扩展。

依赖

  • six

  • IPy

  • pytz[可选,DatetimeFieldtzinfo 参数需要一个 tzinfo 对象]

安装

pip install python-validator

快速入门

假设现在正在开发一个上传用户信息的接口 POST /api/user/,用户信息如下:

  • name

    string,必选。

  • age

    integer,可选,默认 20。

  • sex

    string, 'f'表示女, 'm'表示男。可选, 默认 None。

原始的、枯燥无味的、重复性劳动的数据校验代码可能是下面这样:

def user(request):
    # data = json.loads(request.body)
    data = {
        'age': '24f',
        'sex': 'f'
    }
    name = data.get('name')
    age = data.get('age', 20)
    sex = dage.get('sex')
if name is None or len(name) == 0:
    return Response('必须提供 name', status=400)

try:
    age = int(age)
except ValueError as e:
    return Response('age 格式错误', status=400)

if sex is not None and sex not in ('f', 'm'):
    return Response('sex 格式错误', status=400)

user_info = {
    'name': name,
    'age': age,
    'sex': sex,
}
...

上面这段代码总的来说有几个问题:

  • 枯燥无味和重复性代码,不断的取出数据,检查字段是否缺失,类型是否合法等等。

  • 从数据校验的代码无法轻易看出用户信息的数据结构,即字段是什么类型的,是否可选,默认值是什么。

使用 python-validator 校验数据

首先定义一个 UserInfoValidator 类

# validators.py
from validator import Validator, StringField, IntegerField, EnumField

class UserInfoValidator(Validator): name = StringField(max_length=50, required=True) age = IntegerField(min_value=1, max_value=120, default=20) sex = EnumField(choices=[‘f’, ‘m’])

接下来使用 UserInfoValidator 进行数据校验,

from .validators import UserInfoValidator

def user(request): # data = json.loads(request.body) data = { ‘age’: ‘24’, ‘sex’: ‘f’ } v = UserInfoValidator(data) if not v.is_valid(): return Response({‘msg’: v.str_errors, ‘code’: 400}, status=400)

user_info = v.validated_data
...

v.str_errors 是一个字段名 - 错误信息的 dict,例如:

{'age': 'got a wrong type: str, expect integer', 'name': 'Field is required'}

错误信息解释:

  • age 等于 "24",不是合法的 int 类型。

  • name 是必须提供的,且没有指定默认值。

v.validated_data 是校验后合法的数据,例如:

{'age': 24, 'name': u'Michael', 'sex': 'f'}

下面是一些错误数据的例子:

data:  {'age': 24, 'name': 'abcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabc', 'sex': 'f'}
is_valid: False
errors: {'name': 'string is too long, max-lenght is 50'}
validated_data: None
data:  {'age': 24, 'name': 'Michael', 'sex': 'c'}
is_valid: False
errors: {'sex': "'c' not in the choices"}
validated_data: None

Python中如何实现数据校验并自定义validator轮子

14 回复

先 start 为敬🌚


在Python里做数据校验,我一般用Pydantic,但自己造轮子也简单。核心就是写个类,用描述符(descriptor)或者__setattr__来拦截赋值操作。

下面这个Validator类就是个基础轮子。它用__init__定义字段和校验规则,__setattr__在赋值时触发校验。validate方法里你可以写任何校验逻辑,比如类型检查、范围判断或者自定义函数。校验失败就抛个ValueError

class Validator:
    def __init__(self):
        self._validators = {}
        self._data = {}

    def add_field(self, name, validator_func):
        """添加字段和对应的校验函数"""
        self._validators[name] = validator_func
        self._data[name] = None

    def __setattr__(self, name, value):
        if name.startswith('_'):
            super().__setattr__(name, value)
        else:
            if name in self._validators:
                validator = self._validators[name]
                if not validator(value):
                    raise ValueError(f"Validation failed for field '{name}' with value {value}")
                self._data[name] = value
            else:
                raise AttributeError(f"No validator defined for field '{name}'")

    def __getattr__(self, name):
        if name in self._data:
            return self._data[name]
        raise AttributeError(f"No such field: '{name}'")

# 使用示例
def is_positive(x):
    return isinstance(x, (int, float)) and x > 0

def is_non_empty_string(s):
    return isinstance(s, str) and len(s) > 0

# 创建实例并添加字段
obj = Validator()
obj.add_field('age', is_positive)
obj.add_field('name', is_non_empty_string)

# 测试赋值
try:
    obj.age = 25  # 通过
    print(f"Age: {obj.age}")
    obj.name = "Alice"  # 通过
    print(f"Name: {obj.name}")
    obj.age = -5  # 触发 ValueError
except ValueError as e:
    print(e)

想更Pythonic的话,可以用描述符。下面这个ValidatedAttribute描述符把校验逻辑封装在__set__里,用起来更干净。

class ValidatedAttribute:
    def __init__(self, validator):
        self.validator = validator
        self.attr_name = None

    def __set_name__(self, owner, name):
        self.attr_name = name

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return obj.__dict__.get(self.attr_name)

    def __set__(self, obj, value):
        if not self.validator(value):
            raise ValueError(f"Invalid value {value} for {self.attr_name}")
        obj.__dict__[self.attr_name] = value

# 使用描述符的类
class Person:
    age = ValidatedAttribute(is_positive)
    name = ValidatedAttribute(is_non_empty_string)

    def __init__(self, name, age):
        self.name = name
        self.age = age

# 测试
try:
    p = Person("Bob", 30)
    print(f"Person: {p.name}, {p.age}")
    p.age = -10  # 触发 ValueError
except ValueError as e:
    print(e)

简单需求自己写,复杂项目直接用Pydantic。

好东西…
能否允许尝试把“ 24 ”被转换为 24 之后再进行校验?
放在 query string 里面的整数一般取出来还是字符串类型…

把“ 24 ”转换为 24

原来文档里有…无视我😂

#2 我当初考虑到了这个情况,将 strict 设为 False 就会尝试进行类型转换了。

同理,StringField 也可以允许非字符串类型的数据,只要可以转换成字符串。

star 了,挺有用的工具

#8 刚看了一下这个库,是有点类似。

看着比 wtforms 舒服

#10 如果在使用过程中遇到问题欢迎提交 issue。

挺好的,不过 django-restful 里面有个类似的,可以参考一下,我之前用过 schema 来做验证,不过对于嵌套的 json 比较捉急,不知道你这个可以吗?

#12 你说的是 Django-restfulframework 的 serializer 吗? serializer 其实和 Django 自带的 forms 表单验证是类似的,多了反序列化功能。

schema 我也看了一下,感觉描述数据约束的方式不够优雅和直观。

python-validator 支持嵌套的。使用 DictField 就行了,文档在这里 https://ausaki.github.io/python-validator/fields/#dictfield。

(:з)∠) 用惯了 jsonschema

回到顶部