常见问题2026-11-05· 7 分钟阅读

身份证校验位怎么算:ISO 7064 算法、15 位升 18 位与末位 X 传参的 3 个坑

身份证第 18 位校验位的 ISO 7064 MOD 11-2 算法(加权因子表+余数映射表)、15 位升 18 位规则与可直接跑的 Python 实现;末位 X 传参必须转小写 x 否则 1103、校验位通过不等于号码真实、1103 的完整排查顺序。

诺
诺正通研究院

一句话结论

身份证第 18 位是校验位:前 17 位按 ISO 7064 MOD 11-2 加权求和、取 mod 11 后查表得出。它只能证明"这个号在格式上算得通",不能证明号码真实存在——本地校验通过后仍要调核验接口确认一致性。传参时还有一个高频翻车点:算法算出来的是大写 X,而接口要求转小写 x,不转返回 1103 编码不合法。

一、算法:两张表说清

步骤规则
1. 取本体码身份证号前 17 位
2. 乘加权因子7、9、10、5、8、4、2、1、6、3、7、9、10、5、8、4、2
3. 求和取余S = Σ(数字 × 因子),T = S mod 11
4. 查表得校验位T=0→1,1→0,2→X,3→9,4→8,5→7,6→6,7→5,8→4,9→3,10→2

第 4 步是最常见的实现错误:X 对应的是余数 2,不是余数 10。网上不少代码写成 {10: 'X'} 再拿余数直接查——那是把另一套等价公式 R=(12−T) mod 11 里的 R 和 T 混了。两种写法要配套使用,直接拿 T 去查 {10:'X'},会把所有末位为 X 的号码判成校验失败,而这类号码占全部号码的约 1/11。

WEIGHTS = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]

CHECK = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'] # 下标 = 余数 T

def check_digit(first17: str) -> str: s = sum(int(c) * w for c, w in zip(first17, WEIGHTS)) return CHECK[s % 11]

def is_valid_id(id18: str) -> bool: id18 = id18.strip().upper() return len(id18) == 18 and id18[:17].isdigit() \ and check_digit(id18[:17]) == id18[17]

验证用例:前 17 位为 11010519491231002 时 S=167、T=2、校验位 X——即国标示例号 11010519491231002X。写完代码先拿它跑一遍,跑不过就是表抄错了。

先查表再算日期:出生日期段(第 7-14 位)本身有合法性——19990230 这种不存在的日期,校验位算得再对也是废号。日期校验比校验位更早拦住脏数据,两道都要有。

二、15 位升 18 位

15 位老证号没有校验位,结构是 6 位地址码 + 6 位出生日期(YYMMDD)+ 3 位顺序码;18 位是 6 位地址码 + 8 位出生日期(YYYYMMDD)+ 3 位顺序码 + 1 位校验码。升位规则:

  • 在第 6 位后插入 19(15 位证 1999 年停发,持证人出生年份均为 19XX);
  • 得到 17 位后按上一节的表重算校验位拼上;
  • 别忘了性别位下标变了:顺序码末位(奇男偶女)在 15 位制里是第 15 位,升位后变成第 17 位——取性别的老代码不改下标,升位后的号会全部取错。
  • def upgrade15(id15: str) -> str:
    

    id15 = id15.strip() if len(id15) != 15 or not id15.isdigit(): raise ValueError("15 位号码应为纯数字") body = id15[:6] + "19" + id15[6:] # → 17 位 return body + check_digit(body) # → 18 位

    升位后用 110105491231002 验证,应得到 11010519491231002X。

    接口不收 15 位号:位数不对直接 1103 编码不合法,接口不会帮你升位。批量数据里的老号必须在入参校验阶段本地升位,等接口报错才发现,批量任务已经跑了一半。

    三、传参的 3 个坑

    坑 1:算法输出大写 X,接口要小写 x

    国标校验字符集里的 X 是大写(它是罗马数字 10,不是字母),证件上印的也是大写;但接口文档明确要求末位 X 转小写 x,不转返回 1103。这不是"不一致",是格式拒绝,不计费,但会污染你的失败率统计。

    id_for_api = id18.strip().upper().replace("X", "x")   # 先归一化,再算签名

    顺序不能反:身份证核验的签名串是 md5(mall_id + realname + idcard + tm + appkey),idcard 参与签名。先算签名再转小写,服务端用收到的小写值重算签名必然对不上,报的还是 1104 签名不合法——错误码把你的排障方向带偏到拼接顺序上,而根因只是大小写。

    坑 2:校验位通过 ≠ 号码真实

    校验位的设计目标是发现录入错误(抄错一位、串位),不是鉴别真伪。一个按算法编出来的 18 位号码可以通过全部本地校验,但在权威库里查无此号。所以本地校验的定位是表单前置拦截:把格式错误挡在请求之前,减少无效调用;真实性核验必须走接口,返回 1001 不一致 或 1002 库中无此号 都是有效结果。

    坑 3:校验位对了,仍可能返回 1103

    1103 编码不合法 的完整触发条件不止校验位:末位大小写、前后空格与零宽字符、全角数字、15 位未升位,任何一条都会触发。排查顺序按命中率排:

  • 末位是不是大写 X 没转小写;
  • 有没有不可见字符(零宽空格、全角空格、复制粘贴带的换行)——strip() 挡不住零宽字符,要用正则清;
  • 是不是 15 位老号没升位;
  • 位数数错(17 位 / 19 位)、中间混了非数字。
  • 四、合规边界

    校验位计算是纯本地运算:不联网、不外发号码、不产生接口调用,放开在浏览器表单里做完全没问题,还能在用户提交前就提示"号码有误"。但要注意两件事:

  • 本地校验日志不要打印完整号码——号码是个人信息,排障用前 6 后 4 脱敏足够定位格式问题;
  • 本地校验通过不构成"已核验"的业务依据,业务上的实名结论只能来自接口返回。
  • 五、上线检查单

    □ 校验位用加权因子表 7 9 10 5 8 4 2 1 6 3 7 9 10 5 8 4 2,下标映射 1 0 X 9 8 7 6 5 4 3 2
    

    □ 已用国标示例号 11010519491231002X 跑通自测 □ 出生日期段单独校验(不存在的日期直接拒) □ 归一化(大写 X → 小写 x、去空白、全角转半角)发生在算签名之前 □ 15 位老号入参阶段本地升位,升位后重算校验位 □ 性别位取值:18 位制取第 17 位,15 位制取第 15 位 □ 失败分支区分 1103(格式,不计费)与 1001(不一致,计费),别合并处理 □ 本地校验日志脱敏,不落完整号码

    相关阅读

  • 身份证核验显示不一致?五步排查顺序:格式全对仍返回不一致时的下一步
  • 身份证核验接入十大常见问题:签名三因、超时重复计费、高峰限流
  • 多语言接入示例:Java / PHP / Python / Go / Node.js:各语言的归一化写法
  • 错误码详解:1000-1110 全码表:1103 与 1001、1104 的完整区分
  • 身份证核验 API:支持 100 次免费试用的实名核验接口
  • 关于诺正通

    校验位验算在本地完成,调用核验接口才计费。身份证核验 API 按量计费、100 次免费试用用于联调,具体价格向商务询价。

    #身份证校验位#ISO 7064#校验码算法#15位升18位#末位X

    看完想立刻接入?

    100 次免费调用,1 个工作日开通

    限时新用户 100 次免费

    1 个工作日开通 · 7×24 技术支持