3. 逻辑校验
数据校验这件事,说白了就是给数据入口装个过滤器。
我以前带新人时,见过太多「跑了两小时 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 文件,方便随时增删规则。
这样一来,数据质量就不再是「出了问题再修」,而是「从一开始就把它拦下来」。