身份证号校验位算法:ISO 7064 mod 11-2 工程实现与常见 bug

· 约 5 分钟 🪪 身份证解析

身份证号末位的”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 "女"

实战清单

必做

  1. 算法实现严格按 ISO 7064 mod 11-2
  2. 权重序列 [7,9,10,5,…,4,2] 固定
  3. 校验表 “10X98765432” 固定
  4. X 大小写都接受 + 内部转大写
  5. 算法验证 + 业务关键场景加二要素核验

避免

  1. int(id_card) 解析(X 报错)
  2. 假设权重顺序可逆
  3. 校验表映射搞错
  4. 仅做算法验证就当真实
  5. 日志 / 错误信息打印完整身份证号

身份证号校验位算法是个优雅的工程实践——理解 mod 11 的设计、正确实现算法、配合业务级真实性验证,能避免常见的”用 int 报错""算法实现错”等坑。

❓ 常见问题

身份证最后一位的 X 是什么?为什么不是数字?

X 是罗马数字 10——不是字母 X。算法:(1) 校验位 = 前 17 位经过加权求和后 mod 11;(2) mod 11 的结果可能是 0、1、2、...、10 共 11 个值;(3) 校验位是单字符 → 9 不够用 → 用 X 表示 10;(4) 用罗马数字 X 是因为它在 ASCII 范围内 + 历史习惯。为什么用 11 而不是 10?:(1) 检错能力强——mod 11 能检出几乎所有单数字错误 + 大部分相邻数字交换错误;(2) mod 10 检错能力弱(只能检出 ≈70% 单字符错误);(3) 11 是质数 → 数学性质好。为什么不用别的字母:(1) X 在英文 / 中文键盘都好输入;(2) 历史标准(ISO 7064 mod 11-2)的传统选择;(3) 大写不易混淆(小写 x 可能与字母 x 混淆)。陷阱:(1) 输入 X 必须大写 —— 部分系统不接受小写 x;(2) 不能写 "10" 占两位 —— 校验位固定 1 位;(3) 系统设计时 char[18] 而不是 int —— 否则存不了 X。

身份证号校验位的算法是怎么运作的?

ISO 7064 mod 11-2 算法步骤:(1) 前 17 位每位乘以对应的权重(权重序列:7,9,10,5,8,4,2,1,6,3,7,9,10,5,8,4,2);(2) 把所有乘积求和;(3) 求和 mod 11;(4) 用结果查表得到校验位字符。查表:``\\nmod 11 结果: 0 1 2 3 4 5 6 7 8 9 10\\n校验字符: 1 0 X 9 8 7 6 5 4 3 2\\n`完整算法`python\\ndef checksum(id17):\\n weights = [7,9,10,5,8,4,2,1,6,3,7,9,10,5,8,4,2]\\n total = sum(int(d) * w for d, w in zip(id17, weights))\\n map = "10X98765432"\\n return map[total % 11]\\n``。:身份证前 17 位 11010519491231002,按权重计算:(1*7) + (1*9) + (0*10) + ... = 总和。陷阱:(1) 权重顺序不能搞反(权重表从左到右,对应身份证从第 1 位到第 17 位);(2) 校验表的映射关系不能记错;(3) X 是第 3 个,不是第 11 个。

我的代码校验出错,常见 bug 是什么?

几个高频错误1. 权重顺序写反:(1) 错误:把权重数组当成 17 → 1 反向用;(2) 正确:权重 [7,9,10,5,8,4,2,1,6,3,7,9,10,5,8,4,2] 从左到右对应身份证第 1 位到第 17 位。2. 校验表错位:(1) 错误:以为 mod 0 = 0,mod 1 = 1(顺序);(2) 正确:mod 0 = "1",mod 1 = "0",mod 2 = "X",依此类推(map = "10X98765432")。3. 不处理 X:(1) 把 X 当字母直接 int(X) 报错;(2) 正确:拿到字符串后再判断 / 在最后一步映射时处理。4. 假定全是数字:(1) int(id_card) 转换 —— X 让函数报错;(2) 正确:把前 17 位当数字 + 第 18 位单独处理。5. 长度判断错:(1) 18 位身份证不一定是身份证 —— 可能是手机号、随机串;(2) 正确:先验长度,再验全是数字 + 最后一位是 0-9 / X,再算校验位。6. 历史 15 位身份证:(1) 1999 年前发的身份证是 15 位 —— 没校验位;(2) 转 18 位需要补出生年的世纪和重新算校验位;(3) 验证 15 位的"校验位"是无效的(没有这一位)。7. 跨国数据混入:(1) 香港 / 澳门 / 台湾身份证格式不同;(2) 不要用大陆算法验证它们。

18 位身份证号能从中读出哪些信息?

6 类信息前 6 位 = 行政区划代码(GB/T 2260):(1) 前 2 位:省 / 直辖市;(2) 第 3-4 位:地级市 / 自治州;(3) 第 5-6 位:县 / 区。例:110105 = 北京市朝阳区。第 7-14 位 = 出生日期(YYYYMMDD):(1) 19840101 = 1984 年 1 月 1 日;(2) 用于年龄计算 / 退休时间;(3) 部分场景脱敏只保留年。第 15-17 位 = 顺序码:(1) 同一行政区域同一天出生的人的编号;(2) 奇数 = 男,偶数 = 女(第 17 位决定);(3) 顺序码不连续 —— 不是按办证顺序,是预分配。第 18 位 = 校验码:见上面的算法。用途:(1) 身份证脱敏 —— 中间 8 位(生日)打 *;(2) 性别推断 —— 第 17 位;(3) 年龄计算 —— 7-14 位;(4) 行政归属 —— 1-6 位。注意**:(1) 校验位算法只验证"号码格式合法" —— 不验证此身份证真实存在;(2) 真实存在性需要查公安部数据库;(3) 已知格式合法但无对应人 —— 是测试 / 编造数据。

怎么生成测试用的身份证号?

用算法反向生成 + 标记测试用途正向算法:(1) 凑前 17 位(行政区划 + 日期 + 顺序);(2) 按校验算法算第 18 位;(3) 拼接得到合法身份证号。示例代码:``python\\nimport random\\n\\ndef generate_test_id():\\n region = "110101" # 北京东城\\n date = "19900101"\\n seq = f"{random.randint(0, 999):03d}"\\n id17 = region + date + seq\\n \\n weights = [7,9,10,5,8,4,2,1,6,3,7,9,10,5,8,4,2]\\n total = sum(int(d) * w for d, w in zip(id17, weights))\\n check = "10X98765432"[total % 11]\\n \\n return id17 + check\\n``。注意:(1) 算法生成的号"格式合法" —— 但不一定真实存在;(2) 真实公民数据库有可能撞号 —— 概率极低;(3) 测试用建议用 明显假的号段(如 000000 开头 / 行政区划虚构)—— 避免误用真实数据。法律警告:(1) 真实身份证号属于个人敏感信息(PIPL);(2) 即使算法生成 —— 如果碰巧匹配真实人 —— 仍可能构成侵权;(3) 测试数据必须明确标记 + 不能用于生产业务。

身份证号验证应该用前端还是后端?

两端都做,目的不同前端验证:(1) 即时反馈用户体验;(2) 可疑输入(错误长度 / 含字母)立即提示;(3) 不能信任前端结果 —— 攻击者可绕过。后端验证:(1) 真正的安全保障;(2) 防止伪造请求;(3) 与公安部接口对接(如果业务需要)确认真实存在。算法验证 vs 真实性验证:(1) 算法验证 —— 校验位 + 行政区码 + 日期合法 —— 仅证明"格式合法",可能仍是假的;(2) 真实性验证 —— 调用公安部"二要素核验"接口(姓名 + 身份证号匹配)—— 真正确认存在;(3) 三要素核验(加照片)—— 实人认证;(4) 多数业务用二要素就够。API 提供方:(1) 阿里云 / 腾讯云 / 各银行 / 公安部直连;(2) 单次调用 0.05-0.5 元;(3) 必须用户授权。实务:(1) 普通注册 —— 前端格式 + 后端格式(无需公安部接口);(2) 金融 / 证券开户 —— 前端 + 后端 + 二要素核验 + 实人认证(活体);(3) 实名注册 —— 前端 + 二要素就够。

行政区划代码可以反向查地址吗?

前 6 位映射到县级行政区机制:(1) GB/T 2260 标准;(2) 每个县级行政区域有唯一 6 位代码;(3) 民政部不定期更新(行政区划调整)。典型查询:(1) 110105 → 北京市朝阳区;(2) 320103 → 江苏省南京市玄武区。陷阱:(1) 行政区划会变 —— 某区撤销 / 合并后旧代码可能失效,但身份证号不变;(2) 历史代码(如老身份证)可能找不到对应当下行政区;(3) 改区划后的人仍用旧代码身份证 —— 不需要换证,但查询要查历史;(4) 同名地区 —— 不同省都有"东城区",必须看完整 6 位代码。实务:(1) 用 GB/T 2260 标准库(pypi: cnstdxpinyin)查询;(2) 接受历史代码 —— 不要因为区划改了就拒绝旧身份证;(3) 仅做"显示用途"——不能根据行政区域做业务判断(如限制服务范围)。

校验位算法能防伪造吗?

只能防"低级伪造"算法能检测到的错误:(1) 单数字错误(90%+ 检出率);(2) 相邻数字交换(多数检出);(3) 长度错误;(4) 含非法字符。算法检测不到的:(1) 完整伪造的合法号 —— 攻击者按算法生成符合规则的假号;(2) 撞号 —— 假号正好等于真号(极小概率但存在);(3) 行政区划伪造 —— 用真实行政区代码 + 编造日期顺序,校验位匹配。对抗高级伪造:(1) 二要素核验(姓名 + 身份证号)—— 攻击者要凑齐两者匹配;(2) 三要素(+ 照片)—— 实人认证;(3) 活体检测 —— 防换脸;(4) 公安部直连 —— 最权威。实务:(1) 算法验证是 第一道闸 —— 拦掉机器随机输入;(2) 不能仅靠它防欺诈;(3) 真实性 / 实名 / 活体是后续多层防御。陷阱:很多业务系统只做算法验证 —— 用户填假号也能注册 —— 这就给"完美伪造"留了口子。

🪪 打开 身份证解析 校验·生日性别·户籍地·脱敏·批量导出

📖 同一工具的其他教程