yanyue404 / blog

Just blog and not just blog.
https://yanyue404.github.io/blog/
Other
87 stars 13 forks source link

公民身份证号码的正则表达式要点 #271

Open yanyue404 opened 5 months ago

yanyue404 commented 5 months ago

身份证正则校验

公民身份号码是 18 位数特征组合码,由 17 位数字本体码和 1 位数字校验码组成。

第 1、2 位数字表示:所在省份的代码 第 3、4 位数字表示:所在城市的代码 第 5、6 位数字表示:所在区县的代码 第 7 位至 14 位为出生日期码,YYYYDDMM 出生年、月、日(其中 7、8、9、10 位是年,11、12 位是月,13、14 位是>日); 第 15 位至 17 位为顺序码,是县、区级政府所辖派出所的分配码, 同时第 17 位兼具性别标识功能,男单女双; 第 18 位为校验码,主要是为了校验计算机输入公民身份证号码的前 17 位数字是否正确,其取值范围是 0 至 10,当值等于 >10 时,用罗马数字符 X 表示。

校验省份

const checkProv = function (val) {
  var pattern = /^[1-9][0-9]/
  var provs = {
    11: '北京',
    12: '天津',
    13: '河北',
    14: '山西',
    15: '内蒙古',
    21: '辽宁',
    22: '吉林',
    23: '黑龙江 ',
    31: '上海',
    32: '江苏',
    33: '浙江',
    34: '安徽',
    35: '福建',
    36: '江西',
    37: '山东',
    41: '河南',
    42: '湖北 ',
    43: '湖南',
    44: '广东',
    45: '广西',
    46: '海南',
    50: '重庆',
    51: '四川',
    52: '贵州',
    53: '云南',
    54: '西藏 ',
    61: '陕西',
    62: '甘肃',
    63: '青海',
    64: '宁夏',
    65: '新疆',
    71: '台湾',
    81: '香港',
    82: '澳门'
  }
  if (pattern.test(val)) {
    if (provs[val]) {
      return true
    }
  }
  return false
}
//输出 true,37是山东
console.log(checkProv(37))
//输出 false,16不存在
console.log(checkProv(16))

校验年月日合理性

const checkDate = function (val) {
  var pattern = /^(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)$/
  if (pattern.test(val)) {
    var year = val.substring(0, 4)
    var month = val.substring(4, 6)
    var date = val.substring(6, 8)
    var date2 = new Date(year + '-' + month + '-' + date)
    if (date2 && date2.getMonth() == parseInt(month) - 1) {
      return true
    }
  }
  return false
}
//输出 true
console.log(checkDate('20240229'))
//输出 false 2月没有31日
console.log(checkDate('20240230'))

校验身份证是否合理(松散,主要校验最后一位校验码)

const checkCode = function (val) {
  var p =
    /^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/
  var factor = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
  var parity = [1, 0, 'X', 9, 8, 7, 6, 5, 4, 3, 2]
  var code = val.substring(17)
  console.log('输入的校验码 code:', code)
  if (p.test(val)) {
    var sum = 0
    // 根据身份证主体码(前17位)分别与对应的加权因子(factor)计算乘积再求和,根据所得结果与11取模得到X值。
    for (var i = 0; i < 17; i++) {
      sum += val[i] * factor[i]
    }

    console.log('正确的校验码 code:', parity[sum % 11])
    // 根据 X 值查询 parity,得出 a18 即校验码值。
    if (parity[sum % 11] == code.toUpperCase()) {
      return true
    }
  }
  return false
}

// 输出 true, 校验码相符
console.log(checkCode('11010519491231002X'))
// 输出 false, 校验码不符
console.log(checkCode('110105194912310021'))
console.log(checkCode('110110196451545457'))

校验外国人永久居留身份证

const validateForeignerIdCard = (prid) => {
  // 判断是否是字符串
  if (typeof prid !== 'string') {
    return false
  }

  // 移除字符串中的空格
  prid = prid.replace(/\s/g, '')

  // 之后判断是否是15位或者18位
  if (prid.length !== 15 && prid.length !== 18) {
    return false
  }

  // 根据不同长度进行校验
  if (prid.length === 15) {
    // 1. 证件号前三位为英文,不区分大小写
    // 2. 第4-13位为纯数字;
    // 3. 第8-9位为出生年份后两位;
    // 4. 第10-11位为出生月份,介于01至12之间;
    // 5. 第12-13位为出生日期,介于01至31之间。
    // TODO: 缺少月份和对应天数的比较,待完善
    if (
      !/^[a-zA-Z]{3}\d{6}(?:0[1-9]|1[0-2])(?:0[1-9]|[21]\d|3[10])\d{2}$/.test(
        prid
      )
    ) {
      return false
    }
  } else if (prid.length === 18) {
    // 1.证件号码为18位,前17位必须为数字,最后一位可以为 数字或大写“X”或小写“x”,若为小写“x”,则系统存储时自动转为对应的大写“X”;
    if (!/^[0-9]{17}[0-9Xx]$/.test(prid)) {
      return false
    }
    // 2.校验首位为9;
    if (prid[0] !== '9') {
      return false
    }
    // 3.号码中第2-3位为省份代码,包含在下列代码之中北京市-11,天津市-12,河北省-13,山西省-14,内蒙古自治区-15,辽宁省-21,吉林省-22,黑龙江省-23,上海市-31,江苏省-32,浙江省-33,安徽省-34,福建省-35,江西省-36,山东省-37,河南省-41,湖北省-42,湖南省-43,广东省-44,广西自治区-45,海南省-46,重庆市-50,四川省-51,贵州省-52,云南省-53,西藏自治区-54,陕西省-61,甘肃省-62,青海省-63,宁夏自治区-64,新疆自治区-65;
    const provinceCode = prid.substring(1, 3)
    const validProvinceCodes = [
      '11',
      '12',
      '13',
      '14',
      '15',
      '21',
      '22',
      '23',
      '31',
      '32',
      '33',
      '34',
      '35',
      '36',
      '37',
      '41',
      '42',
      '43',
      '44',
      '45',
      '46',
      '50',
      '51',
      '52',
      '53',
      '54',
      '61',
      '62',
      '63',
      '64',
      '65'
    ]
    if (!validProvinceCodes.includes(provinceCode)) {
      return false
    }
    // 4.号码中第7-10位为出生年,应大于1850;
    const birthYear = parseInt(prid.substring(6, 10), 10)
    if (birthYear <= 1850) {
      return false
    }
    // 5.号码中第11-12位为出生月,应大于0小于13。
    const birthMonth = parseInt(prid.substring(10, 12), 10)
    if (birthMonth < 1 || birthMonth >= 13) {
      return false
    }
    // 6.号码中第13-14位为出生日,应大于0小于32。
    const birthDay = parseInt(prid.substring(12, 14), 10)
    if (birthDay < 1 || birthDay >= 32) {
      return false
    }

    // 7:根据月份判断天数是否合法
    const daysInMonth = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] // 每月对应的天数(2月非闰年)
    if (
      birthMonth === 2 &&
      ((birthYear % 4 === 0 && birthYear % 100 !== 0) || birthYear % 400 === 0)
    ) {
      daysInMonth[2] = 29 // 闰年2月天数为29天
    }
    if (birthDay > daysInMonth[birthMonth]) {
      return false
    }
    // 8. 第18位采用 MOD11-2 算法设置为校验码,与公民身份号码校验方式相同,即“校验位”与各个位置上的号码字符值(最后一位“X”的值同数字10)应满足下列公式的校验:
    const verifyCode = prid.slice(17).toUpperCase()
    const charArray = prid.slice(0, 17).split('')
    const weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
    const checkCodes = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2']
    let sum = 0
    for (let i = 0; i < charArray.length; i++) {
      sum += parseInt(charArray[i]) * weights[i]
    }
    const modResult = sum % 11
    const calculatedCode = checkCodes[modResult]
    if (calculatedCode !== verifyCode) {
      return false
    }
  }
  return true
}

// 校验外国人永久居留身份证
alert(validateForeignerIdCard('911124198108030024')) // true
alert(validateForeignerIdCard('932682198501010017')) // true
alert(validateForeignerIdCard('911398199012310021')) // true

算算身份证号重复的概率

如果要两个人身份证号相同,首先要保证两个人在同一个地区保证身份证号前六位相同,接下来需要有相同的生日,保证中间八位相同 ,还剩下最后四位,而最后一位校验码的出现是取决于前 17 位的,所以只有第十五到十七位三个数字是可变化的,那能承载多少人呢?

第十五位和十六位为随机数,均可出现 10 中可能,第十七位由于性别奇偶之分,只有 5 中可能,所以可得出下面的算式。

男性:10 10 5 = 550

女性:10 10 5 = 550

我是男性,从上面可以看出来,如果在我所出生的区(身份证前六位精确到区县)并且在我出生的那一天有 501 个男孩出生,如果保证身份证位数恒定 18 位不增加且只允许数字情况下,肯定有两个人要撞身份证号,所以与我撞身份证号的可能性即为五百分之一。

现实数据统计

我查了一些资料,中国 2018 年全年出生人口 1523 万人,2017 年出生人口约为 1723 万,2016 年约为 1786 万,这里就当做每年出生人口为 1700 万,中国一共有超过 2800 多个县区,那我们来算一算大概一个区县每天平均有多少人出生呢?

17000000/365/2800 = 16.63

平均每天每个区县有 17 个新生儿,有一些人口密集的地方超平均数 10 倍或者 20 倍,也可看出来也离 500 个差距甚远,所以可得出结论基本没有可能两个人身份证号会相同,如果人口真到非常庞大的时候 ,身份证号也会相应增加位数或者引入英文字母来规避号码相同的问题。

中心化机构

这里从上面身份证号关于重复的计算可以看出,先有了户籍管理部门来制定好一些规则,然后地方的实施者(派出所)再按照规则去生成每个人的身份证号,在这里抛出了最关键的一个点,中心化的户籍部门。

自动生成指定规则的身份证号码

function dateFormat(fmt, date) {
  date = date || new Date()
  var o = {
    'M+': date.getMonth() + 1, //月份
    'd+': date.getDate(), //日
    'h+': date.getHours(), //小时
    'm+': date.getMinutes(), //分
    's+': date.getSeconds(), //秒
    'q+': Math.floor((date.getMonth() + 3) / 3), //季度
    S: date.getMilliseconds() //毫秒
  }
  if (/([yY]+)/.test(fmt)) {
    fmt = fmt.replace(
      RegExp.$1,
      (date.getFullYear() + '').substr(4 - RegExp.$1.length)
    )
  }
  for (var k in o) {
    if (new RegExp('(' + k + ')').test(fmt)) {
      fmt = fmt.replace(
        RegExp.$1,
        RegExp.$1.length == 1 ? o[k] : ('00' + o[k]).substr(('' + o[k]).length)
      )
    }
  }
  return fmt
}

function getIDCard(code, date, sex) {
  date = date || new Date()
  let random = Math.random().toString().substr(2, 2) // 生成 2 位顺序码
  let sexPos = parseInt(Math.random() * 4) * 2 + (sex == '男' ? 1 : 0)
  let id = code.toString() + dateFormat('yyyyMMdd', date) + random + sexPos

  const factor = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
  const parity = [1, 0, 'X', 9, 8, 7, 6, 5, 4, 3, 2]
  let S = 0
  for (let i = 0; i < id.length; i++) {
    S += id[i] * factor[i]
  }
  id += '10X98765432'.substr(S % 11, 1)
  return id
}

let id = getIDCard('110114', new Date('1994-01-01'), '男')
console.log('生成的身份证号:', id) // 生成的身份证号: 110114199401010171

参考