Python re
模块高级用法:精通正则表达式的艺术
各位同学,大家好!今天我们深入探讨 Python re
模块的高级用法。正则表达式(Regular Expression,简称 regex)是一种强大的文本处理工具,它允许我们通过模式匹配来搜索、替换和提取字符串。re
模块是 Python 中用于处理正则表达式的标准库,掌握其高级用法能极大地提升文本处理的效率和灵活性。
1. 回顾基础:re
模块核心函数
在深入高级用法之前,我们先快速回顾 re
模块中一些核心函数:
re.compile(pattern, flags=0)
: 编译正则表达式模式,返回一个RegexObject
对象,提高效率。re.search(pattern, string, flags=0)
: 在字符串中搜索匹配项,返回第一个匹配的MatchObject
对象,如果没有匹配,返回None
。re.match(pattern, string, flags=0)
: 从字符串的开头开始匹配,返回一个MatchObject
对象,如果没有匹配,返回None
。re.fullmatch(pattern, string, flags=0)
: 匹配整个字符串,返回一个MatchObject
对象,如果没有匹配,返回None
。re.split(pattern, string, maxsplit=0, flags=0)
: 根据模式拆分字符串,返回一个字符串列表。re.findall(pattern, string, flags=0)
: 找到字符串中所有匹配的子字符串,返回一个列表。re.finditer(pattern, string, flags=0)
: 找到字符串中所有匹配的子字符串,返回一个迭代器,每个元素都是一个MatchObject
对象。re.sub(pattern, repl, string, count=0, flags=0)
: 替换字符串中的匹配项,返回替换后的字符串。re.subn(pattern, repl, string, count=0, flags=0)
: 替换字符串中的匹配项,返回一个元组,包含替换后的字符串和替换次数。
这些函数是使用正则表达式的基础,我们将在后续的高级用法中频繁使用它们。
2. 高级特性:分组、命名分组与反向引用
正则表达式的一个强大特性是分组。分组允许我们将模式的一部分括起来,以便后续引用。
2.1 分组 (Grouping)
使用括号 ()
来创建分组。每个分组都会被分配一个编号,从 1 开始,从左到右依次递增。
import re
text = "My phone number is 123-456-7890 and his phone number is 987-654-3210."
pattern = r"(d{3})-(d{3})-(d{4})"
match = re.search(pattern, text)
if match:
print("整个匹配:", match.group(0))
print("第一个分组:", match.group(1))
print("第二个分组:", match.group(2))
print("第三个分组:", match.group(3))
print("所有分组:", match.groups())
输出:
整个匹配: 123-456-7890
第一个分组: 123
第二个分组: 456
第三个分组: 7890
所有分组: ('123', '456', '7890')
match.group(0)
返回整个匹配的字符串。 match.group(n)
返回第 n 个分组匹配的字符串。 match.groups()
返回一个包含所有分组匹配字符串的元组。
2.2 命名分组 (Named Grouping)
除了使用数字索引来引用分组,我们还可以给分组命名,使得代码更易读。
使用 (?P<name>...)
语法来命名分组。
import re
text = "My phone number is 123-456-7890 and his phone number is 987-654-3210."
pattern = r"(?P<area>d{3})-(?P<prefix>d{3})-(?P<number>d{4})"
match = re.search(pattern, text)
if match:
print("区号:", match.group("area"))
print("前缀:", match.group("prefix"))
print("号码:", match.group("number"))
print("所有分组:", match.groupdict())
输出:
区号: 123
前缀: 456
号码: 7890
所有分组: {'area': '123', 'prefix': '456', 'number': '7890'}
match.group("name")
返回名为 "name" 的分组匹配的字符串。 match.groupdict()
返回一个字典,包含所有命名分组的名称和匹配字符串。
2.3 反向引用 (Backreferences)
反向引用允许我们在正则表达式中引用之前匹配的分组。这在匹配重复模式时非常有用。
使用 n
来引用第 n 个分组。
import re
text = "<h1>Title</h1><h2>Title</h2><h3>Title</h3>"
pattern = r"<([hH][1-6])>(.*?)</1>" # 1 引用了第一个分组 ([hH][1-6])
matches = re.findall(pattern, text)
print(matches)
输出:
[('h1', 'Title')]
在这个例子中,1
引用了第一个分组 ([hH][1-6])
匹配的内容。这意味着结束标签必须与开始标签相同。
3. 高级搜索与替换:sub
函数的精妙运用
re.sub
函数是进行字符串替换的核心工具。除了简单的字符串替换,它还支持使用函数进行动态替换。
3.1 使用函数进行替换
re.sub
的第二个参数可以是一个函数。该函数接受一个 MatchObject
对象作为参数,并返回一个字符串,用于替换匹配的文本。
import re
def replace_with_upper(match):
"""将匹配的文本转换为大写"""
return match.group(0).upper()
text = "hello world"
pattern = r"w+"
new_text = re.sub(pattern, replace_with_upper, text)
print(new_text)
输出:
HELLO WORLD
在这个例子中,replace_with_upper
函数将每个单词转换为大写。
3.2 复杂替换的场景
假设我们需要将 HTML 代码中的所有链接地址提取出来,并用新的链接地址替换。
import re
def replace_url(match):
"""替换 URL 地址"""
old_url = match.group(1)
new_url = "https://www.example.com/" + old_url.split("/")[-1] # 假设新的URL是基于旧URL的文件名
return f'<a href="{new_url}">{match.group(2)}</a>'
html = '<a href="http://www.old-domain.com/page1.html">Link1</a><a href="http://www.old-domain.com/page2.html">Link2</a>'
pattern = r'<a href="(.*?)">(.*?)</a>'
new_html = re.sub(pattern, replace_url, html)
print(new_html)
输出:
<a href="https://www.example.com/page1.html">Link1</a><a href="https://www.example.com/page2.html">Link2</a>
在这个例子中,replace_url
函数接收一个 MatchObject
对象,提取旧的 URL 和链接文本,然后构造新的 URL,并返回包含新 URL 的 HTML 代码。
3.3 使用 subn
获取替换次数
re.subn
函数与 re.sub
类似,但它返回一个元组,包含替换后的字符串和替换次数。
import re
text = "apple banana apple orange apple"
pattern = r"apple"
new_text, count = re.subn(pattern, "pear", text)
print("替换后的字符串:", new_text)
print("替换次数:", count)
输出:
替换后的字符串: pear banana pear orange pear
替换次数: 3
4. 正则表达式标志 (Flags):增强匹配的灵活性
正则表达式标志允许我们修改正则表达式的匹配行为。
标志 | 简写 | 描述 |
---|---|---|
re.IGNORECASE |
re.I |
使匹配对大小写不敏感。 |
re.MULTILINE |
re.M |
使 ^ 和 $ 匹配每一行的开头和结尾,而不是整个字符串的开头和结尾。 |
re.DOTALL |
re.S |
使 . 匹配任何字符,包括换行符。 |
re.ASCII |
re.A |
使 w , b , s 等特殊字符只匹配 ASCII 字符,而不是 Unicode 字符。 |
re.VERBOSE |
re.X |
允许在正则表达式中使用空白字符和注释,以提高可读性。 |
4.1 re.IGNORECASE
(忽略大小写)
import re
text = "Hello World"
pattern = r"hello"
match = re.search(pattern, text) # None
match_ignorecase = re.search(pattern, text, re.IGNORECASE) # re.I 也可以
if match_ignorecase:
print("忽略大小写匹配:", match_ignorecase.group(0))
输出:
忽略大小写匹配: Hello
4.2 re.MULTILINE
(多行匹配)
import re
text = """line1
line2
line3"""
pattern = r"^lined$"
match = re.search(pattern, text) # None
match_multiline = re.search(pattern, text, re.MULTILINE) # re.M 也可以
if match_multiline:
print("多行匹配:", match_multiline.group(0)) #只匹配到line1
pattern_all = r"^lined$"
matches = re.findall(pattern_all, text, re.MULTILINE)
print(matches) #['line1', 'line2', 'line3']
输出:
多行匹配: line1
['line1', 'line2', 'line3']
4.3 re.DOTALL
(点号匹配所有字符)
import re
text = "line1nline2"
pattern = r"line1.line2"
match = re.search(pattern, text) # None
match_dotall = re.search(pattern, text, re.DOTALL) # re.S 也可以
if match_dotall:
print("点号匹配所有字符:", match_dotall.group(0))
输出:
点号匹配所有字符: line1
line2
4.4 re.VERBOSE
(详细模式)
re.VERBOSE
允许我们在正则表达式中使用空白字符和注释,以提高可读性。
import re
pattern = re.compile(r"""
(? # 可选的左括号
(d{3}) # 区号
)? # 可选的右括号
[s-]? # 可选的空格或连字符
(d{3}) # 前缀
[s-]? # 可选的空格或连字符
(d{4}) # 号码
""", re.VERBOSE)
text = "123-456-7890"
match = pattern.search(text)
if match:
print(match.groups())
输出:
('123', '456', '7890')
5. 非捕获分组与零宽断言:更精细的控制
5.1 非捕获分组 (Non-capturing Groups)
非捕获分组使用 (?:...)
语法。它们与普通分组类似,但不分配分组编号,也不会被 match.groups()
返回。这在只想对模式进行分组,而不需要引用分组内容时很有用。
import re
text = "apple banana orange"
pattern = r"(?:apple|banana) (orange)" # 仅捕获 orange
match = re.search(pattern, text)
if match:
print("整个匹配:", match.group(0))
print("第一个分组:", match.group(1))
print("所有分组:", match.groups())
输出:
整个匹配: banana orange
第一个分组: orange
所有分组: ('orange',)
5.2 零宽断言 (Zero-width Assertions)
零宽断言是一种不消耗字符的匹配。它们用于在特定位置进行断言,但不包含在最终的匹配结果中。
- 正向先行断言 (Positive Lookahead Assertion):
(?=...)
断言当前位置后面匹配模式...
。 - 负向先行断言 (Negative Lookahead Assertion):
(?!...)
断言当前位置后面不匹配模式...
。 - 正向后行断言 (Positive Lookbehind Assertion):
(?<=...)
断言当前位置前面匹配模式...
。 - 负向后行断言 (Negative Lookbehind Assertion):
(?<!...)
断言当前位置前面不匹配模式...
。
import re
text = "apple banana orange"
# 正向先行断言:匹配后面跟着 " banana" 的 "apple"
pattern_lookahead = r"apple(?= banana)"
match_lookahead = re.search(pattern_lookahead, text)
if match_lookahead:
print("正向先行断言:", match_lookahead.group(0))
# 负向先行断言:匹配后面不跟着 " banana" 的 "apple"
pattern_negative_lookahead = r"apple(?! banana)"
match_negative_lookahead = re.search(pattern_negative_lookahead, text) # None
# 正向后行断言:匹配前面是 "apple " 的 "banana"
pattern_lookbehind = r"(?<=apple )banana"
match_lookbehind = re.search(pattern_lookbehind, text)
if match_lookbehind:
print("正向后行断言:", match_lookbehind.group(0))
# 负向后行断言:匹配前面不是 "apple " 的 "banana"
pattern_negative_lookbehind = r"(?<!apple )banana"
match_negative_lookbehind = re.search(pattern_negative_lookbehind, text) #None
输出:
正向先行断言: apple
正向后行断言: banana
零宽断言在验证输入、提取特定上下文中的文本等场景中非常有用。
6. 贪婪与非贪婪匹配:控制匹配的范围
正则表达式默认是贪婪匹配,即尽可能多地匹配字符。我们可以通过在量词后面添加 ?
来使其变为非贪婪匹配,即尽可能少地匹配字符。
import re
text = "<a>text1</a><a>text2</a>"
pattern_greedy = r"<a>.*</a>" # 贪婪匹配
pattern_non_greedy = r"<a>.*?</a>" # 非贪婪匹配
match_greedy = re.search(pattern_greedy, text)
match_non_greedy = re.findall(pattern_non_greedy, text)
if match_greedy:
print("贪婪匹配:", match_greedy.group(0))
print("非贪婪匹配:", match_non_greedy)
输出:
贪婪匹配: <a>text1</a><a>text2</a>
非贪婪匹配: ['<a>text1</a>', '<a>text2</a>']
在这个例子中,贪婪匹配尽可能多地匹配字符,直到最后一个 </a>
。而非贪婪匹配则尽可能少地匹配字符,匹配到第一个 </a>
就停止。
7. 编译正则表达式:提高效率
当我们需要多次使用同一个正则表达式时,最好先将其编译成一个 RegexObject
对象,这可以提高匹配效率。
import re
pattern = re.compile(r"d+")
text1 = "123 abc 456"
text2 = "789 def 012"
match1 = pattern.findall(text1)
match2 = pattern.findall(text2)
print(match1)
print(match2)
输出:
['123', '456']
['789', '012']
编译后的 RegexObject
对象可以多次使用,避免了重复编译正则表达式的开销。
8. 常见应用场景:从数据清洗到网络爬虫
正则表达式在各种场景中都有广泛的应用,以下是一些常见的例子:
-
数据清洗: 清理和转换数据,例如删除不需要的字符、替换特定模式、提取有效信息。
import re data = [" 123 ", "456abc", "789 "] cleaned_data = [re.sub(r"^s+|s+$", "", item) for item in data] #去除首尾空格 print(cleaned_data) # ['123', '456abc', '789']
-
验证输入: 验证用户输入是否符合特定格式,例如邮箱地址、电话号码、密码强度。
import re email = "[email protected]" pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$" if re.match(pattern, email): print("Valid email address") else: print("Invalid email address") #Valid email address
-
网络爬虫: 从 HTML 或其他文本格式的网页中提取信息,例如链接、标题、内容。
import re import requests url = "https://www.example.com" response = requests.get(url) html = response.text # 提取所有链接 pattern = r'<a href="(.*?)"' links = re.findall(pattern, html) print(links) # 打印提取的链接 (example的链接被JS生成了,提取出来是空)
-
日志分析: 从日志文件中提取关键信息,例如错误信息、访问记录、性能指标。
import re log_line = "2023-10-27 10:00:00 ERROR: Failed to connect to database" pattern = r"ERROR: (.*)" match = re.search(pattern, log_line) if match: print("Error message:", match.group(1)) # Error message: Failed to connect to database
9. 调试与优化:避免常见的陷阱
- 过度使用复杂表达式: 复杂的正则表达式难以理解和维护。尽量将复杂逻辑拆分成多个简单的表达式。
- 性能问题: 某些正则表达式可能导致性能问题,特别是当处理大量文本时。使用
re.compile
编译正则表达式,并避免使用回溯过多的模式。 - 转义问题: 在正则表达式中,某些字符具有特殊含义,需要进行转义。例如,
用于转义特殊字符,
本身也需要转义为
\
。 - 测试: 编写单元测试来验证正则表达式的正确性。
10. 持续精进:学习资源与实践
- 官方文档: 阅读
re
模块的官方文档,了解所有函数和标志的详细用法。 - 在线工具: 使用在线正则表达式测试工具,例如 regex101.com,来测试和调试正则表达式。
- 书籍: 阅读《精通正则表达式》等经典书籍,深入理解正则表达式的原理和技巧。
- 实践: 通过实际项目来应用正则表达式,例如编写数据清洗脚本、网络爬虫、日志分析工具。
文本处理的利器:掌握re
模块的精髓
通过今天的学习,我们深入了解了 Python re
模块的高级用法,包括分组、命名分组、反向引用、函数替换、正则表达式标志、非捕获分组、零宽断言、贪婪与非贪婪匹配等。掌握这些技巧,你将能够更有效地处理文本数据,解决各种实际问题。希望大家在实践中不断精进,成为真正的正则表达式大师!