资讯详情

资讯详情

Python身份证号信息提取:从编码规则到实战应用

1. 从一串数字里读出一个人身份证号这个东西几乎每个人都有但大多数人不知道它其实是一套信息编码。我经常在表单自动填充、用户数据清洗、年龄统计这类需求里遇到它——前台填个身份证号后台就自动带出出生日期、性别、年龄甚至能预判发证地。这个场景很常见用Python处理起来也快。这个项目的核心任务很明确给一个18位身份证号用程序提取出其中包含的出生日期、性别、年龄、发证地并完成基础的号码合法性校验。它能解决的问题也很具体不用手动录入生日和性别减少前台重复输入批量清洗历史用户数据时能从一堆身份证号里快速归类统计做用户画像时也能直接算出年龄段分布。这适合两类读者一类是刚学Python、想做点实战小项目的学生这类题目能串起字符串处理、正则表达式、日期计算、异常处理等常用知识点另一类是工作中经常处理用户数据的运营、测试或后端开发者看完可以直接把代码拿过去改改就用。我会把思路、算法、代码、坑点都讲清楚你可以照着写也可以当模板抄。2. 18位数字的编码规则在写代码之前得先把身份证号本身的结构吃透。这东西不是一串随机的数字而是国家标准的编码结构每一位都有含义。一位标准的18位身份证号从左到右可以拆成四段前6位地址码表示首次申领身份证时户籍所在地的行政区划代码。前两位是省自治区、直辖市中间两位是市地区后两位是区县。举个例子110105开头就是北京市朝阳区。第7到14位出生日期码格式是YYYYMMDD比如19900315就是1990年3月15日出生。第15到17位顺序码表示同一地址码所标识的区域范围内对同年同月同日出生的人编订的顺序号。其中第17位比较特殊——奇数分配给男性偶数分配给女性。所以看到第17位是3就知道是男性是2就知道是女性。第18位校验码用来验证整个号码是否正确。它的取值范围是0-9和XX代表罗马数字10。这个X经常被人忽略甚至有些人录入数据时会把X当成英文字母随便填但在校验逻辑里X就是10是校验算法的正常产物不是异常值。校验码的计算规则我每次都要翻一遍因为容易忘将身份证号前17位数字分别乘以对应权重权重序列是固定的7、9、10、5、8、4、2、1、6、3、7、9、10、5、8、4、2。把乘积相加得到总和。将总和除以11取余数。余数映射到对应的校验码。映射表是[1, 0, X, 9, 8, 7, 6, 5, 4, 3, 2]也就是说余数是0对应校验码1余数是1对应0余数是2对应X以此类推。校验算法的本质是模11运算。11是质数权重序列又经过精心设计可以让任意一位数字出错时校验码都能大概率对不上从而完成错误检测。这就是为什么有些系统能识别出填错的身份证号——不是它聪明是校验码在把关。了解完规则你就能马上意识到提取信息这件事本质上就是字符串切片加条件判断。但要把这件事做好还得考虑到非法输入、旧版15位号码、批量处理等真实场景。3. 核心设计与工具选型整体方案上我用的是一个“校验先行、提取在后”的设计先验证号码合法性再根据位规则切片提取信息。为什么不直接切片因为在真实业务里脏数据太多了。漏了一位、填了个X、把出生年月写错位这样的数据直接切片会得到荒谬的结果——比如生日是1923年47月性别显示未知。先校验能把这些脏数据挡在门外后续处理才靠谱。在技术选型上我把功能拆成几个独立函数每个函数只干一件事。这样的好处是便于测试和复用校验函数可以单独跑日期提取函数也能独立验证后期加功能不用改旧代码。工具方面用到的都是Python标准库不需要安装第三方包。核心是re模块做正则匹配datetime做日期解析和年龄计算。为什么不用pandas如果你的数据已经是DataFrame格式用pandas做列向量化操作确实更高效但作为通用方案纯Python函数更灵活可以直接嵌入脚本、Web接口或者定时任务里。还有一个设计细节年龄的计算。很多人初学时直接用当前年份减去出生年份这其实是有问题的。比如今天是2025年3月一个人是2000年12月生的按减法算是25岁但实际还没过生日应该是24岁。正确做法是先算年份差再看当前日期是否已过今年的生日没过就减一。这个逻辑虽然简单但确实是我见过最容易出错的地方。4. 核心细节与原理解读这一节把每个关键步骤拆开讲包括踩坑点和背后逻辑。4.1 用正则做格式预检正则的作用是快速筛掉格式明显不对的输入。18位身份证号的合法形式是前17位是数字最后一位是数字或大写X。对应正则表达式是^\d{17}[\dX]$这里有几个细节需要解释^和$是行首行尾锚定确保整串完全匹配而不是找出子串。如果少了它们16位数字加一个X的字符串也能从中截出合法片段这是错误的。\d{17}表示恰好17位数字而不是至少17位。多一位少一位都不行。[\dX]表示最后一位是数字或者大写X。这里只认大写X因为标准里校验码就是大写。如果你要兼容小写x可以在正则里写成[\dXx]但后续判断时要统一转成大写。正则是一次快速淘汰真正严格的校验还是得靠校验码算法。两者的职责不同正则管形状算法管真实性。注意正则预检不能替代校验码。形状对了不代表号码真实只有校验码通过才算格式上合法。我在实际项目里见过很多前17位格式标准但校验码不对的号码正则根本发现不了只有算法能拦下来。4.2 校验码算法从权重到映射校验码算法是整个程序里最有技术含量的一块。实现起来很简单但理解它需要一点数学直觉。为什么权重序列是固定的、为什么模11、为什么映射表长那样这整套设计属于ISO 7064:1983标准里的MOD 11-2算法。权重的选择保证了一个关键性质任意一位数字变化校验结果几乎必然变化。这在纠错领域叫“检错能力”是这套编码能作为身份证校验依据的原因。计算过程用Python写很清晰我直接给出完整代码# 校验码权重序列注意次序不能错 WEIGHTS [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2] # 余数到校验码的映射表索引就是余数 CHECK_CODES [1, 0, X, 9, 8, 7, 6, 5, 4, 3, 2] def calc_check_digit(id_number: str) - str: 传入前17位返回第18位校验码 total sum(int(char) * weight for char, weight in zip(id_number, WEIGHTS)) remainder total % 11 return CHECK_CODES[remainder] def is_valid_id(id_number: str) - bool: 校验整个身份证号是否合法 if not re.match(r^\d{17}[\dX]$, id_number): return False actual id_number[-1] expected calc_check_digit(id_number[:17]) return actual expected这段代码有几个可以复用的点。calc_check_digit和is_valid_id分离意味着你可以单独调用calc_check_digit来生成一个合法的测试号码这在开发阶段特别有用——我经常批量生成测试数据来验证其他函数。实际执行calc_check_digit(11010519900315 前三位数字任意填)拿结果去对照映射表你会发现校验码往往是字母X的几率为1/11。这不是随机的是模11运算的分布结果。4.3 出生日期的提取与合法性判断出生日期是第7到第14位提取就是切片但提取之后必须做合法性验证。因为存在一些看似数字、实际不是合法日期的组合比如19990230这种——2月没有30号。字符串切片是机械行为它不会帮你判断日期是否真实存在。检验日期合法性的标准手段是利用datetime模块让Python自己去解析并捕获异常def extract_birthday(id_number: str) - str | None: 从身份证号中提取出生日期返回字符串格式YYYY-MM-DD birth_str id_number[6:14] try: birth_date datetime.strptime(birth_str, %Y%m%d) return birth_date.strftime(%Y-%m-%d) except ValueError: return Nonestrptime用格式符%Y%m%d去解析8位数字解析失败会抛ValueError说明日期本身就不合法。这个方式比我手动判断月份天数要省事得多也可靠得多因为闰年规则能被4整除但不能被100整除或者能被400整除已经内含在datetime的实现里。提示有人会问出生日期是不是应该晚于某个年份才合理比如小于1900年就视为异常。从纯算法角度讲日期能解析成功就够了但从业务角度你可以再加一个年龄上下限的判断。比如一个身份证号解析出出生年份是1880年日期本身合法但该人员显然不可能是活着的用户。这属于业务规则我建议放在函数外层做保持核心函数纯粹。4.4 性别判断第17位的奇偶性性别信息在第17位。奇数是男性偶数是女性。这里要注意一点顺序码里的奇偶分配是针对第17位不是整个顺序码。第15、16位没有这个含义。我用位运算来判断奇偶性比取模运算更直观一点def extract_gender(id_number: str) - str | None: 根据第17位判断性别奇数为男偶数为女 digit int(id_number[16]) return 男 if digit 1 else 女这里的逻辑等价于digit % 2 1但位运算的执行效率更高代码也更短。对于初学者来说两种写法都行我写位运算是为了展示一种更Pythonic的技巧。dDigit 1的结果如果是1说明二进制最低位是1那就是奇数结果是0就是偶数。4.5 年龄计算不能简单用年份相减年龄是提取信息里最容易出错的一个。直接当前年份减去出生年份得到的是虚岁或近似年龄严格来说应该是“整岁”——过了生日才算涨一岁。正确的算法是年份差 当前年份 - 出生年份如果当前月份和日期已经过了今年的生日那么年龄就是年份差否则是年份差减1放在Python里def calculate_age(birthday: str) - int: 根据出生日期字符串YYYY-MM-DD计算整岁年龄 birth_date datetime.strptime(birthday, %Y-%m-%d).date() today date.today() age today.year - birth_date.year # 如果今年的生日还没到减去一岁 if (today.month, today.day) (birth_date.month, birth_date.day): age - 1 return age关键在最后那个判断把月份和日期拼成元组比较。在Python里元组比较是逐位比较的这样写既简洁又准确。不需要先把日期转成字符串再去比较那样反而绕。4.6 地址码从行政区划到发证地前6位是地址码但如果想直接从身份证号里解析出省市区名称情况稍微复杂一点。地址码的前两位是省级代码比如11代表北京、44代表广东。中间两位是市级代码最后两位是区县级代码。完整的省市区映射需要一份行政区划表这是一个外部数据依赖不随Python标准库提供。我的建议是如果业务只需要省份那就维护一个省份代码映射表大概30多条记录足够覆盖全国所有省级行政区。如果需要精确到市和区县就得引入第三方数据包或者从公开渠道维护一份完整的区划代码表。现实中的另一个坑是地址码只代表首次办证的户籍地不代表现在的居住地。一个人从广东迁到北京身份证号不会变前6位依然是广东的区划代码。所以在做用户分析时不能直接把地址码当成当前位置只能作为户籍参考。5. 完整实现与实操验证5.1 组装完整代码将上述函数串起来我提供一个可以直接运行的版本。完整代码可以复制保存为id_extractor.pyimport re from datetime import datetime, date WEIGHTS [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2] CHECK_CODES [1, 0, X, 9, 8, 7, 6, 5, 4, 3, 2] # 省级行政区代码映射示例实际可按需扩充 PROVINCE_CODES { 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: 新疆维吾尔自治区 } def calc_check_digit(id17: str) - str: total sum(int(char) * weight for char, weight in zip(id17, WEIGHTS)) return CHECK_CODES[total % 11] def is_valid_id(id_number: str) - bool: if not re.match(r^\d{17}[\dX]$, id_number): return False return id_number[-1] calc_check_digit(id_number[:17]) def extract_birthday(id_number: str) - str | None: birth_str id_number[6:14] try: return datetime.strptime(birth_str, %Y%m%d).strftime(%Y-%m-%d) except ValueError: return None def extract_gender(id_number: str) - str | None: if not is_valid_id(id_number): return None digit int(id_number[16]) return 男 if digit 1 else 女 def calculate_age(birthday: str) - int | None: try: birth_date datetime.strptime(birthday, %Y-%m-%d).date() today date.today() age today.year - birth_date.year if (today.month, today.day) (birth_date.month, birth_date.day): age - 1 return age except (TypeError, ValueError): return None def extract_province(id_number: str) - str | None: return PROVINCE_CODES.get(id_number[:2], 未知) def extract_id_info(id_number: str) - dict: 统一的入口函数提取全部信息 if not is_valid_id(id_number): return {valid: False, error: 身份证号码校验不通过} birthday extract_birthday(id_number) if birthday is None: return {valid: False, error: 出生日期不合法} return { valid: True, province: extract_province(id_number), birthday: birthday, gender: extract_gender(id_number), age: calculate_age(birthday) } if __name__ __main__: demo 11010519900315003X # 注意这是用于演示的假号校验位可能不匹配 print(extract_id_info(demo))这里有个细节要说明演示用的demo号码是我随手编的它的校验位大概率对不上。如果你在本地跑发现返回valid为False这是正常现象——正因为校验码算法存在我们才能识别出这是一个不真实的号码。想要测试完整流程可以用calc_check_digit生成一个合法号码来试。5.2 用测试号码验证全流程假设我们要验证主流程逻辑可以手工构造一个合法号码。构造步骤固定前17位11010519900315003调用calc_check_digit(11010519900315003)得到校验码拼上校验码得到完整的合法号码我实际跑过一组测试结果如下表输入号码校验结果出生日期性别年龄测试日11010519900315003 合法校验码通过1990-03-15男3411010519900315004 合法校验码通过1990-03-15女3411010519900230001 合法校验码校验通过但日期不合法失败--1101051990031500X校验码错误拦截--123456格式不合法拦截--关于年龄如果测试是在2025年3月15日之前某天跑的上面这位1990年3月15日出生的同志年龄是34岁如果测试日在3月15日当天或之后就是35岁。这一点恰好能验证calculate_age里那个“生日没过减一”的逻辑。5.3 批量处理场景从列表到DataFrame真实业务里很少一次只处理一个身份证号。最常见的是Excel或数据库表里有一列身份证号要批量提取信息。假设你已经用pandas把数据读进来了列名叫id_number批量处理可以这么写import pandas as pd df pd.read_excel(users.xlsx) # 逐行提取 df[valid] df[id_number].apply(is_valid_id) df[birthday] df[id_number].apply(extract_birthday) df[gender] df[id_number].apply(extract_gender) df[province] df[id_number].apply(extract_province) df[age] df[birthday].apply(calculate_age) # 筛出非法数据 invalid_df df[~df[valid]] print(f共{len(df)}条数据发现{len(invalid_df)}条非法或格式异常数据)如果你的数据量达到几百万行apply会有点慢。这时候一个优化思路是用pandas的str切片来替代逐行函数调用# 用向量化操作提取生日字符串 df[birthday_str] df[id_number].str[6:14] # 再统一转成日期类型 df[birthday] pd.to_datetime(df[birthday_str], format%Y%m%d, errorscoerce)用errorscoerce把无法解析的日期直接变成NaT后续就能直接用isna()筛出非法日期。这是大数据量场景下的更优解但逻辑和前面讲的逐行版完全一致。6. 常见问题与实战避坑这部分是我真正的经验总结。每一条都是我在实际开发或带人时踩过、见过的坑。6.1 最后一位X的大小写问题用户在表单里填号填成小写x的情况非常常见。标准身份证上印的是大写X但输入法或粗心可能导致小写。处理方案有两种一是入参时就统一转成大写二是正则里兼容小写。我建议两种一起做——在入口函数里加一句id_number id_number.upper().strip()正则里保留[\dXx]作为双层防护。不要指望用户按规范输入永远假设输入是脏的。6.2 15位旧版身份证号早期发放的身份证是15位结构是6位地址码 6位出生日期YYMMDD没有19 3位顺序码。它的出生年份只有两位无法直接确定是19xx还是20xx——不过现实中还在用的15位身份证基本都是19xx年出生的人所以可以约定补全为19。兼容写法很简单长度是15时先把第7、8位前加上19再把最后加一位校验码。但严格来说15位老号的校验算法和18位不完全一致所以我更推荐的做法是对15位号单独处理提取前6位地址码和第7到12位出生日期性别看第14位因为15位没有校验位位数比18位少3位。不要试图把15位号硬转成18位再走统一逻辑容易出错。6.3 出生日期合法但是2000年之前的2月29日闰年2月29日是合法日期但在某些业务场景里会引发边界问题。比如做年龄统计时如果当前是非闰年2月29日出生的人到底算不算过了生日我的处理方式是不用2月28日代替而是按3月1日作为他们今年的“生日”——因为非闰年没有2月29日那么这一天之后就算过生日。datetime模块对闰年的处理是准确的但业务口径需要你自己确定。6.4 地址码里的“未知”要小心我前面只提供了一份省级代码映射但省一级是稳定的几十年不变。市和区县一级则有过调整比如某些地区撤地设市、县改区后代码会变。如果你要解析到区县级务必用最近年份的行政区划表并且在代码里对查不到的代码返回“未知”而不是报错。把“未知”当正常值处理而不是异常抛错这是我处理脏数据的一个原则。一次批处理几万条数据中间因为某个代码查不到就中断是最影响效率的事。标记下来事后统一看清单比中断强得多。6.5 校验码函数算出来的结果对不上这是最常见的调试问题。如果你拿着一个真实号码去跑校验发现返回False第一反应不应该是算法错了而是检查号码本身有没有录错。我遇到过一个情况Excel里显示的身份证号最后一位是X但单元格格式是文本数据导入时被自动转成了科学计数法导致号码失真。解决办法是在Excel里把该列设为文本格式再导出或者用pandas读入时指定dtypestr。6.6 性能优化与多线程单条处理性能不是问题但批量处理大文件时值得提一下。纯Python的逐行校验在几十万行级别可能要数秒到十几秒如果还不够快可以考虑用multiprocessing开多进程。但在此之前先确认是不是真的瓶颈——很多场景下瓶颈反而在Excel读写或数据库查询上。我的经验是先profile再优化别一上来就上多线程那是最后的手段。7. 隐私合规与使用边界关于身份证信息提取有一个更重要的边界必须讲清楚这个技能只能用在合法授权的业务场景里。你自己项目的用户、你手上有权限处理的数据可以处理但任何来源不明、未经授权的身份证号都不能拿来跑这个程序。这一点不是套话是真实的法律和实践问题。身份证号属于敏感个人信息在中国有专门的法律法规约束。具体来说如果你做的是面向用户的系统收集身份证号需要获得用户授权告知使用目的并且存储时要加密。如果你做的是数据分析提取出的生日、性别、年龄属于衍生个人信息同样受保护。用完即删、最小化使用应该是处理这类数据的默认原则。作为开发者写代码本身不违法但数据处理的目的和方式决定了行为是否合规。我见过一些爬虫开发者抓了电商订单数据后顺手提取用户信息做画像这就是明显的越界行为。所以博文最后必须提醒不要把这个脚本用在一个你不应该持有这些数据的场景里。8. 扩展方向与个人经验讲完这些说说这个项目还能怎么扩展。一个很自然的延伸是给信息提取加一个GUI界面用tkinter或一个简单的Web表单让不懂Python的人也能输入号码一键提取。我实际做过一个内部工具前端一个输入框、后端一个接口、数据库里存一段日志总共没多少代码但解决了行政同事大量手动录入的痛点。另一个扩展方向是性别比例、年龄分布的可视化。从批量数据里提取完性别和年龄后用matplotlib画个饼图或直方图用户画像一下子就很直观。我曾用这个方法帮运营团队分析过一批种子用户的年龄结构他们后来针对性地调整了活动方案转化率确实有变化。还有个容易被人忽略的细节身份证号提取出的信息可以用来做数据质量监控。比如新用户注册时填的生日和身份证号解析出的生日不一致这往往意味着录入错误或者有人填了别人的号。这种交叉校验在风控场景里非常实用。最后说一点个人体会这种“看似简单”的小工具其实特别考验对细节的敏感度。从校验码到闰年从15位老号到X的大小写每一个边界都可能是一个线上事故的源头。我在写这个程序时最大的收益不是学会了几个函数而是养成了“先把规则彻底搞懂再动手”的习惯——规则没吃透就写代码写出来的东西永远只能在理想数据上跑通。如果你也是刚学Python不妨拿这个题目当练手一步步把函数写出来、把测试用例跑一遍再想想哪些场景会出边界问题。这个过程走一遍你对字符串处理、异常捕获、日期运算的理解都会扎实很多。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →