Python正则表达式实战:解析和验证香港身份证、车牌、电话格式

文章目录

环境信息

项目 版本/说明
Python 3.10+
标准库 re(正则)、无需第三方依赖
验证对象 香港身份证 / 车牌 / 电话号码
数据来源 格式规则和校验码算法为公开标准

前言:正则能匹配"格式",但匹配不了"真假"

上周我在做一个表单数据清洗的活,需要校验用户填写的香港身份证号码。我第一反应是写个正则表达式:

python 复制代码
import re
pattern = r'^[A-Z]{1,2}\d{6}\([0-9A]\)$'

格式上是匹配了------1到2个大写字母 + 6位数字 + 括号里的校验码。但很快我就发现一个问题:A123456(3)A123456(9) 都符合这个正则,但只有一个是真的。

正则表达式只能验证"格式对不对",验证不了"号码是不是真实存在的"。香港身份证号码里那个括号中的校验码,不是随便写的------它是通过一个 mod 11 加权算法算出来的。格式匹配只是第一层,校验码验证才是第二层。

这篇文章讲的就是这"两层验证"。前半部分是正则解析香港身份证、车牌、电话的格式,后半部分是香港身份证校验码算法的完整实现。

收藏提示①:正则匹配"像不像",校验码算法验证"是不是真的"。表单验证、数据清洗、反作弊,缺一层都不行。文末有完整可复用的验证器代码。

一、香港身份证号码的格式:三层结构

香港身份证号码由三部分组成(以 A123456(3) 为例):

复制代码
A      123456      (3)
↑      ↑           ↑
字母   6位数字     校验码(0-9或A)

规则:

  • 开头是 1-2 个大写英文字母(A-Z)
  • 中间是 6 位数字
  • 括号里是校验码,0-9 或字母 A
  • 括号可能是半角 () 或全角 ()

对应的正则:

python 复制代码
import re

# 支持半角/全角括号,校验码0-9或A
HKID_PATTERN = re.compile(
    r'^([A-Z]{1,2})(\d{6})[((]([0-9A])[))]$'
)

def parse_hkid(hkid: str):
    """解析香港身份证号码,返回(字母部分, 数字部分, 校验码)"""
    m = HKID_PATTERN.match(hkid.strip().upper())
    if not m:
        return None
    return m.group(1), m.group(2), m.group(3)

# 测试
print(parse_hkid('A123456(3)'))   # ('A', '123456', '3')
print(parse_hkid('AB987654(3)'))  # 全角括号也能识别
print(parse_hkid('A1234563'))      # 无括号 → None(不匹配)

注意一个细节:我用了 [((][))] 来同时匹配半角和全角括号------香港用户在中文输入法下输入时,经常打出全角括号,只匹配半角会漏掉一大批真实数据。

二、校验码算法:mod 11 加权

这是本文的核心。香港身份证的校验码算法是公开的,逻辑如下:

  1. 字母转数字:A=10, B=11, C=12, ..., Z=35
  2. 单字母补空格:如果只有一个开头字母,前面补一个"空格",值=36
  3. 加权相乘:从右到左,权重依次是 9,8,7,6,5,4,3,2,1(校验码位权重是1)
  4. 求和取余:所有乘积相加,除以 11 取余数
  5. 校验码规则
    • 余数 = 0 → 校验码是 0
    • 余数 = 1 → 校验码是 A
    • 余数 = 2~10 → 校验码 = 11 - 余数

A123456(3) 举例验证:

复制代码
A=10,单字母前面补空格=36
加权:36×9 + 10×8 + 1×7 + 2×6 + 3×5 + 4×4 + 5×3 + 6×2
    = 324 + 80 + 7 + 12 + 15 + 16 + 15 + 12
    = 481
481 ÷ 11 = 43 余 8
11 - 8 = 3  → 校验码 = 3 ✓

完整实现:

python 复制代码
def calculate_hkid_check_digit(letters: str, digits: str) -> str:
    """
    计算香港身份证号码的校验码
    letters: 开头字母部分(1-2个大写字母)
    digits: 6位数字字符串
    返回: 校验码('0'-'9' 或 'A')
    """
    # 1. 构建完整的8位"值序列"(不含校验码位)
    # 字母转数字:A=10 ... Z=35
    values = []
    if len(letters) == 1:
        values.append(36)  # 单字母时,前面补空格=36
        values.append(ord(letters[0]) - ord('A') + 10)
    else:  # 双字母
        values.append(ord(letters[0]) - ord('A') + 10)
        values.append(ord(letters[1]) - ord('A') + 10)
    
    # 2. 数字部分
    for ch in digits:
        values.append(int(ch))
    
    # 3. 加权求和(权重从9开始递减)
    weights = [9, 8, 7, 6, 5, 4, 3, 2]
    total = sum(v * w for v, w in zip(values, weights))
    
    # 4. 取余
    remainder = total % 11
    
    # 5. 校验码规则
    if remainder == 0:
        return '0'
    elif remainder == 1:
        return 'A'
    else:
        return str(11 - remainder)


def validate_hkid(hkid: str) -> bool:
    """完整验证香港身份证号码(格式 + 校验码)"""
    parsed = parse_hkid(hkid)
    if not parsed:
        return False
    letters, digits, check_digit = parsed
    calculated = calculate_hkid_check_digit(letters, digits)
    return calculated == check_digit

# 测试
print(calculate_hkid_check_digit('A', '123456'))  # '3'
print(validate_hkid('A123456(3)'))   # True
print(validate_hkid('A123456(9)'))   # False(校验码不对)

收藏提示②:validate_hkid 这个函数是"两层验证"的完整实现------第一层正则查格式,第二层 mod 11 查校验码。直接用 validate_hkid() 就能判断一个香港身份证号码是否逻辑有效。注意:逻辑有效 ≠ 真实存在(它只能证明号码结构正确,不能证明这个身份证真的发给了某个人)。

三、香港车牌格式:传统 vs 自定义

香港车牌有两种,格式完全不同:

传统车牌 :1-2 个字母 + 1-4 位数字,如 AB 1234A 1

自定义车牌 (2006年起):最多 8 个字符,可以是字母+数字+空格的任意组合,如 MYCARL0VE U

python 复制代码
# 传统车牌:1-2字母 + 1-4数字(可能有空格分隔)
TRADITIONAL_PLATE = re.compile(r'^([A-Z]{1,2})\s?(\d{1,4})$')

# 自定义车牌:最多8个字符(字母/数字/空格)
CUSTOM_PLATE = re.compile(r'^[A-Z0-9 ]{1,8}$')

def parse_plate(plate: str) -> str:
    """识别香港车牌类型"""
    plate = plate.strip().upper()
    if TRADITIONAL_PLATE.match(plate):
        return f'传统车牌: {plate}'
    elif CUSTOM_PLATE.match(plate):
        return f'自定义车牌: {plate}'
    return '无效车牌'

print(parse_plate('AB 1234'))  # 传统车牌
print(parse_plate('MYCAR'))    # 自定义车牌
print(parse_plate('ABC12345')) # 无效(9个字符超限)

四、香港电话号码格式

香港电话号码统一 8 位数字,但不同号段有不同含义:

  • 固话:通常 2 或 3 开头
  • 手机:4/5/6/9 开头
  • 国际格式+852 前缀
python 复制代码
# 8位数字(可带 +852 或 00852 国际前缀)
PHONE_PATTERN = re.compile(
    r'^(?:\+852|00852)?\s?([2-9]\d{7})$'
)

def parse_phone(phone: str) -> str:
    """解析香港电话号码"""
    m = PHONE_PATTERN.match(phone.strip())
    if not m:
        return '无效电话号码'
    number = m.group(1)
    prefix = number[0]
    if prefix in '23':
        return f'固话: {number}'
    elif prefix in '4569':
        return f'手机: {number}'
    else:
        return f'其他: {number}'

print(parse_phone('+852 2345 6789'))  # 固话
print(parse_phone('98765432'))        # 手机(9开头)
print(parse_phone('12345678'))        # 无效(1开头,且只有7位数字)

五、完整验证器(可直接复用)

把三个验证器整合成一个工具类:

python 复制代码
import re

class HKDataValidator:
    """香港身份证/车牌/电话格式验证器"""
    
    # 身份证
    HKID_PATTERN = re.compile(r'^([A-Z]{1,2})(\d{6})[((]([0-9A])[))]$')
    
    # 车牌
    TRADITIONAL_PLATE = re.compile(r'^([A-Z]{1,2})\s?(\d{1,4})$')
    CUSTOM_PLATE = re.compile(r'^[A-Z0-9 ]{1,8}$')
    
    # 电话
    PHONE_PATTERN = re.compile(r'^(?:\+852|00852)?\s?([2-9]\d{7})$')
    
    @staticmethod
    def _letter_to_num(ch: str) -> int:
        return ord(ch) - ord('A') + 10
    
    @classmethod
    def validate_hkid(cls, hkid: str) -> bool:
        m = cls.HKID_PATTERN.match(hkid.strip().upper())
        if not m:
            return False
        letters, digits, check_digit = m.group(1), m.group(2), m.group(3)
        
        # 构建值序列
        if len(letters) == 1:
            values = [36, cls._letter_to_num(letters[0])]
        else:
            values = [cls._letter_to_num(letters[0]), cls._letter_to_num(letters[1])]
        values += [int(ch) for ch in digits]
        
        # 加权求和
        weights = [9, 8, 7, 6, 5, 4, 3, 2]
        total = sum(v * w for v, w in zip(values, weights))
        remainder = total % 11
        
        # 校验码
        if remainder == 0:
            calculated = '0'
        elif remainder == 1:
            calculated = 'A'
        else:
            calculated = str(11 - remainder)
        
        return calculated == check_digit
    
    @classmethod
    def validate_plate(cls, plate: str) -> bool:
        plate = plate.strip().upper()
        return bool(cls.TRADITIONAL_PLATE.match(plate) or cls.CUSTOM_PLATE.match(plate))
    
    @classmethod
    def validate_phone(cls, phone: str) -> bool:
        return bool(cls.PHONE_PATTERN.match(phone.strip()))


# 使用示例
validator = HKDataValidator()
print(validator.validate_hkid('A123456(3)'))   # True
print(validator.validate_hkid('A123456(9)'))   # False
print(validator.validate_plate('AB 1234'))      # True
print(validator.validate_phone('+852 9876 5432'))  # True

六、三个易错点

坑1:全角括号

香港用户用中文输入法时,括号经常是全角 ()。如果你只写 \(...\)(半角),会漏掉一大批真实数据。正则里同时匹配半角和全角 ,用 [((][))]

坑2:字母转数字的偏移量

字母 A 对应数字 10 (不是 1)。这是最容易写错的地方------如果你用 ord(ch) - ord('A') + 1,A 会变成 1,整个校验码全错。正确是 + 10

python 复制代码
# ❌ 错误:A=1
# ✅ 正确:A=10
def _letter_to_num(ch): return ord(ch) - ord('A') + 10

坑3:逻辑有效 ≠ 真实存在

校验码算法只能证明"这个号码的结构是自洽的"(校验码和前面的字符对得上)。它不能证明这个身份证真的发给了某个人。真正的身份核验需要调政府接口或人工核对,正则和校验码只是第一道防线。

收藏提示③:三个坑里,全角括号和字母偏移是最容易在真实项目里踩的。字母 A=10 不是 A=1------这个偏移量错了,整个校验码算法就崩了。

七、总结

正则表达式能解决的,是"格式对不对"。校验码算法能解决的,是"号码逻辑真不真"。香港身份证号码恰好是两者的完美结合------格式(1-2字母+6数字+校验码)适合正则,校验码(mod 11 加权)适合算法验证。

三个东西记住:

  1. 正则验证格式,校验码验证逻辑------两层验证缺一不可,这是数据校验的通用思路
  2. 香港身份证 A=10 不是 A=1------字母转数字的偏移量,是最容易写错的细节
  3. 全角括号是真实数据里的坑------中文输入法下,半角正则匹配不到全角括号

这篇文章是0810那篇"数据清洗"的自然延续------0810讲的是怎么处理Big5编码,这篇讲的是怎么校验数据格式。两者合起来,才是完整的"香港数据清洗与验证"闭环。


本文为 Python 正则表达式与校验码算法的技术分享。香港身份证校验码算法为公开标准(mod 11 加权),文中示例号码均为演示用途的假号码,不涉及任何真实个人数据。本文不构成任何身份核验建议。

相关推荐
阿童木写作1 小时前
跨境电商图片翻译工具推荐:跨马翻译批量处理视频字幕与抠图
python·音视频
我不是疯子是傻子1 小时前
串口通信数据帧粘包拆包再进化:基于 Qt 的滑动窗口与 CRC 校验实战
开发语言·qt
程序员老陆1 小时前
Qt中实现窗口真全屏(覆盖Windows任务栏)
开发语言·windows·qt
霸道流氓气质2 小时前
Java中信号量(Semaphore):从本地到分布式
java·开发语言·分布式
怪奇云呼军2 小时前
G.711、Opus 和重采样会拖慢识别吗?闪电智能VoiceAgent 的音频入口怎么选
java·人工智能·python·算法·云计算·音视频
ZC跨境爬虫2 小时前
LeetCode 27. 移除元素(双指针详解 + Java Python 多解法对比)
java·python·leetcode
路溪非溪2 小时前
Python语言的执行流程总结
开发语言·python
民乐团扒谱机2 小时前
【超详解】量子克拉美罗下界QCRB:时延估计精度极限推导+MATLAB多场景绘图实战
开发语言·机器学习·matlab·量子力学·qcrb
宋均浩3 小时前
AI 生成代码质量防线实战:5 个 CI/CD 配置,把 8% 的逻辑错拦在线上之前0
python