Chapter 09 / 10

常用正则模式库

经过验证的正则表达式集合——邮箱、URL、日期、手机号、身份证、代码处理

WARNING(正则的局限性)正则表达式验证格式,但不验证语义。"邮箱格式正确"不代表邮箱真实存在;"手机号格式正确"不代表号码已注册。生产环境请使用经过充分测试的验证库(Python:email-validatorphonenumbers;JavaScript:validator.js)或通过实际验证(发送邮件/短信)来确认真实性。

邮箱地址

邮箱验证是正则的经典场景,但完整的 RFC 5321 邮箱规范极其复杂(允许很多奇怪的格式)。实用版本覆盖 99% 的真实邮箱:

# 实用版(平衡精度与可读性)
EMAIL = re.compile(r"""
    ^
    [a-zA-Z0-9._%+\-]+      # 本地部分:允许字母、数字、._%+-
    @                        # @ 符号
    [a-zA-Z0-9.\-]+          # 域名:字母、数字、点、连字符
    \.                       # 最后一个点
    [a-zA-Z]{2,}             # 顶级域名:至少2位字母
    $
""", re.VERBOSE)

def is_valid_email(email: str) -> bool:
    return bool(EMAIL.fullmatch(email.strip().lower()))

# 测试用例
is_valid_email('user@example.com')           # True  ✓ 标准格式
is_valid_email('user+tag@sub.domain.org')    # True  ✓ 含标签
is_valid_email('first.last@company.co.uk')   # True  ✓ 多级域名
is_valid_email('invalid@')                   # False ✗ 缺少域名
is_valid_email('@nodomain.com')              # False ✗ 缺少用户名
is_valid_email('no spaces@example.com')      # False ✗ 含空格

# 注意:以下合法但可能不符合业务需求
is_valid_email('a@b.io')   # True(顶级域 io,2位)
is_valid_email('x@y.z')   # True(格式正确但域名可能无效)

URL

# 简单 URL 验证
URL_SIMPLE = re.compile(r'^https?://[^\s/$.?#].[^\s]*$')

# 详细版(解析各部分,供提取使用)
URL_DETAILED = re.compile(r"""
    ^
    (?P<scheme>https?|ftp)://     # 协议(http/https/ftp)
    (?P<host>                      # 主机
        (?:[a-zA-Z0-9\-]+\.)+      # 子域和域名(可以有多级)
        [a-zA-Z]{2,}               # 顶级域名
    )
    (?::(?P<port>\d{1,5}))?        # 端口(可选,1-5位数字)
    (?P<path>/[^\s?#]*)?           # 路径(可选,/ 开头)
    (?:\?(?P<query>[^\s#]*))?      # 查询字符串(可选,? 开头)
    (?:\#(?P<fragment>\S*))?       # 锚点(可选,# 开头)
    $
""", re.VERBOSE)

# 测试
m = URL_DETAILED.match('https://api.example.com:8080/v1/users?page=1#top')
m.groupdict()
# {'scheme': 'https', 'host': 'api.example.com', 'port': '8080',
#  'path': '/v1/users', 'query': 'page=1', 'fragment': 'top'}

# 从文本中提取所有 URL
def extract_urls(text):
    return re.findall(r'https?://[^\s><"\'()]+(?<![.,)])', text)

IP 地址

# IPv4(精确版:每段 0-255)
# 拆解:255-255 | 200-249 | 100-199 | 10-99 | 0-9
OCTET = r'(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)'
IPv4 = re.compile(fr'^{OCTET}(?:\.{OCTET}){{3}}$')

IPv4.match('192.168.1.1')     # 匹配 ✓
IPv4.match('255.255.255.0')   # 匹配 ✓
IPv4.match('256.0.0.1')       # None ✗(256 超范围)
IPv4.match('192.168.1')       # None ✗(只有3段)

# IPv6(简化版,不处理压缩写法 ::)
IPv6_SIMPLE = re.compile(
    r'^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$'
)

# CIDR 表示法(如 192.168.1.0/24)
CIDR = re.compile(fr'^{OCTET}(?:\.{OCTET}){{3}}/(?:3[0-2]|[12]\d|\d)$')
CIDR.match('10.0.0.0/8')       # 匹配 ✓
CIDR.match('192.168.1.0/24')   # 匹配 ✓
CIDR.match('10.0.0.0/33')      # None ✗(前缀长度超过32)

日期与时间

# ISO 日期 YYYY-MM-DD(只验证格式,不验证月份天数合法性)
DATE = re.compile(r'^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$')

DATE.fullmatch('2026-03-26')   # 匹配 ✓
DATE.fullmatch('2026-13-01')   # None ✗(月份 13)
DATE.fullmatch('2026-03-32')   # None ✗(日期 32)
DATE.fullmatch('2026-02-31')   # 匹配(正则无法验证2月没有31天)

# 时间 HH:MM:SS(24小时制)
TIME = re.compile(r'^([01]\d|2[0-3]):[0-5]\d:[0-5]\d$')

# ISO 8601 完整格式 YYYY-MM-DDTHH:MM:SS(.fff)?(Z|±HH:MM)?
ISO8601 = re.compile(r"""
    ^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])  # 日期
    T                                                # 分隔符
    ([01]\d|2[0-3]):[0-5]\d:[0-5]\d                # 时间
    (?:\.\d+)?                                       # 毫秒(可选)
    (?:Z|[+-]\d{2}:\d{2})?                          # 时区(可选)
    $
""", re.VERBOSE)

ISO8601.fullmatch('2026-03-26T10:30:45')          # 匹配 ✓
ISO8601.fullmatch('2026-03-26T10:30:45.123Z')     # 匹配 ✓
ISO8601.fullmatch('2026-03-26T10:30:45+08:00')    # 匹配 ✓

中国手机号

# 精简版:1开头,第2位3-9,共11位
PHONE_CN = re.compile(r'^1[3-9]\d{9}$')

# 宽松版:允许 +86 前缀、空格、连字符分隔
PHONE_CN_LOOSE = re.compile(r'^(?:\+?86)?[-\s]?1[3-9]\d{9}$')

# 精确版(按运营商号段,可能过时)
PHONE_CN_STRICT = re.compile(r"""
    ^1
    (?:
        3[0-9]   |   # 130-139(移动、电信等)
        4[5-9]   |   # 145-149(数据卡)
        5[0-35-9]|   # 150-153, 155-159
        6[2567]  |   # 162, 165, 166, 167
        7[0-8]   |   # 170-178
        8[0-9]   |   # 180-189
        9[0-35-9]    # 190-193, 195-199
    )
    \d{8}
    $
""", re.VERBOSE)

PHONE_CN.match('13800138000')    # 匹配 ✓
PHONE_CN.match('12345678901')    # None ✗(第2位是2)
PHONE_CN_LOOSE.match('+8613800138000')  # 匹配 ✓

中国居民身份证

# 18位身份证(只验证格式,不验证校验码)
ID_CARD = re.compile(r"""
    ^
    \d{6}                     # 地区码(6位)
    (19|20)\d{2}              # 年份(1900-2099)
    (0[1-9]|1[0-2])           # 月份(01-12)
    (0[1-9]|[12]\d|3[01])     # 日期(01-31)
    \d{3}                     # 顺序码(3位)
    [\dX]                     # 校验码(数字或X)
    $
""", re.VERBOSE)

# 完整验证需要 Luhn 算法验证最后一位校验码
def verify_id_card(id_str):
    if not ID_CARD.fullmatch(id_str):
        return False
    # 验证校验码(加权求和)
    weights = [7,9,10,5,8,4,2,1,6,3,7,9,10,5,8,4,2]
    check_codes = '10X98765432'
    total = sum(int(id_str[i]) * weights[i] for i in range(17))
    return id_str[17].upper() == check_codes[total % 11]

密码强度

# 策略一:用多个前瞻检查各个条件(推荐,可读性好)
STRONG_PWD = re.compile(r"""
    ^
    (?=.*[A-Z])              # 至少一个大写字母
    (?=.*[a-z])              # 至少一个小写字母
    (?=.*\d)                 # 至少一个数字
    (?=.*[!@#$%^&*()_+\-=])  # 至少一个特殊字符
    .{8,64}                  # 长度 8-64 位
    $
""", re.VERBOSE)

STRONG_PWD.match('Abc123!@')    # 匹配 ✓
STRONG_PWD.match('abc123!@')    # None ✗(无大写)
STRONG_PWD.match('Abcdefgh')    # None ✗(无数字和特殊字符)
STRONG_PWD.match('Ab1!')        # None ✗(长度不足8)

# 策略二:返回密码强度等级
def password_strength(pwd):
    score = 0
    if re.search(r'[a-z]', pwd): score += 1     # 有小写
    if re.search(r'[A-Z]', pwd): score += 1     # 有大写
    if re.search(r'\d', pwd): score += 1        # 有数字
    if re.search(r'[!@#$%^&*]', pwd): score += 1  # 有特殊字符
    if len(pwd) >= 12: score += 1              # 长度加分
    levels = ['极弱', '弱', '中等', '强', '很强', '极强']
    return levels[score]

信用卡号

# 各卡种识别(格式验证,不包含 Luhn 校验)

# Visa:4 开头,16 位
VISA = re.compile(r'^4\d{15}$')

# MasterCard:51-55 或 2221-2720 开头
MASTERCARD = re.compile(
    r'^(?:5[1-5]\d{14}|2(?:2[2-9]\d|[3-6]\d{2}|7[01]\d|720)\d{12})$'
)

# American Express:34 或 37 开头,15位
AMEX = re.compile(r'^3[47]\d{13}$')

# 银联:62 开头,16-19 位
UNIONPAY = re.compile(r'^62\d{14,17}$')

# 通用格式(允许空格/连字符分组)
CARD_FORMAT = re.compile(r'^\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}$')

# 格式化:去除非数字字符后验证
def normalize_card(card):
    digits = re.sub(r'[\s\-]', '', card)  # 去除空格和连字符
    return digits if re.fullmatch(r'\d{13,19}', digits) else None

代码相关的常用模式

# 十六进制颜色值 (#rgb 或 #rrggbb)
HEX_COLOR = re.compile(r'^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$')

HEX_COLOR.fullmatch('#fff')     # 匹配 ✓(3位)
HEX_COLOR.fullmatch('#ff5733') # 匹配 ✓(6位)
HEX_COLOR.fullmatch('#gggggg') # None ✗(g不是十六进制)

# 语义化版本号(Semver)
SEMVER = re.compile(r"""
    ^
    (?P<major>0|[1-9]\d*)     # 主版本
    \.
    (?P<minor>0|[1-9]\d*)     # 次版本
    \.
    (?P<patch>0|[1-9]\d*)     # 补丁版本
    (?:-(?P<pre>[a-zA-Z0-9.-]+))?    # 预发布标签(可选)
    (?:\+(?P<build>[a-zA-Z0-9.-]+))? # 构建元数据(可选)
    $
""", re.VERBOSE)

m = SEMVER.match('2.1.0-alpha.1+build.100')
m.groupdict()
# {'major':'2', 'minor':'1', 'patch':'0',
#  'pre':'alpha.1', 'build':'build.100'}

# Python 变量名(合法标识符)
PY_IDENTIFIER = re.compile(r'^[a-zA-Z_][a-zA-Z0-9_]*$')

# 中国邮政编码(6位数字)
ZIP_CN = re.compile(r'^[1-9]\d{5}$')

# MAC 地址
MAC_ADDR = re.compile(r'^([0-9a-fA-F]{2}[:-]){5}[0-9a-fA-F]{2}$')
MAC_ADDR.fullmatch('00:1A:2B:3C:4D:5E')  # 匹配 ✓

HTML 属性提取

# 提取 href 链接
def extract_links(html):
    return re.findall(r'href=["\']([^"\']+)["\']', html, re.IGNORECASE)

# 提取 src 属性(图片/脚本)
def extract_src(html):
    return re.findall(r'src=["\']([^"\']+)["\']', html, re.IGNORECASE)

# 提取特定标签的 class
def extract_classes(html, tag):
    pattern = re.compile(rf'<{tag}[^>]*class=["\']([^"\']+)["\']', re.IGNORECASE)
    return pattern.findall(html)

# 移除 HTML 标签(提取纯文本)
def strip_tags(html):
    html = re.sub(r'<(?:script|style)[^>]*>.*?</(?:script|style)>',
                  '', html, flags=re.DOTALL|re.IGNORECASE)
    html = re.sub(r'<[^>]+>', '', html)
    html = re.sub(r'&\w+;', '', html)   # 移除 HTML 实体
    return re.sub(r'\s+', ' ', html).strip()
INFO(HTML 处理的最佳实践)正则只适合提取已知、简单、固定结构的 HTML 属性(如批量提取 href)。对于需要完整解析 HTML 的场景,请使用 BeautifulSoup(Python)或 DOMParser(浏览器 JavaScript)——它们能正确处理嵌套、注释、乱序属性等复杂情况,而正则无法做到。

小结

本章要点