逻辑校验:从源头卡住脏数据

数据校验这件事,说白了就是给数据入口装个过滤器。
我以前带新人时,见过太多「跑了两小时 ETL 才发现源头字段写错了」的惨案。
逻辑校验就是用来堵这个窟窿的。它一共分四类:范围、格式、正则、枚举。
每一类都有自己擅长的地方,也都有各自的坑。

范围校验

范围校验最直观。你想想看,年龄不可能 200 岁,温度不可能 -300 度。
它的任务就是保证数据落在合理区间内。

常见场景

  • 年龄:0 ~ 150
  • 温度:-40 ~ 85
  • 页码:≥ 1
  • 百分比:0 ~ 100

实现方式

def validate_range(value, min_v, max_v):
    if value is None:
        return False
    return min_v <= value <= max_v

# 例子
assert validate_range(25, 0, 150) == True
assert validate_range(-5, 0, 150) == False
注意边界值
我遇到过好多次「漏掉边界」的事。
比如时间区间校验,有的人写 start < end,但忘了考虑 start == end 的情况。
边界测试是一定要加的。

格式校验

格式校验检查数据的长相是否符合预期。
比如手机号必须是 11 位,邮箱必须带 @。
这种校验往往在数据录入阶段做,但下游做分析时也得补一刀。
我记得有一次,生产环境有批手机号多了空格和横杠。格式校验没做 trim,结果模型训练直接崩了。

常见格式校验项

字段 预期格式 校验逻辑
手机号 纯数字,11 位 len == 11 且 isdigit()
邮箱 包含 @ 和 . @ 位置不能为首尾
身份证 18 位,字母结尾 len == 18,末位可为 X
邮政编码 6 位数字 len == 6

实现示例

def validate_phone(phone):
    cleaned = phone.strip().replace(" ", "").replace("-", "")
    return len(cleaned) == 11 and cleaned.isdigit()
我的习惯
做格式校验时,一定先做归一化。
空格、全角符号、BOM 字符,这些都是隐藏刺客。
归一化后再校验,不然误报率高得离谱。

正则表达式校验

正则表达式是格式校验的增强版。当格式规则比较复杂时,就得靠它了。
比如密码强度:必须包含大写、小写、数字、特殊字符,长度 8-20 位。
你很难用 if-else 堆出可维护的代码,但正则一行就能搞定。

举个实际的

import re

pattern = r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,20}$'

def validate_password(pwd):
    return bool(re.match(pattern, pwd))

正则写起来爽,可读性差。我在项目中踩过几次「正则回溯超时」的坑。
比如一个复杂的嵌套模式,遇上特殊输入,直接让 CPU 飙到 100%。
所以我的建议是:

  • 能用简单字符串方法解决的,就别上正则
  • 正则尽量写成分段注解模式
  • 给正则加个超时保护

常用正则速查

场景 正则
邮箱 ^[\w\.-]+@[\w\.-]+\.\w+$
URL ^https?://[\w./-]+$
IPv4 ^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$
日期(yyyy-mm-dd) ^\d{4}-\d{2}-\d{2}$
避坑指南
我曾经接手过一个项目,正则全部写在一行,没有注释也没有分组。
后来改需求时,没人敢动那段代码。
强烈建议使用 verbose 模式:
pattern = re.compile(r"""
    ^
    (?=.*[a-z])      # 至少一个小写
    (?=.*[A-Z])      # 至少一个大写
    (?=.*\d)         # 至少一个数字
    [A-Za-z\d@$!]{8,20}
    $
""", re.VERBOSE)

枚举值校验

有些字段的值只能从固定的集合里选。
比如性别:男、女、其他;状态码:0、1、2。
枚举值校验干的就是这个事。

它在很多场景里被低估了。你想想看,状态字段如果允许了脏数据,下游 join 会丢数据,聚合会歪曲结果。
我在一个风控项目里就吃过这个亏。当时有个字段叫 risk_level,枚举是 LOW、MEDIUM、HIGH。
结果有人写了个 low(小写)。线上跑了两周才发现,模型 AUC 降了 5 个点。

实现方式

VALID_STATUS = {0, 1, 2}

def validate_status(value):
    return value in VALID_STATUS

高级用法:反查 + 容错

如果你想让系统更健壮,可以这样:

STATUS_MAP = {
    "active": 1,
    "inactive": 2,
    "pending": 0
}

def normalize_status(raw):
    raw = raw.strip().lower()
    if raw in STATUS_MAP:
        return STATUS_MAP[raw]
    # 匹配不到时返回 None,不硬猜
    return None
大小写与空格
枚举校验最常见的 bug 就是大小写不一致。
我从那以后制定了规则:所有枚举字段在校验前统一 lower() + strip()。
这能挡掉 80% 的脏数据。
逻辑校验四种类型 原始数据 范围校验 格式校验 正则校验 枚举值校验 汇聚校验结果 四种校验可以组合使用,按需选择合适的逻辑 避免不必要的性能开销,也避免遗漏关键规则

四种校验的适用场景对比

校验类型 典型字段 适用场景 注意事项
范围校验 年龄、温度、金额 数值型字段的合理性检查 注意边界值
格式校验 手机号、邮箱 字段长度或模式的匹配 先归一化再校验
正则校验 密码、URL、IP 复杂且固定的文本模式 加超时,用 verbose
枚举校验 状态码、性别 取值有限且固定的字段 统一大小写与去空格

逻辑校验这东西,做多了你会发现,它不只是检测数据,其实也在反向约束流程。
数据在源头被卡住,下游就少了很多麻烦。
我自己的习惯是在每张表入库前加一层校验配置,写成 JSON 文件,方便随时增删规则。
这样一来,数据质量就不再是「出了问题再修」,而是「从一开始就把它拦下来」。