5 min read

R语言实例:校验身份证号码是否合法

背景

在一些情况下,系统获得的身份证号码是无效的。

例如在收集阶段的身份证填写中,系统未做严格的合法性验证,用户输错或随意编造了虚假号码。

这些错误或者虚假的身份证号码属于脏数据,在做分析或建模时要剔除,在此之前先判断身份证是否合法。

目标

对于输入的中国大陆身份证号码,包括第一代和第二代居民身份证,返回是否合法,也就是否有效的逻辑判断。

业务逻辑

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为桥西区
  • 19670401196741日出生
  • 001为顺序码
  • 6为校验码

15位第一代居民身份证

15位身份证号码分别代表的含义:

  • 第 1-2 位:省级行政区代码
  • 第 3-4 位:地级行政区划分代码
  • 第 5-6 位:县区行政区分代码
  • 第 7-12位:出生年月日,年份不包含“19”开头,比如出生年月日“670401”代表“1967年4月1日”
  • 第13-15位:顺序码,同一地区同年、同月、同日出生人的编号,奇数是男性,偶数是女性

示例130503670401001的含义:

  • 13为河北省
  • 05为邢台市
  • 03为桥西区
  • 6704016741日出生(年份67代表的是1967年)
  • 001为顺序码

15位与18位号码有两个区别:一是年份,15位少了世纪年份19;而是校验码,15位没有校验码。

如需将15位号码升级为18位,可做如下处理:

  • 在第7位插入“19”,总长度变为17位
  • 按照18位校验码的规则补充1位校验码放到最后,最终升级为18位身份证号码

二代身份证第18位校验码的计算模型

第二代居民身份证最后一位(第18位)校验码,其结果是用前17位数字计算所得,计算模型如下图所示:



上图中红色箭头和绿色箭头两种计算方案等价。

这里选取绿色箭头计算方案,归纳步骤如下:

  1. 将身份证号码的前 17 个字符拆分为 17 个独立数字;记 i 为身份证每个数字的位置序号,从 1 到 17 ;记 \(A_i\) 为身份证号码第 i 个位置对应的数字
  2. 计算 17 个数字对应的 2 的 n 次方的值,n 从 17 到 1 ,记为权重系数 \(W_i\)
  3. 计算 \(A_i * W_i\) 的积:17 个数字对应权重系数相乘后求和
  4. 除以 11 求余数
  5. 余数的结果,用余数列表校验码对照表映射,获得对应的校验码

R语言实现

针对身份证号码的的这些特点,可以用多个逻辑分别判断,且顺序应是简单在前、复杂在后:

  1. 号码长度只能是15位或18位
  2. 地区代码和出生年月日有一定规则和范围限制,可以用正则表达式判断
  3. 校验码是通过数字计算的,作为最后的校验规则

基本实现流程如下:

  1. 首先对单个身份证号码做逻辑校验(长度为1的向量)
  2. 然后将其函数化
  3. 最后将其向量化

单个向量逻辑实现

预先准备几个身份证号码,可参考下面这四个示例:

# 示例身份证号码
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])
}

备注:

  1. 函数体执行到 return() 时直接返回结果,不再执行后续代码
  2. 包::函数 这样的用法,可以在不加载整个函数包的情况下直接使用包内的函数

应用函数

在单个身份证号码的向量上应用函数:

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. 首先对单个身份证号码做逻辑校验(长度为1的向量)
  2. 然后将其函数化
  3. 最后将其向量化

该案例主要用到的包与函数:

  • stringr 包主要用来做字符处理,用 str_sub() 提取,用 str_detect() 判断(检测)

该案例使用函数的最佳实践:

  • 函数体中分支逻辑后可用 return() 直接返回
  • 包::函数的用法,可不直接加载整个包,只使用其中的某个函数
  • 针对单个元素的函数过程,通过 sapply() 即可转为向量化函数