
文章目录
-
- 环境信息
- 前言:正则能匹配"格式",但匹配不了"真假"
- 一、香港身份证号码的格式:三层结构
- [二、校验码算法:mod 11 加权](#二、校验码算法:mod 11 加权)
- [三、香港车牌格式:传统 vs 自定义](#三、香港车牌格式:传统 vs 自定义)
- 四、香港电话号码格式
- 五、完整验证器(可直接复用)
- 六、三个易错点
-
- 坑1:全角括号
- 坑2:字母转数字的偏移量
- [坑3:逻辑有效 ≠ 真实存在](#坑3:逻辑有效 ≠ 真实存在)
- 七、总结
环境信息
| 项目 | 版本/说明 |
|---|---|
| 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 加权
这是本文的核心。香港身份证的校验码算法是公开的,逻辑如下:
- 字母转数字:A=10, B=11, C=12, ..., Z=35
- 单字母补空格:如果只有一个开头字母,前面补一个"空格",值=36
- 加权相乘:从右到左,权重依次是 9,8,7,6,5,4,3,2,1(校验码位权重是1)
- 求和取余:所有乘积相加,除以 11 取余数
- 校验码规则 :
- 余数 = 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 1234、A 1。
自定义车牌 (2006年起):最多 8 个字符,可以是字母+数字+空格的任意组合,如 MYCAR、L0VE 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 加权)适合算法验证。
三个东西记住:
- 正则验证格式,校验码验证逻辑------两层验证缺一不可,这是数据校验的通用思路
- 香港身份证 A=10 不是 A=1------字母转数字的偏移量,是最容易写错的细节
- 全角括号是真实数据里的坑------中文输入法下,半角正则匹配不到全角括号
这篇文章是0810那篇"数据清洗"的自然延续------0810讲的是怎么处理Big5编码,这篇讲的是怎么校验数据格式。两者合起来,才是完整的"香港数据清洗与验证"闭环。
本文为 Python 正则表达式与校验码算法的技术分享。香港身份证校验码算法为公开标准(mod 11 加权),文中示例号码均为演示用途的假号码,不涉及任何真实个人数据。本文不构成任何身份核验建议。
