身份证号浅析
今天我们来简要了解一下平时经常面对的身份证号码里面包含的信息。很多时候每个人的常识都不一样,你的常识不代表他人的常识,了解多一点有趣的东西应该不是一件坏事。
身份证编码标准
我们可以先去下载身份证标准的 pdf,和文章对照着看。也可以直接看文章,后面通过一个简单的例子进行说明。
公民身份证号标准:公民身份号码 GB 11643-1999
示例身份证号码
我们来以一个不存在的身份证号来说明它所包含的信息:
411324201102310057
身份证号数值含义
地址码
411324
身份证号的前 6 位称为地址码。地址码所引用的标准是:GB/T 2260-1995。你可以在 行政区划代码 看到部分编码,但是严格来说,还是得看上面的这个引用标准。它需要以这种方式进行解读:
- 将后面4位换成0,也就是
410000
> 河南省 - 将后面2位换成0,也就是
411300
> 南阳市 - 最后是原来的数值:
411324
> 镇平县
所以上面地址码 411324
所携带的信息是
河南省南阳市镇平县
表明了这个人出生时登记的派出所的地区位置。
出生日期码
20110231
在地址码后面的 8 位是出生日期码,这个大家从小到大都熟悉得不能再熟悉了。这里就没必要过多说明。上面的日期因为 2 月不存在 31 日,所以上面的身份证是为了说明方便所写的。并不存在这个身份证号码。
另外值得注意的是,这个日期是不好分辨是农历还是阳历的,因为有的地区是可能用农历去登记的。
这个能获取到的信息是出生日期和当前的年龄。
顺序码
005
日期码后的 3 位数是顺序码。它里面有着这样的规则:
- 男性是单数,女性是偶数
# 尾数 0, 2, 4, 6, 8 女性
# 尾数 1, 3, 5, 7, 9 男性
比如 001
就代表了该地区第一个登记的男性。而上面的 005
就代表了这个身份证号是该地区第 3 个进行登记的男性。
也就是说,我们可以简单地从顺序码一眼看出身份证人的性别。
不过这里也存在着我所不太明白的点。从理论上说,这里的取值范围就是 000-999
一共 1000 个数,男女各 500 个。换句话说,该地区一天的出生不会(还是不能?)超过 1000 个。超过了会怎样?还是不可能超过?不太清楚。另外,我看见有软件将 002
作为第一个出生的女性?理论上不是 000
吗?我倒觉得应该是 000
才对,不过也可能 000
也像 ip 地址一样有着特殊的含义,有待考证。
校验码
身份证号的最后一位是校验码位,它是根据前面 17 位计算出来的。其取值范围是 [0-9X]
,这里的 X
代表罗马数字的 10
,换言之,取值范围是 [0-10]
。
总结
从上面的解析中,我们可以了解了身份证号的数值组成和里面所代表的含义。如果校验码说明这个章节太无聊,可以直接看最后的 最后的总结 部分。
校验码说明
校验公式
代表的是身份证的第几位数,从右边算起。 表示该数值对应的权重。具体可以看下面的权重表。
身份证上的每一位数字乘以对应位的权重相加,然后对 11 取余(就是除以 11 的余数)恒等于一。
权重
由公式 计算得出。
换算表
这里引用一下身份证标准 pdf 里面的:
当 i=1 时, 所以,校验公式可表示成:
为了满足该公式,标准里面给出了下面的表 2
这个换算表格的含义是,我们先计算除了最后一位其他17位的数值乘以对应的权重之和,然后对 11 取余。得到的取余结果再根据表格映射出最后一位的数值。
例子说明
将刚才的身份证号码来进行说明好了。
411324201102310057
最后一位是怎么来的呢?我们来编写一个简单的 python 代码来计算一下就知道了。
def get_weights():
"""获取所有的权重值
这里的 i 是从右开始,数值从 1 到 18
"""
w = {}
for i in range(1, 19):
w[i] = 2 ** (i - 1) % 11
return w
def get_last_num(num_str):
w = get_weights()
# 最后一位的数值映射代码直接使用,文章后面有相关说明
last_num_map = {
0: '1', 1: '0', 2: 'X',
3: '9', 4: '8', 5: '7',
6: '6', 7: '5', 8: '4',
9: '3', 10: '2'
}
result = 0
# 这里只是简单进行编写,没有进行校验。
for i, n in enumerate(num_str[:17]):
result += w[18 - i] * int(n)
result = result % 11
return last_num_map[result]
if __name__ == '__main__':
last_num = get_last_num('41132420110231005')
print(last_num)
运行一下,得到结果是 7
,符合预期。其实,这个身份证的数值本来就是这么计算出来的。你可以拿自己的身份证号在本地试一下。
思考
一开始看到上面的校验公式时,我越看越觉得奇怪。我们再看一下。
每一个值乘以对应权重相加,然后对 11 取余,恒等于一。恒等于一......虽说最后一位是从前面 17 位计算得来,其实严格来说,公式本身的含义应该是:
身份证的任何一位都可以通过另外17位计算得来。
这样才合理。要证明也很简单,我们先看一下上面的换算表,也就是
last_num_map = {
0: '1', 1: '0', 2: 'X',
3: '9', 4: '8', 5: '7',
6: '6', 7: '5', 8: '4',
9: '3', 10: '2'
}
这玩意是怎么来的?上面这个是最后一个字符的换算表,所以,理论上,身份证 18 位每一个字符都应该有一个换算表。可能的话,我们需要编写一个函数来获得身份证 18 位每一位的换算表来做为验证的起点。
目标:获取每一个位数对应的换算表。
先来看一个小例子。
17 % 11 = 6
17 % 11 = (12 % 11 + 5 % 11) % 11 = 6
这种方式类似于分配率。因为等会的说明会涉及到。
而上面的校验公式,在一个数值未知的情况下,可以理解成这个样子:
其中, 表示我们不知道的那位数,而 代表了该位数对应的权重值, 代表了其他所有值乘以对应的权重值之和,然后对 11 取余后的数值。 表示身份证的第几位,从右算起。取值范围是 [1-18]
我们可以这么理解:对于整体来说,问号的数值可以取值 [0-10]
, 理论上也是 [0-10]
。当我们其他 17 位都已知的情况下,我们可以先计算出上面公式里问号的值。假设计算出来的结果是 5. 那么在未知位乘以对应权重的前提下,将 的值进行逐个尝试(0-9),有且只有一个数能满足上式的成立。然后我们就可以得到这个第 i 位换算表中 5 的映射。同理,将 [0-10]
当成问号的值进行迭代,就能得到第 i 位整个换算表。
编写一下这个函数来获取任意一位的换算关系表:
def get_weights():
"""获取所有的权重值
这里的 i 是从右开始,数值从 1 到 18
"""
w = {}
for i in range(1, 19):
w[i] = 2 ** (i - 1) % 11
return w
def get_ai_map(i):
"""获取第 i 位的换算关系表
这里的 i 是从右开始,数值从 1 到 18
"""
w = get_weights()
wi = w[i]
ai_map = {}
for result in range(0, 11):
for ai in range(0, 11):
if (ai * wi + result) % 11 == 1:
ai_map[result] = 'X' if ai == 10 else str(ai)
break
return ai_map
if __name__ == '__main__':
print(get_ai_map(1))
上面的代码输出 i = 1 时的换算关系表,也就是身份证标准的 pdf 里面(上面图片中)的表格,来看看是否正确。
{0: '1', 1: '0', 2: 'X', 3: '9', 4: '8', 5: '7', 6: '6', 7: '5', 8: '4', 9: '3', 10: '2'}
可以发现,其映射结果是一模一样的。所以,我们通过 get_ai_map
函数可以方便地得到任意一个位数的换算关系表。由此,我们就能够很简单地推出未知一位的数值。
- 先计算除了未知一位的其他 17 位数乘以对应权重之和对 11 的取余结果;
- 从 get_ai_map 函数拿到未知一位的换算关系表,从 1 的计算结果中通过这个表直接得到这个数值。
我们来实际编码试一下。
def get_weights():
"""获取所有的权重值
这里的 i 是从右开始,数值从 1 到 18
"""
w = {}
for i in range(1, 19):
w[i] = 2 ** (i - 1) % 11
return w
def get_ai_map(i):
"""获取第 i 位的换算关系表
这里的 i 是从右开始,数值从 1 到 18
"""
w = get_weights()
wi = w[i]
ai_map = {}
for result in range(0, 11):
for ai in range(0, 11):
if (ai * wi + result) % 11 == 1:
ai_map[result] = 'X' if ai == 10 else str(ai)
break
return ai_map
def get_unkown_num(id_num):
w = get_weights()
i = 18 - id_num.index('?')
ai_map = get_ai_map(i)
result = 0
for index, n in enumerate(id_num):
if n == '?':
continue
wi = w[18-index]
num = int(n) if n != 'X' else 10
result += wi * num
result = result % 11
return ai_map[result]
if __name__ == '__main__':
# 411324201102310057
print(get_unkown_num('41132420110231005?'))
print(get_unkown_num('4113242?1102310057'))
print(get_unkown_num('411324201102?10057'))
可以看到程序结果:
7
0
3
完全符合预期。
题外话
文章里没有去深入验证算法的准确性。算了,有时候我应该不是一个较真的家伙。
在计算映射的部分后来想到了仿射加密法,总感觉两者的手法很相似。
最后的总结
从上面的说明之中,我们可以清楚知道身份证号每一个数值所代表的含义。另外有一些需要注意的地方:
- 身份证号的任何一位都可以通过其他的十七位进行计算得到。
- 对于一个自然人来说,世代居住在同一个地方的人,前 6 位几乎是固定的。而出生日期很多社交账号都能方便得到(不管是农历还是阳历)。所以一个人所未知的部分其实就只有顺序码那 3 个。因为最后一个可以计算得到。
所以,遮掉一位放出去的身份证号码就和皇帝的新装一样...
文章的编写目的仅在说明,如果侵犯了某人的隐私的话,请联系删除。