fc / apps /core /utils /id_card_utils.py
jun2024's picture
Upload 2241 files
b4f2238 verified
Raw History Blame Contribute Delete
8.78 kB
"""
身份证号码解析工具类
提供统一的身份证号码解析功能,支持 15 位和 18 位身份证.
"""
import logging
from dataclasses import dataclass
from datetime import date
from django.utils.translation import gettext_lazy as _
logger = logging.getLogger(__name__)
# 校验码权重因子
ID_CARD_WEIGHTS = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
# 校验码对照表
ID_CARD_CHECK_CODES = ["1", "0", "X", "9", "8", "7", "6", "5", "4", "3", "2"]
@dataclass
class IdCardInfo:
"""身份证信息数据类"""
birth_date: str | None = None # 格式:YYYY年MM月DD日
gender: str | None = None # 男/女
age: int | None = None # 年龄
class IdCardUtils:
"""身份证号码解析工具类"""
@classmethod
def parse_id_card_info(cls, id_number: str) -> IdCardInfo:
"""
解析身份证号码,返回出生日期、性别和年龄
Args:
id_number: 身份证号码(15位或18位)
Returns:
IdCardInfo: 包含出生日期、性别、年龄的数据类
"""
if not id_number or len(id_number) < 14:
return IdCardInfo()
return IdCardInfo(
birth_date=cls.extract_birth_date(id_number),
gender=cls.extract_gender(id_number),
age=cls.calculate_age(id_number),
)
@classmethod
def extract_birth_date(cls, id_number: str) -> str | None:
"""
从身份证号码提取出生日期
Args:
id_number: 身份证号码
Returns:
格式化的出生日期字符串(YYYY年MM月DD日),解析失败返回 None
"""
if not id_number:
return None
try:
if len(id_number) == 18:
year = id_number[6:10]
month = id_number[10:12]
day = id_number[12:14]
return f"{year}年{month}月{day}日"
elif len(id_number) == 15:
year = "19" + id_number[6:8]
month = id_number[8:10]
day = id_number[10:12]
return f"{year}年{month}月{day}日"
except (ValueError, IndexError) as e:
logger.warning(f"解析身份证出生日期失败: {e}")
return None
@classmethod
def extract_gender(cls, id_number: str) -> str | None:
"""
从身份证号码提取性别
Args:
id_number: 身份证号码
Returns:
性别字符串(男/女),解析失败返回 None
"""
if not id_number or len(id_number) < 14:
return None
try:
# 18位身份证:倒数第二位;15位身份证:最后一位
gender_digit = int(id_number[-2]) if len(id_number) == 18 else int(id_number[-1])
return "男" if gender_digit % 2 == 1 else "女"
except (ValueError, IndexError) as e:
logger.warning(f"解析身份证性别失败: {e}")
return None
@classmethod
def calculate_age(cls, id_number: str) -> int | None:
"""
从身份证号码计算年龄
Args:
id_number: 身份证号码
Returns:
年龄(整数),解析失败返回 None
"""
if not id_number:
return None
try:
if len(id_number) == 18:
birth_year = int(id_number[6:10])
birth_month = int(id_number[10:12])
birth_day = int(id_number[12:14])
elif len(id_number) == 15:
birth_year = 1900 + int(id_number[6:8])
birth_month = int(id_number[8:10])
birth_day = int(id_number[10:12])
else:
return None
today = date.today()
age = today.year - birth_year
# 如果还没过生日,年龄减1
if (today.month, today.day) < (birth_month, birth_day):
age -= 1
return age
except (ValueError, IndexError) as e:
logger.warning(f"计算年龄失败: {e}")
return None
@classmethod
def validate_id_card(cls, id_number: str) -> dict[str, str | bool]:
"""
校验身份证号码是否合法
校验规则:
1. 18位身份证:
- 前17位必须为数字,第18位可以是数字或X(大小写不敏感)
- 前6位为地区码(简单校验前两位是否为有效省份代码: 11-65)
- 7-14位为出生日期,需要验证日期是否有效
- 第18位为校验码,根据前17位通过特定算法计算
2. 15位身份证(旧版):
- 全部为数字
- 前6位为地区码
- 7-12位为出生日期(YYMMDD),需要验证日期是否有效
Args:
id_number: 身份证号码
Returns:
dict: {"valid": bool, "message": str}
"""
if not id_number:
return {"valid": False, "message": str(_("身份证号码不能为空"))}
id_number = id_number.strip().upper()
# 长度校验
if len(id_number) not in (15, 18):
return {"valid": False, "message": str(_("身份证号码长度应为15位或18位"))}
# 18位身份证校验
if len(id_number) == 18:
return cls._validate_18_digit_id(id_number)
# 15位身份证校验
return cls._validate_15_digit_id(id_number)
@classmethod
def _validate_18_digit_id(cls, id_number: str) -> dict[str, str | bool]:
"""校验18位身份证号码"""
# 前17位必须为数字
if not id_number[:17].isdigit():
return {"valid": False, "message": str(_("身份证前17位必须为数字"))}
# 第18位校验
last_char = id_number[17]
if not (last_char.isdigit() or last_char == "X"):
return {"valid": False, "message": str(_("身份证第18位必须为数字或X"))}
# 地区码校验(前两位: 11-65)
province_code = int(id_number[:2])
if province_code < 11 or province_code > 65:
return {"valid": False, "message": str(_("身份证地区码无效"))}
# 出生日期校验
birth_date_str = id_number[6:14]
if not cls._validate_birth_date(birth_date_str, is_18_digit=True):
return {"valid": False, "message": str(_("身份证出生日期无效"))}
# 校验码计算
check_sum = 0
for i in range(17):
check_sum += int(id_number[i]) * ID_CARD_WEIGHTS[i]
check_code = ID_CARD_CHECK_CODES[check_sum % 11]
if id_number[17] != check_code:
return {"valid": False, "message": str(_("身份证校验码错误,正确校验码应为 %(code)s")) % {"code": check_code}}
return {"valid": True, "message": str(_("身份证号码格式正确"))}
@classmethod
def _validate_15_digit_id(cls, id_number: str) -> dict[str, str | bool]:
"""校验15位身份证号码"""
# 全部为数字
if not id_number.isdigit():
return {"valid": False, "message": str(_("15位身份证必须全部为数字"))}
# 地区码校验(前两位: 11-65)
province_code = int(id_number[:2])
if province_code < 11 or province_code > 65:
return {"valid": False, "message": str(_("身份证地区码无效"))}
# 出生日期校验(YYMMDD, 默认19XX年)
birth_date_str = "19" + id_number[6:12]
if not cls._validate_birth_date(birth_date_str, is_18_digit=True):
return {"valid": False, "message": str(_("身份证出生日期无效"))}
return {"valid": True, "message": str(_("身份证号码格式正确"))}
@classmethod
def _validate_birth_date(cls, date_str: str, *, is_18_digit: bool) -> bool:
"""
校验出生日期是否有效
Args:
date_str: 日期字符串(YYYYMMDD格式)
is_18_digit: 是否为18位身份证格式
Returns:
bool: 日期是否有效
"""
if len(date_str) != 8:
return False
try:
year = int(date_str[:4])
month = int(date_str[4:6])
day = int(date_str[6:8])
# 基本范围校验
if month < 1 or month > 12:
return False
if day < 1 or day > 31:
return False
# 年份范围校验
current_year = date.today().year
if year < 1900 or year > current_year:
return False
# 具体日期校验
date(year, month, day)
return True
except (ValueError, TypeError):
return False