前置知识: Python

正则表达式

2 minIntermediate2026/6/13

Python正则表达式re模块详解:模式语法、匹配方法、分组、替换与实战应用。

1. re 模块基础

1.1 基本匹配

import re

# re.search: 搜索第一个匹配(返回Match对象或None)
match = re.search(r'\d+', 'abc123def')
if match:
    print(f"匹配: {match.group()}")     # 123
    print(f"位置: {match.start()}-{match.end()}")  # 3-6

# re.match: 从字符串开头匹配
match = re.match(r'Hello', 'Hello World')
print(match.group() if match else "No match")  # Hello

match = re.match(r'World', 'Hello World')
print(match)  # None(不在开头)

# re.fullmatch: 完全匹配整个字符串
match = re.fullmatch(r'\d{3}', '123')
print(bool(match))  # True

match = re.fullmatch(r'\d{3}', '1234')
print(bool(match))  # False

# re.findall: 找到所有匹配,返回列表
numbers = re.findall(r'\d+', 'a1b22c333')
print(numbers)  # ['1', '22', '333']

# re.finditer: 找到所有匹配,返回迭代器
for m in re.finditer(r'\d+', 'a1b22c333'):
    print(f"Match: {m.group()} at {m.start()}")
# Match: 1 at 1
# Match: 22 at 3
# Match: 333 at 6

1.2 替换与分割

import re

# re.sub: 替换
result = re.sub(r'\d+', 'NUM', 'a1b22c333')
print(result)  # aNUMbNUMcNUM

# 使用回调函数替换
result = re.sub(r'\d+', lambda m: str(int(m.group()) * 2), 'a1b22c333')
print(result)  # a2b44c666

# 限制替换次数
result = re.sub(r'\d+', 'NUM', 'a1b22c333', count=2)
print(result)  # aNUMbNUMc333

# re.split: 分割
parts = re.split(r'[,;|]', 'one,two;three|four')
print(parts)  # ['one', 'two', 'three', 'four']

# 保留分隔符(使用捕获组)
parts = re.split(r'([,;|])', 'one,two;three|four')
print(parts)  # ['one', ',', 'two', ';', 'three', '|', 'four']

2. 模式语法

2.1 字符与量词

import re

# 字符类
print(re.findall(r'[aeiou]', 'Hello World'))      # ['e', 'o', 'o']
print(re.findall(r'[^aeiou\s]', 'Hello World'))   # ['H', 'l', 'l', 'W', 'r', 'l', 'd']

# 预定义字符类
# \d  数字    \D  非数字
# \w  单词字符 \W  非单词字符
# \s  空白    \S  非空白
# \b  单词边界

# 量词
print(re.findall(r'a{2,4}', 'a aa aaa aaaa aaaaa'))
# ['aa', 'aaa', 'aaaa', 'aaaa']

# 贪婪 vs 非贪婪
html = '<div>content1</div><div>content2</div>'
greedy = re.findall(r'<div>.*</div>', html)
print(greedy)  # ['<div>content1</div><div>content2</div>'](贪婪)

non_greedy = re.findall(r'<div>.*?</div>', html)
print(non_greedy)  # ['<div>content1</div>', '<div>content2</div>'](非贪婪)

2.2 分组与捕获

import re

# 基本分组
match = re.search(r'(\d{4})-(\d{2})-(\d{2})', 'Date: 2026-06-13')
if match:
    print(match.group())    # 2026-06-13(完整匹配)
    print(match.group(1))   # 2026(第一个分组)
    print(match.group(2))   # 06
    print(match.group(3))   # 13
    print(match.groups())   # ('2026', '06', '13')

# 命名分组
match = re.search(r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})', '2026-06-13')
if match:
    print(match.group('year'))   # 2026
    print(match.group('month'))  # 06
    print(match.groupdict())     # {'year': '2026', 'month': '06', 'day': '13'}

# 非捕获分组
result = re.findall(r'(?:https?://)?(\w+\.\w+)', 'Visit example.com or https://google.com')
print(result)  # ['example.com', 'google.com']

# 前瞻断言
# (?=...)  正向前瞻:后面匹配
# (?!...)  负向前瞻:后面不匹配
prices = re.findall(r'\d+(?=)', '苹果5元,香蕉3元,橙子4斤')
print(prices)  # ['5', '3']

# 后顾断言(Python支持)
names = re.findall(r'(?<=用户:)\w+', '用户:Alice,用户:Bob')
print(names)  # ['Alice', 'Bob']

3. 编译标志

import re

# re.IGNORECASE (re.I): 忽略大小写
print(re.findall(r'hello', 'Hello HELLO hello', re.I))
# ['Hello', 'HELLO', 'hello']

# re.MULTILINE (re.M): 多行模式(^和$匹配行首行尾)
text = "Line 1\nLine 2\nLine 3"
print(re.findall(r'^Line', text, re.M))  # ['Line', 'Line', 'Line']

# re.DOTALL (re.S): . 匹配换行符
text = "Start\nMiddle\nEnd"
print(re.findall(r'Start.*End', text, re.S))  # ['Start\nMiddle\nEnd']

# re.VERBOSE (re.X): 允许注释和空白
pattern = re.compile(r"""
    ^                   # 字符串开头
    (?P<protocol>\w+)   # 协议名
    ://                 # 分隔符
    (?P<host>[\w.]+)    # 主机名
    (?::(?P<port>\d+))? # 可选端口
    (?P<path>/.*)?      # 可选路径
    $                   # 字符串结尾
""", re.VERBOSE)

match = pattern.match('https://example.com:8080/path')
if match:
    print(match.groupdict())
    # {'protocol': 'https', 'host': 'example.com', 'port': '8080', 'path': '/path'}

4. 预编译正则

import re

# 编译正则表达式(提升重复使用的性能)
email_pattern = re.compile(
    r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
)

# 使用编译后的对象
emails = ['user@example.com', 'invalid-email', 'test@test.org']
valid_emails = [e for e in emails if email_pattern.match(e)]
print(valid_emails)  # ['user@example.com', 'test@test.org']

# 编译时指定标志
phone_pattern = re.compile(r'^1[3-9]\d{9}$')
print(phone_pattern.match('13800138000'))  # Match

5. 实战应用

5.1 数据提取与清洗

import re

# 提取URL
text = 'Visit https://example.com and http://test.org for info.'
urls = re.findall(r'https?://[^\s<>"\']+', text)
print(urls)  # ['https://example.com', 'http://test.org']

# 清理HTML标签
html = '<p>Hello <b>World</b></p>'
clean = re.sub(r'<[^>]+>', '', html)
print(clean)  # Hello World

# 提取中文
text = 'Hello世界,Python编程123'
chinese = re.findall(r'[\u4e00-\u9fa5]+', text)
print(chinese)  # ['世界', '编程']

# 千分位格式化
def format_number(n):
    return re.sub(r'(?<=\d)(?=(\d{3})+$)', ',', str(n))

print(format_number(1234567890))  # 1,234,567,890

# 驼峰转下划线
def camel_to_snake(name):
    s1 = re.sub(r'(.)([A-Z][a-z]+)', r'\1_\2', name)
    return re.sub(r'([a-z0-9])([A-Z])', r'\1_\2', s1).lower()

print(camel_to_snake('myVariableName'))  # my_variable_name

# 密码强度检测
def check_password_strength(password):
    checks = {
        'length': len(password) >= 8,
        'lower': bool(re.search(r'[a-z]', password)),
        'upper': bool(re.search(r'[A-Z]', password)),
        'digit': bool(re.search(r'\d', password)),
        'special': bool(re.search(r'[!@#$%^&*(),.?":{}|<>]', password)),
    }
    score = sum(checks.values())
    return score, checks

score, details = check_password_strength('MyP@ss123')
print(f"强度分数: {score}/5, 详情: {details}")

5.2 日志解析

import re
from collections import defaultdict

# Apache日志格式解析
log_pattern = re.compile(
    r'(?P<ip>\d+\.\d+\.\d+\.\d+)'
    r' - - '
    r'\[(?P<datetime>[^\]]+)\]'
    r' "(?P<method>\w+) (?P<path>\S+) HTTP/\d\.\d"'
    r' (?P<status>\d{3})'
    r' (?P<size>\d+)'
)

def parse_log_file(filepath):
    ip_counts = defaultdict(int)
    status_counts = defaultdict(int)

    with open(filepath, 'r') as f:
        for line in f:
            match = log_pattern.match(line)
            if match:
                ip_counts[match.group('ip')] += 1
                status_counts[match.group('status')] += 1

    return dict(ip_counts), dict(status_counts)

# 示例日志行
log_line = '192.168.1.1 - - [13/Jun/2026:14:30:00 +0800] "GET /api/users HTTP/1.1" 200 1234'
match = log_pattern.match(log_line)
if match:
    print(match.groupdict())

6. 常见问题与解决方案

6.1 正则中的反斜杠

# Python原始字符串(r前缀)避免双重转义
# 匹配反斜杠
print(re.findall(r'\\', r'a\b\c'))  # ['\\', '\\']

# 不用r前缀需要双重转义
print(re.findall('\\\\', 'a\\b\\c'))  # ['\\', '\\']

# 始终使用r前缀!

6.2 性能优化

import re

# 1. 预编译常用正则
EMAIL_RE = re.compile(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$')

# 2. 避免灾难性回溯
# 危险:嵌套量词
# bad = re.compile(r'(a+)+b')

# 3. 使用非捕获分组(稍微更快)
# (?:pattern) 代替 (pattern) 当不需要捕获时

# 4. 尽早使用锚定和具体字符
# ^\d{4}-\d{2}-\d{2}$ 比 .*日期.* 更高效

7. 总结与最佳实践

7.1 方法选择

需求方法返回值
检查是否匹配re.search / re.matchMatch对象或None
完全匹配re.fullmatchMatch对象或None
找所有匹配re.findall列表
替换re.sub新字符串
分割re.split列表

7.2 最佳实践

  1. 使用原始字符串r'pattern' 避免转义问题
  2. 预编译正则:重复使用的正则用 re.compile
  3. 使用命名分组(?P<name>...) 提高可读性
  4. VERBOSE模式:复杂正则用注释说明
  5. 非贪婪默认:需要精确匹配时用 *? +?
  6. 测试正则:使用 regex101.com 等工具验证