第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. 正向后顾断言
正向后顾断言 (?<=...) 检查当前位置前面是否匹配指定的模式。
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. 负向后顾断言
负向后顾断言 (?<!...) 检查当前位置前面是否不匹配指定的模式。
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个橘子', 替换次数: 23. 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+'七、总结
本集我们深入学习了正则表达式的模式匹配:
- 捕获组与非捕获组:用于提取匹配的部分内容
- 命名捕获组:使用名称引用捕获的内容,提高可读性
- 断言:包括前瞻断言和后顾断言,用于条件匹配
- 正则表达式标志:如IGNORECASE、MULTILINE、DOTALL等,修改匹配行为
- 匹配对象详解:包含丰富的属性和方法,用于获取匹配信息
- re模块高级函数:如finditer、subn等
- 复杂模式匹配案例:HTML解析、密码验证、JSON提取等
正则表达式是一个强大的工具,但也需要谨慎使用。建议在编写复杂正则表达式时:
- 使用在线工具(如regex101.com)进行测试
- 添加适当的注释(使用re.VERBOSE)
- 考虑性能问题,避免过度使用复杂模式
- 对于非常复杂的文本处理,考虑使用专门的解析库
下一集我们将学习re模块的替换与分割功能(第99集)。
八、课后练习
- 编写一个正则表达式,匹配HTML中的所有链接(a标签的href属性)
- 使用正则表达式验证身份证号码(18位,最后一位可能是X)
- 编写一个函数,使用正则表达式将驼峰命名法转换为下划线命名法
- 使用正则表达式从日志文件中提取所有的错误信息
- 编写一个正则表达式,匹配有效的IPv4地址(严格验证每个部分的范围是0-255)