Python正则表达式进阶:从入门到精通的实战案例

3 阅读4分钟

摘要:正则表达式是文本处理的瑞士军刀,但很多人只会基础的匹配。本文通过12个实战案例,带你掌握分组、断言、贪婪/非贪婪、命名捕获等进阶技巧,让你的正则水平上一个台阶。

基础回顾

先快速过一遍常用语法:

import re

# 基础匹配
re.findall(r'\d+', 'price: 99, qty: 5')        # ['99', '5']
re.findall(r'[a-zA-Z]+', 'Hello World 123')     # ['Hello', 'World']
re.sub(r'\s+', ' ', 'too   many   spaces')      # 'too many spaces'

这些大家都会。下面进入正题。

案例1:提取嵌套括号内容

text = '函数调用: func(arg1, func2(arg2, arg3), arg4)'

# 错误做法:贪婪匹配会吃掉太多
re.findall(r'\(.*\)', text)    # ['(arg1, func2(arg2, arg3), arg4)']

# 非贪婪匹配:最短匹配
re.findall(r'\(.*?\)', text)   # ['(arg1, func2(arg2, arg3)', '(arg2, arg3)']

# 正确做法:匹配不含括号的内容
re.findall(r'\([^()]*\)', text)  # ['(arg2, arg3)']

关键点:[^()] 匹配除括号外的任意字符,这样就不会跨越括号边界。

案例2:命名捕获组

提取结构化数据时,命名捕获比数字索引可读性好得多:

log = '2026-02-23 08:15:32 [ERROR] Connection timeout: 192.168.1.100:3306'

pattern = r'(?P<date>\d{4}-\d{2}-\d{2}) (?P<time>\d{2}:\d{2}:\d{2}) \[(?P<level>\w+)\] (?P<message>.+)'
match = re.match(pattern, log)

if match:
    print(match.group('date'))     # 2026-02-23
    print(match.group('level'))    # ERROR
    print(match.group('message'))  # Connection timeout: 192.168.1.100:3306
    print(match.groupdict())       # {'date': '2026-02-23', 'time': '08:15:32', ...}

案例3:零宽断言(Lookahead/Lookbehind)

断言不消耗字符,只做位置判断:

# 前瞻断言:匹配后面跟着"元"的数字
text = '价格99元,数量5个,重量3kg'
re.findall(r'\d+(?=元)', text)    # ['99']

# 否定前瞻:匹配后面不跟"元"的数字
re.findall(r'\d+(?!元)', text)    # ['5', '3']

# 后顾断言:匹配前面是"¥"的数字
text = '商品A ¥199 商品B $299'
re.findall(r'(?<=¥)\d+', text)    # ['199']

# 否定后顾:匹配前面不是"$"的数字
re.findall(r'(?<!\$)\d+', text)   # ['199']

实际应用——提取密码强度验证:

def check_password(pwd):
    """密码必须包含大写、小写、数字,且长度>=8"""
    pattern = r'^(?=.*[A-Z])(?=.*[a-z])(?=.*\d).{8,}$'
    return bool(re.match(pattern, pwd))

check_password('Abc12345')   # True
check_password('abc12345')   # False(缺大写)
check_password('Abcdefgh')   # False(缺数字)

案例4:条件替换

re.sub的替换参数可以是函数:

# 把所有数字翻倍
text = '苹果3个,香蕉5个,橙子12个'
result = re.sub(r'\d+', lambda m: str(int(m.group()) * 2), text)
print(result)  # 苹果6个,香蕉10个,橙子24个

# 敏感词过滤:保留首尾字符
def mask_word(match):
    word = match.group()
    if len(word) <= 2:
        return '*' * len(word)
    return word[0] + '*' * (len(word) - 2) + word[-1]

sensitive = ['暴力', '赌博', '色情内容']
pattern = '|'.join(re.escape(w) for w in sensitive)
text = '这里有暴力和赌博以及色情内容'
print(re.sub(pattern, mask_word, text))
# 这里有暴*和赌*以及色**容

案例5:解析CSV(处理引号内的逗号)

# 简单split会被引号内的逗号干扰
line = '张三,"北京市,朝阳区",25,"爱好:读书,跑步"'

# 正则正确解析
pattern = r',(?=(?:[^"]*"[^"]*")*[^"]*$)'
fields = re.split(pattern, line)
print(fields)
# ['张三', '"北京市,朝阳区"', '25', '"爱好:读书,跑步"']

这个正则的意思是:匹配逗号,但要求逗号后面的引号数量是偶数(说明逗号不在引号内)。

案例6:提取URL的各个部分

url = 'https://user:pass@www.example.com:8080/path/to/page?key=value&foo=bar#section'

pattern = r'''
    ^(?P<scheme>https?)://          # 协议
    (?:(?P<user>[^:@]+)             # 用户名
       :(?P<password>[^@]+)@)?      # 密码
    (?P<host>[^/:]+)                # 主机
    (?::(?P<port>\d+))?             # 端口
    (?P<path>/[^?#]*)?              # 路径
    (?:\?(?P<query>[^#]*))?         # 查询参数
    (?:\#(?P<fragment>.*))?$        # 锚点
'''

match = re.match(pattern, url, re.VERBOSE)
if match:
    for k, v in match.groupdict().items():
        print(f'{k}: {v}')

re.VERBOSE标志允许在正则中加注释和空白,大幅提升可读性。

案例7:中文文本处理

text = '联系方式:手机13812345678,邮箱test@example.com,QQ:123456789'

# 提取手机号
phones = re.findall(r'1[3-9]\d{9}', text)  # ['13812345678']

# 提取邮箱
emails = re.findall(r'[\w.+-]+@[\w-]+\.[\w.]+', text)  # ['test@example.com']

# 提取中文
chinese = re.findall(r'[\u4e00-\u9fff]+', text)  # ['联系方式', '手机', '邮箱']

# 去除所有非中文字符
clean = re.sub(r'[^\u4e00-\u9fff]', '', text)  # '联系方式手机邮箱'

案例8:多行日志解析

log_text = """
[2026-02-23 08:00:01] INFO  Server started on port 8080
[2026-02-23 08:00:15] ERROR Database connection failed
  Traceback: ConnectionRefusedError
  Host: 192.168.1.100
  Port: 3306
[2026-02-23 08:00:16] INFO  Retrying connection...
"""

# 匹配完整的日志条目(包括多行堆栈)
pattern = r'\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\] (\w+)\s+(.+?)(?=\n\[|\Z)'
entries = re.findall(pattern, log_text, re.DOTALL)

for time, level, msg in entries:
    print(f'[{level}] {time}: {msg.strip()[:50]}')

案例9:HTML标签处理

html = '<div class="content"><p>Hello <b>World</b></p></div>'

# 去除所有HTML标签
clean = re.sub(r'<[^>]+>', '', html)  # 'Hello World'

# 提取所有链接
html2 = '<a href="https://a.com">Link1</a> <a href="https://b.com">Link2</a>'
links = re.findall(r'href="([^"]+)"', html2)  # ['https://a.com', 'https://b.com']

# 提取带文本的链接
link_pairs = re.findall(r'<a[^>]+href="([^"]+)"[^>]*>([^<]+)</a>', html2)
# [('https://a.com', 'Link1'), ('https://b.com', 'Link2')]

案例10:数字格式化

# 千分位分隔
def add_commas(n):
    return re.sub(r'(\d)(?=(\d{3})+(?!\d))', r'\1,', str(n))

add_commas(1234567890)  # '1,234,567,890'

# 金额提取和标准化
text = '总价¥1,234.56元,优惠¥100元,实付1134.56'
amounts = re.findall(r'[¥¥]?([\d,]+\.?\d*)', text)
# ['1,234.56', '100', '1134.56']

性能优化

# 预编译:如果同一个正则要用多次
pattern = re.compile(r'\d{4}-\d{2}-\d{2}')
for line in huge_file:
    match = pattern.search(line)  # 比re.search()快

# 非捕获组:不需要提取的分组用(?:...)
# 慢:每个()都会创建捕获组
re.findall(r'(https?://)(\w+)(\.com)', text)
# 快:只捕获需要的部分
re.findall(r'(?:https?://)(\w+)(?:\.com)', text)

总结

正则表达式的进阶技巧:

  • 命名捕获组让代码更可读
  • 零宽断言解决"匹配但不消耗"的需求
  • re.VERBOSE让复杂正则可维护
  • re.sub配合函数实现条件替换
  • 预编译提升性能

正则不是万能的(别用它解析HTML/JSON),但在文本处理场景下,它依然是最锋利的刀。