正则表达式实战:从身份证号校验码反推,教你写出更精准的验证规则
身份证号码验证是开发中常见的需求,但大多数开发者只是简单地复制网上的正则表达式,却不知道背后的设计逻辑。本文将带你从校验码的计算公式出发,逆向推导出完整的身份证号验证规则,让你真正掌握正则表达式的设计精髓。
1. 身份证号码的结构解析
18位身份证号码并非随机组合的数字,而是经过精心设计的特征组合码。理解其结构是编写验证规则的基础:
- 地址码(前6位):代表户籍所在地的行政区划代码
- 前两位表示省份(如11代表北京,31代表上海)
- 中间两位表示地级市
- 后两位表示区县
- 出生日期码(8位):格式为YYYYMMDD
- 年份:1900-2099
- 月份:01-12
- 日:根据月份和闰年情况变化
- 顺序码(3位):同一地区同一天出生人员的顺序编号
- 奇数分配给男性,偶数分配给女性
- 校验码(1位):根据前17位计算得出,可能是0-9或X
2. 校验码的计算原理
校验码是整个身份证验证系统的核心,理解它的计算方式才能设计出精准的正则表达式。
2.1 加权因子与计算公式
校验码的计算使用了一套固定的加权因子:
位置i: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Wi: 7 9 10 5 8 4 2 1 6 3 7 9 10 5 8 4 2计算步骤:
- 计算加权和:S = Sum(Ai × Wi)
- 计算模:Y = mod(S, 11)
- 根据Y值查找校验码:
| Y值 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 校验码 | 1 | 0 | X | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 |
2.2 计算示例
以身份证号"11010519491231002X"为例:
位置: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 数字:1 1 0 1 0 5 1 9 4 9 1 2 3 1 0 0 2 Wi: 7 9 10 5 8 4 2 1 6 3 7 9 10 5 8 4 2 计算:1×7 +1×9 +0×10 +1×5 +0×8 +5×4 +1×2 +9×1 +4×6 +9×3 +1×7 +2×9 +3×10 +1×5 +0×8 +0×4 +2×2 = 157 Y = 157 % 11 = 3 校验码 = X (与第18位一致)3. 从校验码反推前17位的约束条件
理解了校验码的计算方式后,我们可以逆向推导出前17位必须满足的条件,这些条件将直接转化为正则表达式的各个部分。
3.1 地址码的约束
地址码必须符合国家行政区划编码规则:
- 第一位:1-9(不能为0)
- 前两位:有效的省份代码(11-91之间的特定值)
- 后四位:有效的市县代码
对应的正则部分:
^[1-9]\d{5}3.2 出生日期码的约束
出生日期是最复杂的部分,需要考虑:
年份范围:1900-2099
(19|20)\d{2}月份和日期的组合:
- 31天的月份:01,03,05,07,08,10,12
(01|03|05|07|08|10|12)(0[1-9]|[12]\d|3[01]) - 30天的月份:04,06,09,11
(04|06|09|11)(0[1-9]|[12]\d|30) - 2月份:区分闰年和平年
- 闰年:
02(0[1-9]|[12]\d) - 平年:
02(0[1-9]|1\d|2[0-8])
- 闰年:
- 31天的月份:01,03,05,07,08,10,12
3.3 顺序码和校验码
顺序码是3位数字,校验码是数字或X:
\d{3}[\dXx]$4. 构建完整的正则表达式
结合上述所有约束条件,我们可以构建完整的正则表达式。考虑到闰年判断的复杂性,通常需要根据年份动态生成正则表达式。
4.1 静态正则表达式(区分闰年)
闰年版本:
^[1-9]\d{5}(19|20)\d{2}((01|03|05|07|08|10|12)(0[1-9]|[12]\d|3[01])|(04|06|09|11)(0[1-9]|[12]\d|30)|02(0[1-9]|[12]\d))\d{3}[\dXx]$平年版本:
^[1-9]\d{5}(19|20)\d{2}((01|03|05|07|08|10|12)(0[1-9]|[12]\d|3[01])|(04|06|09|11)(0[1-9]|[12]\d|30)|02(0[1-9]|1\d|2[0-8]))\d{3}[\dXx]$4.2 MySQL中的实现方案
在MySQL中,我们可以创建一个函数来动态判断闰年并选择相应的正则表达式:
DELIMITER // CREATE FUNCTION validate_id_card(id_card VARCHAR(18)) RETURNS BOOLEAN DETERMINISTIC BEGIN DECLARE year INT; DECLARE is_leap_year BOOLEAN; DECLARE regex_pattern VARCHAR(300); -- 检查长度 IF LENGTH(id_card) != 18 THEN RETURN FALSE; END IF; -- 提取年份 SET year = SUBSTRING(id_card, 7, 4); -- 判断闰年 SET is_leap_year = (year % 400 = 0) OR (year % 100 != 0 AND year % 4 = 0); -- 设置正则表达式 IF is_leap_year THEN SET regex_pattern = '^[1-9]\\d{5}(19|20)\\d{2}((01|03|05|07|08|10|12)(0[1-9]|[12]\\d|3[01])|(04|06|09|11)(0[1-9]|[12]\\d|30)|02(0[1-9]|[12]\\d))\\d{3}[\\dXx]$'; ELSE SET regex_pattern = '^[1-9]\\d{5}(19|20)\\d{2}((01|03|05|07|08|10|12)(0[1-9]|[12]\\d|3[01])|(04|06|09|11)(0[1-9]|[12]\\d|30)|02(0[1-9]|1\\d|2[0-8]))\\d{3}[\\dXx]$'; END IF; -- 执行验证 RETURN id_card REGEXP regex_pattern; END // DELIMITER ;5. 验证逻辑的完整实现
除了正则表达式验证外,完整的身份证验证还应包括:
校验码验证:
def validate_check_digit(id_card): if len(id_card) != 18: return False # 加权因子 weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2] # 校验码对应关系 check_codes = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'] # 计算加权和 total = 0 for i in range(17): total += int(id_card[i]) * weights[i] # 计算校验码 mod = total % 11 expected_check = check_codes[mod] # 比较校验码 return id_card[-1].upper() == expected_check地址码验证:
def validate_area_code(id_card): province_codes = ['11', '12', '13', '14', '15', '21', '22', '23', '31', '32', '33', '34', '35', '36', '37', '41', '42', '43', '44', '45', '46', '50', '51', '52', '53', '54', '61', '62', '63', '64', '65', '71', '81', '82', '91'] return id_card[:2] in province_codes完整验证流程:
def validate_id_card(id_card): # 基础检查 if not isinstance(id_card, str) or len(id_card) != 18: return False # 正则表达式验证 if not re.match(r'^[1-9]\d{5}(19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$', id_card): return False # 校验码验证 if not validate_check_digit(id_card): return False # 地址码验证 if not validate_area_code(id_card): return False return True
在实际项目中,我发现最常出现问题的环节是校验码计算和闰年判断。特别是在处理大量数据时,预先验证地址码可以快速过滤掉大部分无效数据,提高验证效率。