🐍 第23课:正则进阶

—— 复杂匹配/替换/性能优化

🏆 命名分组+环视+回调替换
✅ Python验证通过

📌 本课目标

1️⃣ 命名分组

import re

# 基础分组:用编号访问
m = re.match(r"(\d{4})-(\d{2})-(\d{2})", "2024-03-15")
print(m.group(1))  # 2024
print(m.group(2))  # 03

# 命名分组:用名字访问,更清晰
m = re.match(r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})", "2024-03-15")
print(m.group("year"))   # 2024
print(m.group("month"))  # 03
print(m.group("day"))    # 15

# groupdict() 一次获取所有命名分组
print(m.groupdict())
# {'year': '2024', 'month': '03', 'day': '15'}

# 反向引用:匹配重复单词
text = "this is is a test test string"
duplicates = re.findall(r"\b(\w+)\s+\1\b", text)
print(duplicates)  # ['is', 'test']

# 命名反向引用
pattern = r"(?P<word>\w+)\s+(?P=word)"
m = re.search(pattern, "hello hello")
print(m.group("word"))  # hello

2️⃣ 环视断言(零宽断言)

import re

# 正向前瞻:(?=pattern)
text = "价格100元,折扣20元"
prices = re.findall(r"\d+(?=元)", text)
print(prices)  # ['100', '20']

# 负向前瞻:(?!pattern)
files = "photo.jpg logo.png icon.svg banner.png"
non_png = re.findall(r"\b\w+\.(?!png)\w+", files)
print(non_png)  # ['photo.jpg', 'icon.svg']

# 正向后顾:(?<=pattern)
html = "<h1>标题</h1> <p>段落</p>"
contents = re.findall(r"(?<=<\w+>).+?(?=</\w+>)", html)
print(contents)  # ['标题', '段落']

# 负向后顾:(?<!pattern)
text = "price: $100, count: 50, $30, qty: 20"
nums = re.findall(r"(?<!\$)\b\d+", text)
print(nums)  # ['50', '20']

# 实战:密码强度检查
def check_password_strength(password):
    """使用环视断言检查密码强度"""
    checks = {
        "长度≥8": bool(re.search(r".{8,}", password)),
        "含大写": bool(re.search(r"(?=.*[A-Z])", password)),
        "含小写": bool(re.search(r"(?=.*[a-z])", password)),
        "含数字": bool(re.search(r"(?=.*\d)", password)),
        "含特殊字符": bool(re.search(r"(?=.*[!@#$%^&*])", password)),
    }
    score = sum(checks.values())
    for k, v in checks.items():
        print(f"  {'✅' if v else '❌'} {k}")
    return score

3️⃣ re.sub() 高级替换

import re

# 回调函数替换:银行卡号脱敏
def mask_card(match):
    """保留前4后4"""
    card = match.group()
    return card[:4] + "*" * (len(card) - 8) + card[-4:]

result = re.sub(r"\d{16,19}", mask_card, "卡号6225881234567890")
print(result)  # 卡号6225********7890

# 手机号脱敏
def mask_phone(match):
    phone = match.group()
    return phone[:3] + "****" + phone[-4:]

text = "联系方式:13812345678,备用:15987654321"
result = re.sub(r"1[3-9]\d{9}", mask_phone, text)
print(result)  # 联系方式:138****5678,备用:159****4321

# 模板变量替换
template = "Hello {{name}}, welcome to {{city}}!"
data = {"name": "张三", "city": "北京"}
result = re.sub(r"\{\{(\w+)\}\}", lambda m: data.get(m.group(1), m.group(0)), template)
print(result)  # Hello 张三, welcome to 北京!

# 驼峰转下划线
camel = "myVariableName"
snake = re.sub(r"(?<!^)(?=[A-Z])", "_", camel).lower()
print(snake)  # my_variable_name

4️⃣ 常用实战模式

import re

# URL 匹配
url_pattern = re.compile(
    r"https?://"
    r"(?:[\w-]+\.)+[\w]{2,}"   # 域名
    r"(?::\d+)?"                # 端口
    r"(?:/[^\s]*)?"             # 路径
)

# 邮箱匹配(实用版)
email_pattern = re.compile(
    r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}"
)

# IPv4 匹配
ipv4_pattern = re.compile(
    r"(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}"
    r"(?:25[0-5]|2[0-4]\d|[01]?\d\d?)"
)

# 中国手机号
phone_pattern = re.compile(r"1[3-9]\d{9}")

# 中国身份证号(18位)
id_pattern = re.compile(
    r"[1-9]\d{5}"              # 地区码
    r"(?:19|20)\d{2}"          # 出生年
    r"(?:0[1-9]|1[0-2])"      # 月
    r"(?:0[1-9]|[12]\d|3[01])" # 日
    r"\d{3}"                   # 顺序码
    r"[\dXx]"                  # 校验码
)

# VERBOSE 模式:可读正则
verbose_url = re.compile(r"""
    https?://                  # 协议
    (?:[\w-]+\.)+[\w]{2,}     # 域名部分
    (?::\d+)?                  # 可选端口
    (?:/[^\s]*)?               # 可选路径
""", re.VERBOSE)

# 测试
text = "访问 https://example.com:8080/api 或 http://test.cn 联系 admin@mail.com"
print("URLs:", url_pattern.findall(text))
print("Emails:", email_pattern.findall(text))

5️⃣ 性能优化

import re

# 1. 预编译正则——循环中极重要
pattern = re.compile(r"\b\w+@\w+\.\w+\b")

# 2. 避免灾难性回溯——嵌套量词 (a+)+ 极易指数级回溯
# Python 3.11+ 支持 atomic 分组 (?>...)

# 3. 否定字符类 > 非贪婪匹配
# 慢:'".*?"'  快:'"[^"]*"'
slow = re.compile(r'".*?"')
fast = re.compile(r'"[^"]*"')

# 4. finditer 惰性生成,大量匹配时省内存
matches = list(pattern.finditer("contact: a@b.com and c@d.com"))
for m in matches:
    print(f"  找到: {m.group()} 位置: {m.start()}-{m.end()}")

6️⃣ 验证脚本

#!/usr/bin/env python3
"""第23课 正则进阶验证"""
import re

def test_named_groups():
    m = re.match(r"(?P<year>\d{4})-(?P<month>\d{2})", "2024-03")
    assert m.group("year") == "2024"
    assert m.group("month") == "03"
    assert m.groupdict() == {"year": "2024", "month": "03"}
    print("✅ 命名分组测试通过")

def test_lookaround():
    result = re.findall(r"\d+(?=元)", "100元 200刀 300元")
    assert result == ["100", "300"]
    result = re.findall(r"(?<=: )\d+", "port: 8080, pid: 1234")
    assert "8080" in result and "1234" in result
    print("✅ 环视断言测试通过")

def test_sub_callback():
    def double(match):
        return str(int(match.group()) * 2)
    result = re.sub(r"\d+", double, "3 apples and 5 oranges")
    assert result == "6 apples and 10 oranges"
    print("✅ 回调替换测试通过")

def test_precompiled():
    pattern = re.compile(r"\b\w{5}\b")
    result = pattern.findall("hello world python regex advanced")
    assert "hello" in result and "world" in result
    print("✅ 预编译性能测试通过")

def test_real_world_patterns():
    email_re = re.compile(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}")
    assert email_re.search("test@example.com")
    assert not email_re.search("not-an-email")
    phone_re = re.compile(r"1[3-9]\d{9}")
    assert phone_re.search("13812345678")
    assert not phone_re.search("12345678901")
    print("✅ 实战模式测试通过")

if __name__ == "__main__":
    test_named_groups()
    test_lookaround()
    test_sub_callback()
    test_precompiled()
    test_real_world_patterns()
    print("\n🎉 第23课全部验证通过!")

7️⃣ 正则调试技巧

import re

# 1. 逐步调试匹配
text = "订单号:ORD-2024-0315-8899"
pattern = r"ORD-(\d{4})-(\d{4})-(\d{4})"

m = re.search(pattern, text)
if m:
    print(f"完整匹配: {m.group(0)}")
    print(f"年份: {m.group(1)}")
    print(f"月日: {m.group(2)}")
    print(f"序号: {m.group(3)}")
    print(f"位置: {m.start()}-{m.end()}")

# 2. 常见正则陷阱
# 陷阱1:贪婪匹配吃太多
text = "<div>A</div><div>B</div>"
wrong = re.findall(r"<div>.*</div>", text)
right = re.findall(r"<div>.*?</div>", text)

# 陷阱2:点号不匹配换行
text = "line1
line2"
match = re.findall(r"line1.line2", text, re.DOTALL)

# 陷阱3:^$ 与多行模式
text = "first
second
third"
single = re.findall(r"^\w+", text)              # ['first']
multi = re.findall(r"^\w+", text, re.MULTILINE)  # ['first', 'second', 'third']

8️⃣ 实战:日志解析器

import re
from collections import Counter
from dataclasses import dataclass
from typing import List

@dataclass
class LogEntry:
    ip: str
    timestamp: str
    method: str
    path: str
    status: int
    size: int

LOG_PATTERN = re.compile(
    r'(\S+) - - '
    r'\[([^\]]+)\] '
    r'"(\S+) (\S+) \S+" '
    r'(\d+) (\d+)'
)

def parse_log(content: str) -> List[LogEntry]:
    entries = []
    for line in content.strip().split("
"):
        m = LOG_PATTERN.match(line)
        if m:
            entries.append(LogEntry(
                ip=m.group(1), timestamp=m.group(2),
                method=m.group(3), path=m.group(4),
                status=int(m.group(5)), size=int(m.group(6)),
            ))
    return entries

def analyze_logs(entries: List[LogEntry]):
    print(f"总请求数: {len(entries)}")
    print(f"唯一IP: {len(set(e.ip for e in entries))}")
    print(f"状态码分布: {dict(Counter(e.status for e in entries))}")
    print(f"Top5路径: {Counter(e.path for e in entries).most_common(5)}")
    
    errors = [e for e in entries if e.status >= 400]
    print(f"错误率: {len(errors)/len(entries)*100:.1f}%")

9️⃣ Unicode 与多字节字符

import re

# 匹配中文字符
text = "Hello世界Python编程"
chinese = re.findall(r"[一-鿿]+", text)
print(chinese)  # ['世界', '编程']

# 匹配中文标点
punct = re.findall(r"[,。!?、;:""''()【】]", "你好,世界!测试。")
print(punct)  # [',', '!', '。']

# 匹配 emoji
emoji_pattern = re.compile(
    "[😀-🙏"  # emoticons
    "🌀-🗿"   # symbols & pictographs
    "🚀-🛿"   # transport & map
    "🇠-🇿"   # flags
    "]+", flags=re.UNICODE
)
text = "你好👋世界🌍Python🐍"
emojis = emoji_pattern.findall(text)
print(emojis)  # ['👋', '🌍', '🐍']

# 去除 emoji
clean = emoji_pattern.sub("", text)
print(clean)  # 你好世界Python

# \w 在 re.UNICODE 下匹配中文
words = re.findall(r"\w+", text, re.UNICODE)
print(words)  # ['你好', '世界', 'Python']

# 按字符类型分割
text = "Python3.9版本2024年3月"
parts = re.findall(r"[a-zA-Z]+|\d+|[^\w]+", text)
print(parts)  # ['Python', '3', '.', '9', '版本', '2024', '年', '3', '月']

🔟 re 模块速查表

方法用途返回
re.match()从开头匹配Match 或 None
re.search()搜索第一个匹配Match 或 None
re.findall()所有匹配列表
re.finditer()所有匹配(惰性)迭代器
re.sub()替换新字符串
re.split()分割列表
re.compile()预编译Pattern 对象

🔟 正则可视化工具

"""推荐正则可视化工具:
1. regex101.com —— 在线测试 + 解释
2. debuggex.com —— 可视化正则流程图
3. regexr.com —— 实时匹配 + 语法参考
4. Python re.DEBUG —— 内置编译调试

使用 regex101.com 的技巧:
- 选择 Python flavor
- 开启 Explanation 面板
- 保存分享链接给同事
- 用单元测试验证边界情况
"""

🔑 本课要点

  1. 原始字符串 r''——避免反斜杠转义地狱
  2. 命名分组 (?P<name>)——让正则可读可维护
  3. 环视断言——零宽断言,只判断不消费字符
  4. re.sub()——支持回调函数,实现复杂替换逻辑
  5. 性能优化——预编译 + 避免回溯 + atomic 分组