第98集:re模块:模式匹配

学习目标

  • 深入理解正则表达式的捕获组和非捕获组
  • 掌握前瞻断言和后顾断言的使用方法
  • 学习正则表达式的各种标志(flags)
  • 熟练使用匹配对象(Match Object)的属性和方法
  • 能够编写复杂的模式匹配正则表达式

一、捕获组与非捕获组

1. 捕获组

捕获组是将正则表达式的一部分括在括号 () 中,以便在匹配后能够提取这部分内容。

import re

# 捕获日期的年、月、日
pattern = r'(\d{4})-(\d{2})-(\d{2})'
text = '今天是2023-10-05'
result = re.search(pattern, text)

if result:
    print(f'完整日期: {result.group(0)}')  # 完整匹配
    print(f'年份: {result.group(1)}')     # 第一个捕获组
    print(f'月份: {result.group(2)}')     # 第二个捕获组
    print(f'日期: {result.group(3)}')     # 第三个捕获组
    print(f'所有捕获组: {result.groups()}')  # 所有捕获组的元组

2. 非捕获组

有时我们需要将多个字符作为一个整体处理,但不需要捕获这部分内容,这时可以使用非捕获组 (?:...)

import re

# 非捕获组示例
pattern = r'(?:https?://)?(?:www\.)?example\.com'
texts = ['example.com', 'www.example.com', 'http://example.com', 'https://www.example.com']

for text in texts:
    result = re.match(pattern, text)
    print(f'模式在 "{text}" 中匹配: {result.group() if result else "None"}')

3. 命名捕获组

为了提高代码的可读性,我们可以为捕获组命名,使用语法 (?P<name>...)

import re

# 命名捕获组
pattern = r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})'
text = '2023-10-05'
result = re.search(pattern, text)

if result:
    print(f'完整日期: {result.group(0)}')
    print(f'年份: {result.group("year")}')
    print(f'月份: {result.group("month")}')
    print(f'日期: {result.group("day")}')
    print(f'所有命名组: {result.groupdict()}')  # 返回命名组的字典

二、前瞻断言与后顾断言

断言(Assertions)用于检查某个位置的前后是否满足特定条件,但不会消耗字符。

1. 正向前瞻断言

正向前瞻断言 (?=...) 检查当前位置后面是否匹配指定的模式。

import re

# 匹配后面跟着 "@example.com" 的用户名
pattern = r'\w+(?=@example.com)'
text = 'user1@example.com, user2@test.com, user3@example.com'

results = re.findall(pattern, text)
print(f'匹配到的用户名: {results}')  # ['user1', 'user3']

2. 负向前瞻断言

负向前瞻断言 (?!...) 检查当前位置后面是否匹配指定的模式。

import re

# 匹配后面不是 "@example.com" 的用户名
pattern = r'\w+(?!@example.com)@\w+\.\w+'
text = 'user1@example.com, user2@test.com, user3@example.com'

results = re.findall(pattern, text)
print(f'匹配到的邮箱: {results}')  # ['user2@test.com']

3. 正向后顾断言

正向后顾断言 (?&lt;=...) 检查当前位置前面是否匹配指定的模式。

import re

# 匹配前面是 "https://" 的域名
pattern = r'(?<=https://)\w+\.\w+'
text = '访问 http://example.com 或 https://test.org 获取信息'

results = re.findall(pattern, text)
print(f'匹配到的域名: {results}')  # ['test.org']

4. 负向后顾断言

负向后顾断言 (?&lt;!...) 检查当前位置前面是否匹配指定的模式。

import re

# 匹配前面不是 "https://" 的域名
pattern = r'(?<!https://)(?:http://)?\w+\.\w+'
text = '访问 http://example.com 或 https://test.org 获取信息'

results = re.findall(pattern, text)
print(f'匹配到的URL: {results}')  # ['http://example.com']

三、正则表达式的标志(Flags)

正则表达式的标志用于修改匹配的行为,re模块提供了多种标志:

标志 含义 简写
re.IGNORECASE 忽略大小写 re.I
re.MULTILINE 多行模式,^和$匹配每行的开头和结尾 re.M
re.DOTALL 点号匹配包括换行符在内的所有字符 re.S
re.VERBOSE 忽略空格和注释,提高可读性 re.X
re.ASCII 使\w, \d等仅匹配ASCII字符 re.A
re.LOCALE 使\w, \b等依赖于当前区域设置 re.L

1. re.IGNORECASE (re.I)

忽略大小写进行匹配:

import re

pattern = r'hello'
text = 'Hello, HELLO, hello!'

# 不使用标志
results1 = re.findall(pattern, text)
print(f'不忽略大小写: {results1}')  # ['hello']

# 使用忽略大小写标志
results2 = re.findall(pattern, text, re.IGNORECASE)
print(f'忽略大小写: {results2}')  # ['Hello', 'HELLO', 'hello']

2. re.MULTILINE (re.M)

多行模式,使^和$匹配每行的开头和结尾:

import re

pattern = r'^\w+'
text = '''
Line 1: First line
Line 2: Second line
Line 3: Third line
'''

# 不使用多行模式
results1 = re.findall(pattern, text)
print(f'单行模式: {results1}')  # [] 因为整个字符串开头是换行符

# 使用多行模式
results2 = re.findall(pattern, text, re.MULTILINE)
print(f'多行模式: {results2}')  # ['Line', 'Line', 'Line']

3. re.DOTALL (re.S)

使点号匹配包括换行符在内的所有字符:

import re

pattern = r'a.*b'
text = 'a\n\nb'

# 不使用DOTALL
results1 = re.findall(pattern, text)
print(f'不使用DOTALL: {results1}')  # []

# 使用DOTALL
results2 = re.findall(pattern, text, re.DOTALL)
print(f'使用DOTALL: {results2}')  # ['a\n\nb']

4. re.VERBOSE (re.X)

忽略空格和注释,提高正则表达式的可读性:

import re

# 使用VERBOSE标志的正则表达式
pattern = re.compile(r'''
    (?P<year>\d{4})  # 年份
    -                # 分隔符
    (?P<month>\d{2}) # 月份
    -                # 分隔符
    (?P<day>\d{2})   # 日期
''', re.VERBOSE)

text = '2023-10-05'
result = pattern.match(text)
print(f'日期: {result.groupdict()}')  # {'year': '2023', 'month': '10', 'day': '05'}

5. 组合使用标志

可以使用 | 运算符组合多个标志:

import re

pattern = r'^hello.*world$'
text = '''
Hello
This is a test
World
'''

# 组合使用IGNORECASE和MULTILINE
result = re.search(pattern, text, re.IGNORECASE | re.MULTILINE)
print(f'组合标志匹配: {result.group() if result else "None"}')  # 'Hello' (仅匹配开头)

# 组合使用IGNORECASE、MULTILINE和DOTALL
result = re.search(pattern, text, re.IGNORECASE | re.MULTILINE | re.DOTALL)
print(f'组合标志匹配(含DOTALL): {result.group() if result else "None"}')  # 匹配整个字符串

四、匹配对象(Match Object)详解

当re模块的函数匹配成功时,会返回一个匹配对象(Match Object),它包含了丰富的属性和方法:

1. 常用属性

属性 描述
group() / group(0) 返回完整的匹配字符串
group(n) 返回第n个捕获组的内容
group(name) 返回命名捕获组name的内容
groups() 返回所有捕获组的元组
groupdict() 返回命名捕获组的字典
start() / start(n) 返回匹配开始的位置 / 第n个捕获组开始的位置
end() / end(n) 返回匹配结束的位置 / 第n个捕获组结束的位置
span() / span(n) 返回匹配的起止位置元组 / 第n个捕获组的起止位置元组

2. 常用方法

方法 描述
expand(template) 使用匹配的内容替换模板中的\n或\g引用
re 返回匹配使用的正则表达式对象
string 返回匹配的原始字符串

3. 示例

import re

pattern = r'(?P<first>\w+) (?P<last>\w+)'  # 匹配名字和姓氏
text = 'Hello, my name is John Doe.'

result = re.search(pattern, text)

if result:
    # 基本属性
    print(f'完整匹配: {result.group()}')
    print(f'匹配位置: {result.span()}')
    print(f'匹配开始位置: {result.start()}')
    print(f'匹配结束位置: {result.end()}')
    
    # 捕获组
    print(f'第一个捕获组: {result.group(1)}')
    print(f'第二个捕获组: {result.group(2)}')
    print(f'所有捕获组: {result.groups()}')
    
    # 命名捕获组
    print(f'名字: {result.group("first")}')
    print(f'姓氏: {result.group("last")}')
    print(f'命名组字典: {result.groupdict()}')
    
    # 捕获组的位置
    print(f'名字的位置: {result.span("first")}')
    print(f'姓氏的位置: {result.span("last")}')
    
    # expand方法
    print(f'格式化输出: {result.expand("姓氏: \g<last>, 名字: \g<first>")}')
    
    # 其他属性
    print(f'使用的正则表达式: {result.re}')
    print(f'原始字符串: {result.string}')

五、re模块的高级函数

1. re.finditer()

返回一个迭代器,包含所有匹配的Match对象:

import re

pattern = r'\d+'
text = '有123个苹果和456个橘子'

# 使用findall
print(f're.findall: {re.findall(pattern, text)}')  # ['123', '456']

# 使用finditer
print('re.finditer:')
for match in re.finditer(pattern, text):
    print(f'  匹配: {match.group()}, 位置: {match.span()}')

2. re.subn()

与re.sub()类似,但返回一个元组,包含替换后的字符串和替换次数:

import re

pattern = r'\d+'
text = '有123个苹果和456个橘子'

# 使用re.sub
new_text = re.sub(pattern, 'X', text)
print(f're.sub: {new_text}')  # '有X个苹果和X个橘子'

# 使用re.subn
new_text, count = re.subn(pattern, 'X', text)
print(f're.subn: {new_text}, 替换次数: {count}')  # '有X个苹果和X个橘子', 替换次数: 2

3. re.split()的高级用法

使用捕获组保留分割符:

import re

pattern = r'(\s*,\s*)'  # 匹配逗号及其前后的空格
text = 'apple, banana,  orange,grape'

# 不使用捕获组
result1 = re.split(pattern, text)
print(f'不保留分割符: {result1}')  # ['apple', 'banana', 'orange', 'grape']

# 使用捕获组
result2 = re.split(r'\s*(,\s*)', text)
print(f'保留分割符: {result2}')  # ['apple', ', ', 'banana', ',  ', 'orange', ',', 'grape']

# 重新组合字符串
recombined = ''.join(result2)
print(f'重新组合: {recombined}')  # 'apple, banana,  orange,grape'

六、复杂模式匹配案例

1. 解析HTML标签

import re

def parse_html_tags(html):
    """解析HTML标签,返回标签名和属性"""
    # 匹配HTML标签的正则表达式
    pattern = r'<(?P<tag>\w+)(?P<attrs>.*?)>'
    results = []
    
    for match in re.finditer(pattern, html):
        tag = match.group('tag')
        attrs_str = match.group('attrs')
        attrs = {}
        
        # 解析属性
        attr_pattern = r'\s+(?P<key>\w+)=(?:"(?P<value1>[^"]*)"|\'(?P<value2>[^\']*)\'|(?P<value3>[^\s>]*))'
        for attr_match in re.finditer(attr_pattern, attrs_str):
            key = attr_match.group('key')
            # 获取属性值(可能在双引号、单引号或没有引号中)
            value = attr_match.group('value1') or attr_match.group('value2') or attr_match.group('value3')
            attrs[key] = value
        
        results.append((tag, attrs))
    
    return results

# 测试
html = '<div class="container" id="main"><img src="image.jpg" alt="示例图片" /><a href="https://example.com">链接</a></div>'
tags = parse_html_tags(html)
print("解析HTML标签:")
for tag, attrs in tags:
    print(f'标签: <{tag}>')
    if attrs:
        print(f'  属性: {attrs}')

2. 验证密码强度

import re

def check_password_strength(password):
    """检查密码强度
    返回:
    - 弱:长度小于8位
    - 中:长度8位以上,包含字母和数字
    - 强:长度8位以上,包含大小写字母、数字和特殊字符
    """
    if len(password) < 8:
        return "弱"
    
    # 检查是否包含字母和数字
    has_letters = bool(re.search(r'[a-zA-Z]', password))
    has_digits = bool(re.search(r'\d', password))
    
    # 检查是否包含大小写字母和特殊字符
    has_upper = bool(re.search(r'[A-Z]', password))
    has_lower = bool(re.search(r'[a-z]', password))
    has_special = bool(re.search(r'[^a-zA-Z0-9]', password))
    
    if has_letters and has_digits:
        if has_upper and has_lower and has_special:
            return "强"
        else:
            return "中"
    
    return "弱"

# 测试
passwords = ['123456', 'abc123', 'Abc12345', 'Abc123!@#']
for pwd in passwords:
    strength = check_password_strength(pwd)
    print(f'密码 "{pwd}" 的强度: {strength}')

3. 提取JSON格式中的值

import re

def extract_json_values(json_str, key):
    """从JSON字符串中提取指定键的值(简单实现)"""
    # 简单的JSON键值对匹配(不处理嵌套)
    pattern = rf'"{key}"\s*:\s*(?:"(?P<string>[^"]*)"|(?P<number>\d+(?:\.\d+)?))'
    results = []
    
    for match in re.finditer(pattern, json_str):
        if match.group('string'):
            results.append(match.group('string'))
        elif match.group('number'):
            results.append(float(match.group('number')) if '.' in match.group('number') else int(match.group('number')))
    
    return results

# 测试
json_str = '''
{
  "name": "John",
  "age": 30,
  "city": "New York",
  "salary": 5000.50,
  "hobbies": ["reading", "swimming", "coding"]
}
'''

# 提取不同类型的值
name = extract_json_values(json_str, 'name')
age = extract_json_values(json_str, 'age')
salary = extract_json_values(json_str, 'salary')

print(f'姓名: {name}')    # ['John']
print(f'年龄: {age}')     # [30]
print(f'薪水: {salary}')   # [5000.5]

六、最佳实践与性能优化

1. 编译频繁使用的正则表达式

对于频繁使用的正则表达式,应该使用re.compile()编译,以提高性能:

import re

# 编译正则表达式(只需要执行一次)
email_pattern = re.compile(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$')

# 多次使用编译后的模式
emails = ['user1@example.com', 'user2@test.com', 'invalid-email']
for email in emails:
    if email_pattern.match(email):
        print(f'{email} 是有效的邮箱')
    else:
        print(f'{email} 是无效的邮箱')

2. 避免过度使用正则表达式

对于简单的字符串操作,应优先使用字符串方法,它们通常更快且更易读:

# 好:使用字符串方法检查前缀
text = 'hello world'
if text.startswith('hello'):
    print('以hello开头')

# 不好:使用正则表达式检查前缀
if re.match(r'^hello', text):
    print('以hello开头')

3. 注意回溯问题

复杂的正则表达式可能导致回溯问题,影响性能。应尽量保持正则表达式简单:

# 可能导致回溯的模式
pattern = r'(a+)+b'  # 避免这种嵌套的量词

# 更高效的模式
pattern = r'a+b'     # 简单直接的模式

4. 使用原始字符串

始终使用原始字符串(r'')来定义正则表达式,避免转义问题:

# 推荐
pattern = r'\d+'

# 不推荐
pattern = '\\d+'

七、总结

本集我们深入学习了正则表达式的模式匹配:

  1. 捕获组与非捕获组:用于提取匹配的部分内容
  2. 命名捕获组:使用名称引用捕获的内容,提高可读性
  3. 断言:包括前瞻断言和后顾断言,用于条件匹配
  4. 正则表达式标志:如IGNORECASE、MULTILINE、DOTALL等,修改匹配行为
  5. 匹配对象详解:包含丰富的属性和方法,用于获取匹配信息
  6. re模块高级函数:如finditer、subn等
  7. 复杂模式匹配案例:HTML解析、密码验证、JSON提取等

正则表达式是一个强大的工具,但也需要谨慎使用。建议在编写复杂正则表达式时:

  • 使用在线工具(如regex101.com)进行测试
  • 添加适当的注释(使用re.VERBOSE)
  • 考虑性能问题,避免过度使用复杂模式
  • 对于非常复杂的文本处理,考虑使用专门的解析库

下一集我们将学习re模块的替换与分割功能(第99集)。

八、课后练习

  1. 编写一个正则表达式,匹配HTML中的所有链接(a标签的href属性)
  2. 使用正则表达式验证身份证号码(18位,最后一位可能是X)
  3. 编写一个函数,使用正则表达式将驼峰命名法转换为下划线命名法
  4. 使用正则表达式从日志文件中提取所有的错误信息
  5. 编写一个正则表达式,匹配有效的IPv4地址(严格验证每个部分的范围是0-255)
« 上一篇 re模块:正则表达式基础 下一篇 » re模块:替换与分割