本文来自笔者在另一个平台的博客。探讨了在Postgres环境中,使用单SQL语句实现中国居民身份证号码的正确性检验工作。为了讨论方便和简单起见,这里主要讨论通过校验码来检查其编码的合规性(没有更严格的地域和时间检查)。
基本要求
作为学术讨论,其要求和约束如下
- 单语句实现,不使用psql、复合语句或者自定义函数
- 输入为一个身份证号码,输出为检查结果 T 或者 F
- 主要通过编码和校验规则进行检查
第一条要求主要是为了考虑到可以作为单一SQL语句,嵌入到开发程序和代码实现当中,方便使用和修改。当然作为完整的处理方案,也可以使用存储过程或者自定义函数的方式。本例中使用postgres语法,mysql或者oracle应该也可以采用类似的思路。
编码和校验规则
中国居民身份证号码的编码规则为:
- 二代身份证号码为18位,前17位为数字,最后一位为数字或者'X'
- 前6位为地域编码,表示登记户口时所在地的行政区划代码,具体为省-市州-区县各两位,基本基于行政区划编码标准(GB/T2260)
- 中间8位为出生日期,编码方式为YYYYMMDD,年月日
- 再三位为登记序号,前面补0,奇数分配给男性,偶数分配给女性
- 最后一位(第18位)为信息校验码,可以是数字,也可以是X,此处其实是罗马数字10,具体参见校验规则
和第一代身份证号码编码规则相比,第一代的编码长度为15位,二代编码使用4位来表示年份, 并增加了一个校验位,长度增加到了18位。校验码的设计,其实来自一个ISO标准: ISO 7064: 1983, MOD 11-2。基于此标准的信息编码校验规则的计算方式如下:
- 先定义一个校验数组V(权重数组,作为数字,在0~10中取值,数组长度同要校验的信息长度),这里的具体内容为:
7-9-10-5-8-4-2-1-6-3-7-9-10-5-8-4-2-0
- 再定义一个检查数组,内容为:
1-0-X-9-8-7-6-5-4-3-2
- 将身份证号码按照字符拆分为17个数字组成的信息数组
- 遍历信息数组,将信息数组的元素和校验数组的对应元素相乘,然后求和,得到一个校验码
- 校验码除以11,取余数,作为校验码位置
- 在检查数组中,校验码位置对应的字符,即为校验字符
- 此字符应该和身份证号码最后一个字符匹配
实现
根据上面讨论的校验计算规则,我们可以编写和实现的SQL代码(示例数据)如下:
with
D(idnumber) as ( values
('620304198301251574'),
('620104197301251574'),
('620104197301251575')
),
C as (select idnumber, regexp_split_to_table('7-9-10-5-8-4-2-1-6-3-7-9-10-5-8-4-2-0','-')::int c1, regexp_split_to_table(idnumber,'')::int c2 from D ),
A as (select idnumber, sum(c1*c2) % 11 + 1 c3 from C group by 1)
select A.idnumber, case when (substring('10X98765432',c3::int,1) = substring(idnumber,18,1)) then 'T' else 'F' end cresult from A;
-- 另一个版本,使用字符串作为输入
with
D as (select regexp_split_to_table('620104197301251574,510125200412160158,51012520041216017X',',') d0,
'7-9-10-5-8-4-2-1-6-3-7-9-10-5-8-4-2' d1 ),
A as (select d0, regexp_split_to_table(substring(d0,1,17),'')::int c1, regexp_split_to_table(d1,'-')::int c2 from D),
B as (select d0, sum(c1*c2) % 11 + 1 c3 from A group by 1)
select D.d0, case when (substring('10X98765432',c3::int,1) = substring(D.d0,18,1)) then 'T' else 'F' end from B,D where D.d0= B.d0;
简单说明一下:
- D是要检查的数据,可以用一个虚表检查多个数据
- 这里只检查长度匹配,并且前17位都是数字的数据
- 使用分组,计算权重乘积和,和11的余数作为位置
- 基于位置查询校验码数值
- 权重数组和校验码数值都作为常量写入SQL,固定算法不需要作为参数
- 最后返回身份证件号码和检查结果T或者F
- 根据这个很容易改写为检查单个数值的语句
局限性
上述解决方案的局限性如下:
- 没有检查其编码规则中的地域和时间等的合理性,地域编码可能会发生变化,但时间编码必须是正确的编码信息,并且有其有效范围,如1900~当前检查日期(由业务决定)
- 不能进行批量检查
由于使用regtotable方法构造数据集,仅能够针对单个数据进行处理。(改进后的版本已经可以批量检查)。
校验机制的思考
这个编码规则有个奇怪的地方是最后一位的编码可能为X,显得不够干净,但笔者觉得可能是有原因的。在没有更多的支持和分析信息情况下,这里尝试分析如下。
校验码规则,不仅仅是校验信息编码是否正确,还可以有修复错误信息的功能性,假设前面的信息有一位由于传输或者其他问题发生了错误,可以通过最后一位校验码,进行补偿计算,从而修复原始信息。因此,最后一位校验码,必须可以表示并且倒推计算缺失的编码,就可能需要11种可能性。
上述分析看起来有道理,但实际上在信息技术环境中可能有问题,因为计算机是按照二进制方式进行工作和计算的,前面的编码规则却是基于人类的十进制进行处理的,所以有可能这种设计是多余的。
现在知道,这个校验机制来自一个ISO标准(ISO 7064: 1983, MOD 11-2),但具体理论基础,笔者还没有时间和精力进一步研究。
JS实现
根据上面的分析,我们就可以很容易的写出JS的实现:
let
idstr = "620104197301251574",
A1 = '7-9-10-5-8-4-2-1-6-3-7-9-10-5-8-4-2-0'.split('-'),
A2 = '1-0-X-9-8-7-6-5-4-3-2'.split("-");
let r = A2.at(idstr.split('').reduce((c,v,i)=>c+parseInt(v)*parseInt(A1[i]),0) % 11);
console.log(r == idstr.slice(-1));