`Python`的`正则表达式`:`re`模块的`高级`用法。

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 模块的高级用法,包括分组、命名分组、反向引用、函数替换、正则表达式标志、非捕获分组、零宽断言、贪婪与非贪婪匹配等。掌握这些技巧,你将能够更有效地处理文本数据,解决各种实际问题。希望大家在实践中不断精进,成为真正的正则表达式大师!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注