先把算法本身讲清楚

这套东西是公开的国家标准,身份证最后一位的算法在任何一本教材里都查得到。抛开归属,它值得单独拎出来,是因为它是一个非常干净的「加权和取模再查表」的范式。

规则是这样:身份证号前 17 位是本体,第 18 位是校验码。对前 17 位逐位乘一个固定的权重,权重从第 1 位到第 17 位依次是

7 9 10 5 8 4 2 1 6 3 7 8 9 10 5 8 4 2

十七个乘积加起来,对 11 取余,余数是 0 到 10 里的某个整数,再用余数去查一张 11 格的映射表:

余数:  0 1 2 3 4 5 6 7 8 9 10
校验:  1 0 X 9 8 7 6 5 4 3  2

查到的那个字符就是第 18 位。这张表里之所以有一个 X,是因为第 18 位要表示的候选值有 11 个,而 0 到 9 只有十个数字,多出来的那个用罗马数字 X 顶上,读作 10。这也是为什么一个合法的身份证号最后一位可能是字母——不是录入错误,是它撞上了余数 2。

这套加权和模查表的骨架,和银行卡号、ISBN、IMEI 的校验位是同一种思路,只是权重和模数不同。看懂一个,剩下的都是换数字。

它到底在校验,还是在生成

工程窗口标题写着「校验」,标签上写「请输入身份证号前17位」,看着像个校验器。但读代码就知道不是。

整个程序只有一个事件处理函数,挂在输入框的「内容被改变」上。逻辑是一句条件:当输入框里的字符数正好等于 17 时,按上面那套算出余数 a,然后——把查表得到的那一位拼接到 17 位的后面,整串显示到第二个框里。也就是 第二个框 = 前17位 + 算出来的第18位。

代码里没有「拿你给的 18 位、重算一遍、和第 18 位比、告诉你合不合」这条路径。它从头到尾没有比较过任何一个已经存在的第 18 位。输入框的最大长度被设成了 17,你连第 18 位都打不进去。所以它不校验,它补全:给任意 17 位,它都还你一个校验位合法的 18 位号码。

这一点很重要,后面要不要公开全看它。

不足 17 位时走另一个分支,只做两件很轻的事:先清空输出,再看第 11、12 位(出生日期里的月份)大于 12 没有、第 13、14 位(日)大于 31 没有,命中就弹一句「月份超出12月」「日期超出31日」。这是输入框位数还没到 17 时顺手做的粗筛,不碰地区码、不做真校验。

为什么「它会补全」这件事比听起来要紧,得从校验位本身的性质讲。第 18 位只有 11 种取值(0 到 9 加一个 X),而对任意一组前 17 位,加权和取模后落进哪一格是唯一确定的——也就是说,每一个 17 位前缀,都恰好对应一个能让校验通过的 18 位号码,没有例外。反过来说,「校验位合法」是个必要不充分条件:一个凭空捏造的号,只要最后这一位按规则算出来,就能骗过一切只看校验位的关卡。校验器做不到的是替你补这一位,生成器恰恰做的就是这件事。这段代码全程只做后者,所以它的真实身份,是一个把任意前缀补成合法号码的补全器。这个区别不看代码是分不清的——名字里写着「校验」,逻辑里全是「生成」。

边打字边算,和点提交再算,是两种东西

方法上值得一说的是触发时机。作者没有做「提交」按钮,没有等你按一个确定键。他把逻辑挂在「内容被改变」上——每敲一个字符、每删一个字符都跑一遍。输入框被限成只收数字,所以你每加一位,界面就实时反馈一次:位数没凑够时提示月份日期有没有越界,凑够 17 位的那一下,第 18 位当场蹦出来。

这两种时机给用户的感受差很多。提交式校验是「你填完,我判你错对」,反馈是一次性的、滞后的,用户得先做完动作再被驳回。逐键式反馈是「你一边填,它一边跟着你」,它把校验/补全变成输入过程的一部分,几乎零延迟。代价是每按一键都要重跑逻辑,所以这类实时处理必须足够便宜——这个算法就是十七次乘法加几次加法,随便怎么跑都不卡。把贵的活儿放提交时、把便宜且能安抚人的反馈放逐键,是这个老工程顺手示范的一个取舍。

容器为什么比源码大五十倍

工程文件 203372 字节,能读出来的源码文本才 4 KB 多一点,差了近五十倍。这种差距最容易让人脑补出一堆东西:说容器里塞了编码表、塞了字库、塞了资源区。别信脑补,扫一遍字节。

扫出来的事实是:整个文件里单个字节 0xF6 占了 48.8%,中段一大片区域每个 8K 块里这个字节的占比从 28% 到 97% 不等,那一段的熵只有 4.65 bit/字节——非常低。尾部还挨着一段约 8 KB 的全 0x00。工程既没有内嵌图片也没有内嵌音频(资源清单都是空的)。至于那组权重 7 9 10 5 8 4 2……,它们是直接以 × 7、× 9 的形态硬写在源码表达式里的,根本没有任何查找表存在,所以「GB2312/GBK 权重表」这个猜测两头都不成立。

低熵、单一重复字节、后面接零区,这就是定长分页容器的填充(padding):易语言的 .e 把工程切成固定大小的页来存,没用完的页空间和预留区拿一个默认字节填满,不承载任何信息。真正反编译出来的逻辑只占文件里很小一段。所以「容器 50 倍于源码」讲的是这个格式的存储方式,不是这个程序有多复杂。判一个二进制里的大块是数据还是填充,看字节熵和重复度比看总大小靠谱得多——高熵的密集块才可能是压缩数据或内嵌载荷,一个字节占了快一半的块只能是填充。

顺带一提,这类明文容器的信息是按偏移分区的,串落在哪一段,比串写了什么更能告诉你它是什么。这个 .e 开头不到 2 KB 的那一小段是可读的 GBK 明文元数据,窗口标题、作者备注、署名邮箱、控件名、依赖的支持库及其标识,全挤在这一区;再往后一大片才是那个单一字节铺满的低熵填充区,最尾部才轮到程序集声明。扫的时候先按偏移分带、再看每带的熵和重复度,就能把「作者写的说明」和「格式自带的填充」干净地分开——这跟装不装得回 IDE 没关系,光读字节就够了。