背景
在一些情况下,系统获得的身份证号码是无效的。
例如在收集阶段的身份证填写中,系统未做严格的合法性验证,用户输错或随意编造了虚假号码。
这些错误或者虚假的身份证号码属于脏数据,在做分析或建模时要剔除,在此之前先判断身份证是否合法。
目标
对于输入的中国大陆身份证号码,包括第一代和第二代居民身份证,返回是否合法,也就是否有效的逻辑判断。
业务逻辑
18位第二代居民身份证
18位身份证号码分别代表的含义:
- 第 1-2 位:省级行政区代码
- 第 3-4 位:地级行政区划分代码
- 第 5-6 位:县区行政区分代码
- 第 7-14位:出生年月日
- 第15-17位:顺序码,同一地区同年、同月、同日出生人的编号,奇数是男性,偶数是女性
- 第 18 位:校验码,其结果是由前17位数字按照特定的规则计算余数所得。如果余数是0-9则用位置对照后的0-9数字表示,如果是10则用X表示(X是大写的罗马数字10)
示例130503196704010016
的含义:
13
为河北省05
为邢台市03
为桥西区19670401
为1967
年4
月1
日出生001
为顺序码6
为校验码
15位第一代居民身份证
15位身份证号码分别代表的含义:
- 第 1-2 位:省级行政区代码
- 第 3-4 位:地级行政区划分代码
- 第 5-6 位:县区行政区分代码
- 第 7-12位:出生年月日,年份不包含“19”开头,比如出生年月日“670401”代表“1967年4月1日”
- 第13-15位:顺序码,同一地区同年、同月、同日出生人的编号,奇数是男性,偶数是女性
示例130503670401001
的含义:
13
为河北省05
为邢台市03
为桥西区670401
为67
年4
月1
日出生(年份67
代表的是1967
年)001
为顺序码
15位与18位号码有两个区别:一是年份,15位少了世纪年份19
;而是校验码,15位没有校验码。
如需将15位号码升级为18位,可做如下处理:
- 在第7位插入“19”,总长度变为17位
- 按照18位校验码的规则补充1位校验码放到最后,最终升级为18位身份证号码
二代身份证第18位校验码的计算模型
第二代居民身份证最后一位(第18位)校验码,其结果是用前17位数字计算所得,计算模型如下图所示:
上图中红色箭头和绿色箭头两种计算方案等价。
这里选取绿色箭头计算方案,归纳步骤如下:
- 将身份证号码的前 17 个字符拆分为 17 个独立数字;记 i 为身份证每个数字的位置序号,从 1 到 17 ;记 \(A_i\) 为身份证号码第 i 个位置对应的数字
- 计算 17 个数字对应的 2 的 n 次方的值,n 从 17 到 1 ,记为权重系数 \(W_i\)
- 计算 \(A_i * W_i\) 的积:17 个数字对应权重系数相乘后求和
- 除以 11 求余数
- 余数的结果,用
余数列表
与校验码对照表
映射,获得对应的校验码
R语言实现
针对身份证号码的的这些特点,可以用多个逻辑分别判断,且顺序应是简单在前、复杂在后:
- 号码长度只能是15位或18位
- 地区代码和出生年月日有一定规则和范围限制,可以用正则表达式判断
- 校验码是通过数字计算的,作为最后的校验规则
基本实现流程如下:
- 首先对单个身份证号码做逻辑校验(长度为1的向量)
- 然后将其函数化
- 最后将其向量化
单个向量逻辑实现
预先准备几个身份证号码,可参考下面这四个示例:
# 示例身份证号码
x1 <- c("45102519810711316X") # 18位,合法
x2 <- c("130503670401001") # 15位,合法
x3 <- c("45102519810711316") # 17位,将 x1 末位去掉,非法
x4 <- c("451025198107113169") # 18位,将 x1 末位替换,非法
使用几种不同类型的示例,用来验证代码逻辑及其返回值。
第一种判断:号码长度是否为15位或18位
字符向量长度是否为15位或18位。
nchar(x1) %in% c(18,15)
## [1] TRUE
nchar(x2) %in% c(18,15)
## [1] TRUE
nchar(x3) %in% c(18,15)
## [1] FALSE
nchar(x4) %in% c(18,15)
## [1] TRUE
备注:
nchar()
返回的是字符向量中每个元素的字符长度
length()
返回的是向量的长度,即向量中元素的个数
第二种判断:正则表达式验证
身份证号码正则表达式校验规则如下:
# 15/18位身份证号码验证的正则表达式总结
#
# xxxxxx yyyy MM dd 375 0 十八位
#
# xxxxxx yy MM dd 375 十五位
#
#
# 地区:[1-9]\d{5}
# 年的前两位:(18|19|([23]\d)) 1800-3999
# 年的后两位:\d{2}
# 月份:((0[1-9])|(10|11|12))
# 天数:(([0-2][1-9])|10|20|30|31) 这里不能判断是否闰年
#
# 三位顺序码:\d{3}
#
# 校验码:[0-9Xx]
正则表达式为:
- 十八位:
^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$
- 十五位:
^[1-9]\d{5}\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}$
备注:在通用的正则表达式中,单个反斜杠\
是转义字符;在R语言中,正则表达式的转义中需要改为两个反斜杠\\
。
字符处理通常使用 stringr
包,判断字符是否某个规则(模式)的函数是 str_detect()
。
library(stringr)
# 长度为18位时判断
str_detect(x1, "^[1-9]\\d{5}(18|19|([23]\\d))\\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$")
## [1] TRUE
# 长度为15位时判断
str_detect(x2, "^[1-9]\\d{5}\\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\\d{3}$")
## [1] TRUE
# 长度不在15和18时不用判断
# 在第一步中已经直接返回FASLE
# 长度为18位时判断
str_detect(x4, "^[1-9]\\d{5}(18|19|([23]\\d))\\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$")
## [1] TRUE
上面的身份证示例都是符合所验证的正则表达式的,下面用一些不符合规则的示例:
# 正则表达式,可以先定义为一个模式
p18 <- "^[1-9]\\d{5}(18|19|([23]\\d))\\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$"
p15 <- "^[1-9]\\d{5}\\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\\d{3}$"
x11 <- c("45102516810711316X") # 18位,把1981年改为1681年
x12 <- c("X30503670401001") # 15位,15位中出现非数字
x13 <- c("45102519811329316X") # 18位,出生月份为13月
str_detect(x11, p18)
## [1] FALSE
str_detect(x12, p15)
## [1] FALSE
str_detect(x13, p18)
## [1] FALSE
第三种判断:校验码规则
校验码规则只对18位身份证号码有效,是通过前17位数字做数据逻辑计算后求11的余数与对照表比较。
# 示例号码
x <- c("45102519810711316X") # 先校验一个结尾为X的
# x <- c("350426198709188963") # 再校验一个纯数字的
#
# x <- c("451025198107113169") # 18位非法
# 首先将身份证号码拆分成18个字符向量
id_str <- str_sub(x, 1:18, 1:18) # 返回的是字符型
# 前17位是数字转为数值型,用来做数学运算
# 备注:如果有非数字不会通过前面的正则表达式判断
id17 <- as.numeric(id_str[-18])
# 第18位:数字或者是X
# 备注:统一转为大写,为了兼容错误的小写字母x
id_18 <- str_to_upper(id_str[18])
# 前17个数字的权重系数:2的幂
w <- 2^(17:1)
# 前17个数字的权重和的余数
remainder <- sum(id17 * w) %% 11
# 余数列表
# 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
# 校验码对照表
# 1, 0, X, 9, 8, 7, 6, 5, 4, 3, 2
# 下面校验码对照表包含 X 字符,故而整个向量都会转化为字符类型
check_code <- c(1, 0, "X", 9, 8, 7, 6, 5, 4, 3, 2)
# 用计算余数后的结果,与对照表对应位置的校验码比较是否相等
# 余数的值+1,正好对应着校验码的元素位置
id_18 == check_code[remainder+1] # 两个字符比较是否相等,返回 TRUE 或者 FALSE
## [1] TRUE
# 可以分别用不同的示例号码来验证
# 该代码段被注释的第2和第3行是另外两个示例号码
逻辑函数化
在针对单个身份证号码实现代码逻辑后,将判断逻辑转化为函数,便于应用。
定义函数
idno_verify <- function(x){
############第一种判断:号码长度是否为15位或18位###################
# 长度不等于 15 或 18 即不合法,返回 FALSE
if (!nchar(x) %in% c(18,15)) return(FALSE)
############第二种判断:正则表达式验证#############################
# 长度等于 15 时只用正则表达式校验
if (nchar(x) == 15) return(stringr::str_detect(x, "^[1-9]\\d{5}\\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\\d{3}$"))
# 长度等于 18 时,先用正则表达式校验,不通过则返回 FALSE
# 这里即使通过也不能直接返回 TRUE, 还需要进行校验码判断
if (nchar(x) == 18 && !(stringr::str_detect(x, "^[1-9]\\d{5}(18|19|([23]\\d))\\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$"))) return(FALSE)
############第三种判断:校验码规则###################
id_str <- stringr::str_sub(x, 1:18, 1:18)
id17 <- as.numeric(id_str[-18])
id_18 <- stringr::str_to_upper(id_str[18])
w <- 2^(17:1)
remainder <- sum(id17 * w) %% 11
check_code <- c(1, 0, "X", 9, 8, 7, 6, 5, 4, 3, 2)
return(id_18 == check_code[remainder+1])
}
备注:
- 函数体执行到
return()
时直接返回结果,不再执行后续代码 包::函数
这样的用法,可以在不加载整个函数包的情况下直接使用包内的函数
应用函数
在单个身份证号码的向量上应用函数:
idno_verify(x1)
## [1] TRUE
idno_verify("511702197404273000")
## [1] FALSE
直接将该函数应用在多个身份证号码的向量(长度大于1)时会报错:
idno_verify(c("45102519810711316X", "511702197404273000"))
# 返回如下错误
## the condition has length > 1 and only the first element will be usedthe condition has length > 1 and only the first element will be used[1] FALSE
因为在函数定义过程中,针对的是单个元素的向量,中间用来对比校验码的逻辑中,将单个元素拆分成了18个子元素,该拆分过程针对多个元素的向量会报错。
上面的函数针对单个元素的向量是可用的,故而可以将该函数应用到待判断对象向量的每个元素上,使用 sapply()
即可实现。
sapply(c("45102519810711316X", "511702197404273000"), idno_verify, USE.NAMES = FALSE)
## [1] TRUE FALSE
# USE.NAMES = FALSE 不要返回名称
应用在包含多个身份证号码的长向量上:
#### 测试数据:长度大于1的向量(多个身份证号码) ####
idnumber <- c("511702197404273000",
"522635197801141000",
"53262819820314783X",
"532628197907137000",
"532628197407119000",
"411525198609255000",
"120000197105236000",
"12000019820110541X",
"120000197207182000",
"120000197101106000",
"522635198708184000",
"52263519830114890X",
"522635198001259000",
"451025198607117000",
"451025197308146000",
"45102519810711316X",
"451025197501107000",
"451025198807243000",
"451025197108189000",
"451025198909222000",
"451025198004124000",
"532628198206109000",
"532628197903253000",
"53262819720718326X",
"532628198605112000",
"532628198109243000",
"411525198407154000",
"130503670401001",
"X30503670401001"
)
# 将函数 idno_verify 应用到 idnumber 每个元素上
# 返回等长的逻辑向量
sapply(idnumber, idno_verify, USE.NAMES = FALSE)
## [1] FALSE FALSE TRUE FALSE TRUE FALSE FALSE TRUE FALSE FALSE FALSE TRUE
## [13] FALSE FALSE TRUE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE TRUE
## [25] FALSE FALSE TRUE TRUE FALSE
在数据框上应用函数
library("dplyr")
data.frame(idnumber = idnumber, stringsAsFactors = FALSE) %>% # 转为数据框,不要将字符自动转为因子
mutate(verify_result = sapply(idnumber, idno_verify, USE.NAMES = FALSE)) %>% # 新增一列作为验证结果
filter(verify_result) # 筛选通过验证的记录
## idnumber verify_result
## 1 53262819820314783X TRUE
## 2 532628197407119000 TRUE
## 3 12000019820110541X TRUE
## 4 52263519830114890X TRUE
## 5 451025197308146000 TRUE
## 6 45102519810711316X TRUE
## 7 53262819720718326X TRUE
## 8 411525198407154000 TRUE
## 9 130503670401001 TRUE
函数向量化
上面编写的函数,不能直接应用在长度大于1的向量上,本质原因是该函数并不是向量化的。
将针对单个元素的向量化,只需用 sapply()
将原函数体套在 FUN
参数(函数)中即可。
这里函数过程比较简单,无需单独再给内部函数命名。
idno_verify <- function(y){
sapply(y, function(x){
############第一种判断:号码长度是否为15位或18位###################
# 长度不等于 15 或 18 即不合法,返回 FALSE
if (!nchar(x) %in% c(18,15)) return(FALSE)
############第二种判断:正则表达式验证#############################
# 长度等于 15 时只用正则表达式校验
if (nchar(x) == 15) return(stringr::str_detect(x, "^[1-9]\\d{5}\\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\\d{3}$"))
# 长度等于 18 时,先用正则表达式校验,不通过则返回 FALSE
if (nchar(x) == 18 && !(stringr::str_detect(x, "^[1-9]\\d{5}(18|19|([23]\\d))\\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$"))) return(FALSE)
############第三种判断:校验码规则###################
id_str <- stringr::str_sub(x, 1:18, 1:18)
id17 <- as.numeric(id_str[-18])
id_18 <- stringr::str_to_upper(id_str[18])
w <- 2^(17:1)
remainder <- sum(id17 * w) %% 11
check_code <- c(1, 0, "X", 9, 8, 7, 6, 5, 4, 3, 2)
return(id_18 == check_code[remainder+1])
},
USE.NAMES = FALSE
)
}
上面的函数已经向量化,可直接运用该函数:
idno_verify("130503196704010016")
## [1] TRUE
idno_verify("130503670401001")
## [1] TRUE
idno_verify(x1)
## [1] TRUE
idno_verify(x2)
## [1] TRUE
idno_verify(x3)
## [1] FALSE
idno_verify(x4)
## [1] FALSE
idno_verify(c(x11,x12,x13))
## [1] FALSE FALSE FALSE
idno_verify(idnumber)
## [1] FALSE FALSE TRUE FALSE TRUE FALSE FALSE TRUE FALSE FALSE FALSE TRUE
## [13] FALSE FALSE TRUE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE TRUE
## [25] FALSE FALSE TRUE TRUE FALSE
library("dplyr")
data.frame(idnumber = idnumber, stringsAsFactors = FALSE) %>% # 转为数据框,不要将字符自动转为因子
mutate(verify_result = idno_verify(idnumber)) %>% # 新增一列作为验证结果
filter(verify_result) # 筛选通过验证的记录
## idnumber verify_result
## 1 53262819820314783X TRUE
## 2 532628197407119000 TRUE
## 3 12000019820110541X TRUE
## 4 52263519830114890X TRUE
## 5 451025197308146000 TRUE
## 6 45102519810711316X TRUE
## 7 53262819720718326X TRUE
## 8 411525198407154000 TRUE
## 9 130503670401001 TRUE
需要注意的是,即使以上的身份证号码校验通过,也并不一定说明这些身份证号码就是合法的。
可以按照以上规则虚构出通过校验的号码,例如130503196704010012
,这个号码就就是虚构出来的。
身份证号码是否真实存在,终极的验证方法是与公安系统联网,由其返回验证结果。
总结
该案例R语言实现的基本流程:
- 首先对单个身份证号码做逻辑校验(长度为1的向量)
- 然后将其函数化
- 最后将其向量化
该案例主要用到的包与函数:
stringr
包主要用来做字符处理,用str_sub()
提取,用str_detect()
判断(检测)
该案例使用函数的最佳实践:
- 函数体中分支逻辑后可用
return()
直接返回 包::函数
的用法,可不直接加载整个包,只使用其中的某个函数- 针对单个元素的函数过程,通过
sapply()
即可转为向量化函数