身份证号末位的”X”让很多人困惑——它是字母还是数字?为什么有时是 X 有时是数字?校验位的 ISO 7064 mod 11-2 算法是个简洁优雅的数学构造,理解它能让你正确实现身份证验证逻辑、避免”用 int 解析报错”等常见 bug。
校验位的本质
身份证号 = 前 17 位 + 第 18 位(校验位)
= 行政区划 + 出生日期 + 顺序码 + 校验位
行政区划:6 位
出生日期:8 位(YYYYMMDD)
顺序码:3 位(同区域同日期内编号)
校验位:1 位(算法生成)
校验位的作用:检测号码是否被错误输入(如键盘打错一位)。
算法选择:ISO 7064 mod 11-2 —— 国际标准化组织的”模 11 加权 2 位幂”算法。
为什么是 mod 11 不是 mod 10?
mod 11 的结果:0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
共 11 个可能值
校验位是 1 位字符 → 9 表示 9 → 10 用什么?
答:用罗马数字 X 表示 10
数学优势:
- mod 11 能检出几乎所有单位错误(94-95%)
- mod 10 只能检出 ≈70% 单位错误
- mod 11 能检出多数相邻交换错误(如 12 写成 21)
- 11 是质数,数学性质优秀
X 的选择:
- 罗马数字传统(10 = X)
- ASCII 字符(任何键盘都能输入)
- 单字符 + 不易混淆
完整算法
步骤 1:权重序列
位置: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
权重: 7 9 10 5 8 4 2 1 6 3 7 9 10 5 8 4 2
权重序列不是随机的——是 2 的幂在 mod 11 下的循环:
2^0 = 1 mod 11 = 1 位置 8
2^1 = 2 mod 11 = 2 位置 7, 17
2^2 = 4 mod 11 = 4 位置 6, 16
...
2^16 mod 11 = 7 位置 1, 11
步骤 2:加权求和
身份证号:1 1 0 1 0 5 1 9 8 4 0 1 0 1 0 0 5
权重: 7 9 10 5 8 4 2 1 6 3 7 9 10 5 8 4 2
乘积: 7+9+0+5+0+20+2+9+48+12+0+9+0+5+0+0+10
求和 = 136
步骤 3:mod 11 + 查表
136 mod 11 = 4
查表:
┌────────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬─────┐
│ mod 11 │ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │ 10 │
├────────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼─────┤
│ 校验位 │ 1 │ 0 │ X │ 9 │ 8 │ 7 │ 6 │ 5 │ 4 │ 3 │ 2 │
└────────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴─────┘
mod 11 = 4 → 校验位 = "8"
完整身份证号:110105198401010058
多语言代码实现
Python
def calc_checksum(id17: str) -> str:
weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
total = sum(int(d) * w for d, w in zip(id17, weights))
return "10X98765432"[total % 11]
def validate_id(id_card: str) -> bool:
if len(id_card) != 18:
return False
if not id_card[:17].isdigit():
return False
if id_card[17] not in "0123456789X":
return False
return calc_checksum(id_card[:17]) == id_card[17].upper()
# 使用
print(validate_id("110105198401010058")) # True
JavaScript
function calcChecksum(id17) {
const weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
let total = 0;
for (let i = 0; i < 17; i++) {
total += parseInt(id17[i]) * weights[i];
}
return "10X98765432"[total % 11];
}
function validateId(idCard) {
if (idCard.length !== 18) return false;
if (!/^\d{17}[\dX]$/i.test(idCard)) return false;
return calcChecksum(idCard.slice(0, 17)) === idCard[17].toUpperCase();
}
Java
public class IdCardValidator {
private static final int[] WEIGHTS = {7,9,10,5,8,4,2,1,6,3,7,9,10,5,8,4,2};
private static final String CHECK_MAP = "10X98765432";
public static String calcChecksum(String id17) {
int total = 0;
for (int i = 0; i < 17; i++) {
total += Character.getNumericValue(id17.charAt(i)) * WEIGHTS[i];
}
return String.valueOf(CHECK_MAP.charAt(total % 11));
}
public static boolean validateId(String idCard) {
if (idCard.length() != 18) return false;
if (!idCard.substring(0, 17).matches("\\d{17}")) return false;
char last = Character.toUpperCase(idCard.charAt(17));
if (!Character.isDigit(last) && last != 'X') return false;
return calcChecksum(idCard.substring(0, 17)).charAt(0) == last;
}
}
Go
func calcChecksum(id17 string) string {
weights := []int{7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2}
checkMap := "10X98765432"
total := 0
for i, c := range id17 {
total += int(c-'0') * weights[i]
}
return string(checkMap[total%11])
}
常见 Bug 清单
Bug 1:权重顺序写反
错误:
weights = [2, 4, 8, ...] # 反向
原因:权重表网上有”从右到左”的版本——不是 ISO 7064 标准方向。
修复:固定使用 [7,9,10,5,8,4,2,1,6,3,7,9,10,5,8,4,2] 从位置 1 到位置 17。
Bug 2:校验表映射错位
错误:
return str(total % 11) # mod 11 = 4 时返回 "4"
正确:
return "10X98765432"[total % 11] # mod 11 = 4 时返回 "8"
记忆口诀:mod 0 → “1”,是因为 11 - 0 = 11,再 mod 11 + 偏移得 “1”。
Bug 3:忽略 X 大小写
错误:
if id_card[17] != calc_checksum(id_card[:17]): # 用户输入小写 x
修复:
if id_card[17].upper() != calc_checksum(id_card[:17]):
Bug 4:用 int 解析整个号
错误:
num = int(id_card) # 如果含 X 就报错
修复:分别处理前 17 位(数字)和第 18 位(字符)。
Bug 5:长度判断不严
错误:
if len(id_card) >= 15: # 接受 15 位老身份证
return calc_checksum(id_card[:17]) == id_card[17]
问题:15 位身份证无校验位,逻辑错。
修复:
if len(id_card) == 15:
# 15 位老身份证转 18 位
id_card = convert_15_to_18(id_card)
elif len(id_card) != 18:
return False
Bug 6:行政区划检查过严
错误:拿到旧身份证号 → 行政区划码已废弃 → 拒绝。
问题:行政区划会改,但身份证号不变——人仍合法。
修复:行政区划检查仅作”参考”,不作必要条件。
15 位身份证号的转换
1999 年前发的身份证是 15 位(无校验位、年份只有 2 位):
15 位:行政区划(6) + 年(2) + 月(2) + 日(2) + 顺序(3)
例:320101 84 01 01 234
18 位:行政区划(6) + 年(4) + 月(2) + 日(2) + 顺序(3) + 校验位(1)
例:320101 1984 01 01 234 X
转换:
1. 把年份 2 位扩展为 4 位(约定 1900-2000)
2. 计算校验位
3. 拼接
def convert_15_to_18(id15: str) -> str:
# 假设 1900 年代(或根据顺序码奇偶判断更精确,但简化)
year_2 = id15[6:8]
year_4 = "19" + year_2 # 简化
id17 = id15[:6] + year_4 + id15[8:]
return id17 + calc_checksum(id17)
注意:年份扩展是约定的——20 世纪上半叶生的人也是”19xx”,不存在 2000 年后用 15 位身份证(早就换 18 位了)。
算法验证 vs 真实性验证
算法验证(本地可做):
✓ 长度 = 18
✓ 前 17 位是数字
✓ 第 18 位是 0-9 / X
✓ 校验位算法匹配
✓ 行政区划码合法(GB/T 2260 查表)
✓ 出生日期合法(不超过当前日期 + 不早于 1900)
通过 = "号码格式合法"
但不能证明这个号对应真实人
真实性验证(需要外部接口):
→ 二要素核验:姓名 + 身份证号 调公安部接口
→ 三要素核验:+ 照片
→ 实人认证:+ 活体检测
通过 = 真实存在 + 与照片本人匹配
业务选择:
| 场景 | 验证级别 |
|---|---|
| 普通注册 | 算法验证 |
| 网约车 / 外卖注册 | 算法 + 二要素 |
| 金融账户开户 | 算法 + 二要素 + 实人认证(活体) |
| 政府办事 | 算法 + 公安部直连 |
脱敏与隐私
身份证号是 PIPL 定义的敏感个人信息——存储和传输需要加密:
显示脱敏:
原:110105198401010058
脱敏:110105********0058(隐藏中间出生日期)
存储加密:
原文:不能存
加密:AES-256 加密 + 密钥分离
HASH:HMAC-SHA256 + salt(用于查询而不解密)
日志:绝不打印身份证号
反推性别:
def get_gender(id_card: str) -> str:
"""第 17 位奇数为男,偶数为女"""
return "男" if int(id_card[16]) % 2 == 1 else "女"
实战清单
✅ 必做:
- 算法实现严格按 ISO 7064 mod 11-2
- 权重序列 [7,9,10,5,…,4,2] 固定
- 校验表 “10X98765432” 固定
- X 大小写都接受 + 内部转大写
- 算法验证 + 业务关键场景加二要素核验
❌ 避免:
- 用
int(id_card)解析(X 报错) - 假设权重顺序可逆
- 校验表映射搞错
- 仅做算法验证就当真实
- 日志 / 错误信息打印完整身份证号
身份证号校验位算法是个优雅的工程实践——理解 mod 11 的设计、正确实现算法、配合业务级真实性验证,能避免常见的”用 int 报错""算法实现错”等坑。