表单校验配方:手机号、邮箱、身份证、密码强度的思路与实现
校验的层次感:格式对 ≠ 数据对
先立一条总纲:正则只能回答"长得像不像",回答不了"是不是真的"。手机号格式合规但可能没注册、邮箱拼写合法但可能是空壳、身份证号格式完美但校验位是错的。本章配方全部定位在"格式粗筛"这一层,真实业务里它前面挡一轮,后面还要接短信验证、实名接口等重型校验。层次摆对了,正则才用得安心。
手机号:宽松比精确更耐用
中国大陆手机号规则:11 位、1 开头、第二位 3-9(现行号段集中在 13x-19x,且不定期新增)。写死号段清单(如只认 13[0-9]|15[0-356]|18[025-9])意味着每次工信部放新号段你都要改代码——校验库的年久失修大多源于此。工程上更耐用的写法:
import re
def is_mobile(s):
s = s.strip().replace(" ", "").replace("-", "")
return re.fullmatch(r"(?:\+?86)?1[3-9]\d{9}", s) is not None
print(is_mobile("+86 138-1234-8000"))
print(is_mobile("12345678901"))
# 输出:True
# 输出:False
三个设计点:允许 +86 前缀(用户粘贴习惯)、剥离空格与横杠(输入框里的格式化残留)、第二位只锁 [3-9] 大区间(号段新增不影响)。校验的精度应该与"误杀真实用户"的成本成反比——错杀一个用户比放过一个假格式贵得多。
邮箱:实用主义的边界
RFC 5322 定义的合法邮箱复杂到可以出一本书(引号、注释、IP 域名……),而你的业务永远用不到。业界通行做法是一条"足够好"的实用版:
import re
EMAIL = re.compile(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}")
print(bool(EMAIL.fullmatch("user.tag+news@shop.example.com")))
print(bool(EMAIL.fullmatch("双斜杠@@example..com")))
# 输出:True
# 输出:False
它拒绝了连续点、缺顶级域等明显异常,也接受一些理论上不该存在的形态——这是有意的取舍。邮箱最终校验永远是"发一封验证邮件",正则只负责把一眼假的拦在门外。
身份证:格式正则 + 校验位代码的组合拳
18 位身份证结构固定:6 位区划码 + 8 位出生日期 + 3 位顺序码 + 1 位校验码。格式正则把结构钉死:
import re
ID = re.compile(r"[1-9]\d{5}(?:18|19|20)\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\d|3[01])\d{3}[0-9Xx]")
print(bool(ID.fullmatch("11010519900307857X")))
# 输出:True
区划首段禁 0、年份限 18xx-20xx、月日双区间封死 13 月 与 32 日。但正则管不了"2 月 30 日"和最后一位校验码,这两层必须交给代码:
W = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
M = "10X98765432"
def checksum_ok(id18):
s = sum(int(a) * b for a, b in zip(id18[:17], W))
return M[s % 11] == id18[-1].upper()
print(checksum_ok("11010519900307857X"))
# 输出:True(GB 11643 标准的加权校验算法)
正则管形状、算法管算术,各司其职——这是"正则 + 少量代码"组合的教科书场景。
密码强度:环视的组合艺术
"至少 8 位,须含大写、小写、数字"用三个先行断言并列表达,可读性远超一坨巨型字符类:
import re
P1 = r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$"
P2 = r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\w\s]).{12,}$"
print(bool(re.search(P1, "Abc2024ok")))
print(bool(re.search(P2, "Abc2024ok!!!")))
# 输出:True(满足中级强度)
# 输出:True(满足高级强度:12 位以上再含特殊符号)
强度分级就是把断言数量与长度下限做乘法:断言越多、位数越长,级别越高。
配方速查表
| 字段 | 配方思路 | 正则只解决的部分 |
|---|---|---|
| 手机号 | 归一化 + 宽松号段 | 格式与前缀 |
| 邮箱 | 实用版字符组 | 外形合法性 |
| 身份证 | 结构正则 + 校验位算法 | 区划/日期/长度结构 |
| 密码 | 环视组合分级 | 组成规则与长度 |
| 用户名 | 字符白名单 + 长度 | 如 [一-龥A-Za-z0-9_]{2,16} |
| 金额 | 两段式小数 | 如 ^(?:0|[1-9]\d{0,7})(?:\.\d{1,2})?$ |
避坑提醒:校验正则最大的生产事故不是"拦错了",而是前后端口径不一致——前端一条宽松版、后端一条严格版,用户在前端通过、提交到后端被拒,反复横跳。把正则收进共享常量或统一的校验模块,两端引用同一条,是比写"完美正则"重要十倍的事。
统一校验入口:别让正则散落在代码里
校验正则多了之后,管理问题浮出水面:这条在 A 文件、那条在 B 函数,前后端口径对不上。解法是收进一个校验注册表,所有规则一处声明:
import re
VALIDATORS = {
"mobile": re.compile(r"(?:\+?86)?1[3-9]\d{9}"),
"email": re.compile(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}"),
"postcode": re.compile(r"[1-9]\d{5}"),
}
def validate(kind, value):
return VALIDATORS[kind].fullmatch(value.strip()) is not None
print(validate("mobile", "+8613812348000"), validate("postcode", "100000"))
# 输出:True True
三个收益立竿见影:前后端引用同一份规则;新增校验只改一处;审查安全规范时正则一览无余。校验正则的基础设施化,比任何单条正则的精雕细琢都更值得投入——它是"正则工程"和"正则技巧"的分界线。
校验失败时的提示语同样需要设计
正则只说"不通过",用户体验靠提示语补。把每条规则的"失败原因"写成人话,而不是"格式错误"四个字打发:
RULES = [
("mobile", "手机号应为 11 位数字,且以 1 开头"),
("email", "邮箱格式如 name@example.com,请检查 @ 与域名"),
("pwd", "密码需 8 位以上,且包含数字与字母"),
]
def hint(kind):
return dict(RULES).get(kind, "输入格式不正确")
print(hint("mobile"))
# 输出:手机号应为 11 位数字,且以 1 开头
设计提示语时有个实用检验法:把提示语读给不懂技术的家人听,他们能否照着改正输入。"手机号应为 11 位数字,且以 1 开头"能,"格式非法"不能。正则是内部实现,提示语是产品的一部分,两者质量要一起验收——这也是为什么校验注册表(上一节)里值得连提示语一起登记。
参考资源
- MDN 断言语法(密码强度环视的基础):MDN 正则·断言
- Python re 模块(fullmatch 语义):re — Regular expression operations
本章由 WorkBuddy 原创撰写,示例均经实际验证。
