MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Python正则表达式的高级匹配模式

2021-09-277.3k 阅读

零宽断言

正向肯定断言

正向肯定断言是一种零宽断言,它用于匹配某个模式后紧接着是另一个指定模式的位置,但不包含第二个模式本身。在Python中,使用 (?=pattern) 语法来表示正向肯定断言,其中 pattern 是要匹配的后续模式。

例如,假设我们要匹配所有以 “book” 开头,后面紧接着是 “store” 的字符串中的 “book” 部分。代码如下:

import re

text = "bookstore is a place to buy books"
matches = re.findall(r'book(?=store)', text)
print(matches)

在上述代码中,re.findall(r'book(?=store)', text) 会查找所有满足 “book” 后面紧接着 “store” 的 “book” 子串。由于 (?=store) 是正向肯定断言,它只检查 “book” 后面是否是 “store”,但不把 “store” 包含在匹配结果中。所以输出结果为 ['book']

正向否定断言

正向否定断言与正向肯定断言相反,它用于匹配某个模式后不紧接着是另一个指定模式的位置。在Python中,使用 (?!pattern) 语法,其中 pattern 是不应该紧接着出现的模式。

例如,我们要匹配所有 “book”,但前提是 “book” 后面不是 “store”。代码如下:

import re

text = "book is a good thing, but not bookstore"
matches = re.findall(r'book(?!store)', text)
print(matches)

在这个例子中,re.findall(r'book(?!store)', text) 会查找所有 “book”,但这些 “book” 后面不能紧接着 “store”。所以输出结果会是 ['book', 'book'],因为这两个 “book” 后面都不是 “store”。

反向肯定断言

反向肯定断言用于匹配某个模式前紧接着是另一个指定模式的位置,但不包含前面的模式本身。在Python中,使用 (?<=pattern) 语法,其中 pattern 是前面应该紧接着的模式。

假设我们有一个文本,其中包含价格信息,格式为 “货币符号 + 价格数字”,我们要提取所有价格数字,前提是前面是 “$”。代码如下:

import re

text = "The price is $10, and another price is $20"
matches = re.findall(r'(?<=\$)\d+', text)
print(matches)

在上述代码中,re.findall(r'(?<=\$)\d+', text) 会查找所有前面紧接着 “$” 的数字部分。(?<=\$) 就是反向肯定断言,它确保匹配的数字前面有 “$”,但不把 “$” 包含在匹配结果中。输出结果为 ['10', '20']

反向否定断言

反向否定断言用于匹配某个模式前不紧接着是另一个指定模式的位置。在Python中,使用 (?<!pattern) 语法,其中 pattern 是前面不应该紧接着出现的模式。

例如,我们要匹配所有数字,但前提是这些数字前面不是 “$”。代码如下:

import re

text = "There are 5 apples, and the price is $10"
matches = re.findall(r'(?<!\$)\d+', text)
print(matches)

在这个例子中,re.findall(r'(?<!\$)\d+', text) 会查找所有前面没有 “$” 的数字。所以输出结果为 ['5'],因为 “10” 前面有 “$”,不符合反向否定断言的条件。

分组与捕获

捕获组

捕获组是正则表达式中最常用的功能之一。通过使用圆括号 () 可以定义捕获组,捕获组会捕获匹配的子字符串,并可以在后续使用。

例如,假设我们有一个文本包含日期,格式为 “年-月-日”,我们要分别提取年、月、日。代码如下:

import re

text = "Today is 2023-05-15"
match = re.search(r'(\d{4})-(\d{2})-(\d{2})', text)
if match:
    year = match.group(1)
    month = match.group(2)
    day = match.group(3)
    print(f"Year: {year}, Month: {month}, Day: {day}")

在上述代码中,re.search(r'(\d{4})-(\d{2})-(\d{2})', text) 中的三个圆括号定义了三个捕获组。match.group(1) 获取第一个捕获组(年),match.group(2) 获取第二个捕获组(月),match.group(3) 获取第三个捕获组(日)。

命名捕获组

命名捕获组是为捕获组指定一个名称,这样在后续使用时可以通过名称来访问捕获的内容,而不是通过数字索引。在Python中,使用 (?P<name>pattern) 语法来定义命名捕获组,其中 name 是捕获组的名称,pattern 是要匹配的模式。

例如,我们还是处理日期格式 “年-月-日”,这次使用命名捕获组。代码如下:

import re

text = "Today is 2023-05-15"
match = re.search(r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})', text)
if match:
    year = match.group('year')
    month = match.group('month')
    day = match.group('day')
    print(f"Year: {year}, Month: {month}, Day: {day}")

在这个例子中,通过 (?P<year>\d{4})(?P<month>\d{2})(?P<day>\d{2}) 分别定义了名为 “year”、“month” 和 “day” 的命名捕获组。使用 match.group('year') 等方式通过名称获取捕获的内容,这样代码的可读性更强,尤其是在有多个捕获组的情况下。

非捕获组

有时候我们只是想对模式进行分组,但不想捕获该组匹配的内容,这时可以使用非捕获组。在Python中,使用 (?:pattern) 语法来定义非捕获组,其中 pattern 是要分组的模式。

例如,我们要匹配 “color” 或者 “colour”,但不需要捕获中间的 “our” 或 “or” 部分。代码如下:

import re

text = "I like the color red, and also the colour blue"
matches = re.findall(r'col(?:our|or)', text)
print(matches)

在上述代码中,re.findall(r'col(?:our|or)', text) 使用了非捕获组 (?:our|or)。这样只是对 “our” 和 “or” 进行分组匹配,但不会捕获这部分内容。输出结果为 ['color', 'colour']

贪婪与非贪婪匹配

贪婪匹配

在Python正则表达式中,默认情况下是贪婪匹配。贪婪匹配会尽可能多地匹配字符,直到满足整个模式或者无法继续匹配为止。

例如,我们有一个HTML标签 <div>content1</div><div>content2</div>,我们想提取 <div> 标签内的内容。如果使用贪婪匹配,代码如下:

import re

text = "<div>content1</div><div>content2</div>"
matches = re.findall(r'<div>(.*)</div>', text)
print(matches)

在这个例子中,re.findall(r'<div>(.*)</div>', text) 中的 .* 是贪婪匹配模式。它会尽可能多地匹配字符,所以最终的匹配结果是 ['content1</div><div>content2'],它把两个 <div> 标签之间的所有内容都匹配了,而不是我们期望的分别匹配两个 <div> 标签内的内容。

非贪婪匹配

非贪婪匹配与贪婪匹配相反,它会尽可能少地匹配字符,一旦满足整个模式就停止匹配。在Python中,通过在量词(如 *+?)后面加上 ? 来实现非贪婪匹配。

还是以上面的HTML标签为例,使用非贪婪匹配来提取 <div> 标签内的内容。代码如下:

import re

text = "<div>content1</div><div>content2</div>"
matches = re.findall(r'<div>(.*?)</div>', text)
print(matches)

在这个例子中,re.findall(r'<div>(.*?)</div>', text) 中的 .*? 是非贪婪匹配模式。它会尽可能少地匹配字符,一旦遇到 </div> 就停止匹配。所以输出结果为 ['content1', 'content2'],这正是我们期望的分别匹配两个 <div> 标签内的内容。

环视与边界匹配

单词边界

单词边界用于匹配单词的边界位置,在Python中使用 \b 来表示。单词边界是指单词字符(字母、数字、下划线)与非单词字符之间的位置,或者字符串开头或结尾的位置。

例如,我们要匹配单词 “book”,但确保它是一个独立的单词,而不是某个更长单词的一部分。代码如下:

import re

text = "book is on the bookshelf"
matches = re.findall(r'\bbook\b', text)
print(matches)

在上述代码中,re.findall(r'\bbook\b', text) 会查找所有独立的 “book” 单词。输出结果为 ['book'],因为 “bookshelf” 中的 “book” 由于后面紧接着 “shelf”,不是独立单词,不符合单词边界的匹配条件。

行首与行尾

行首匹配使用 ^,行尾匹配使用 $。它们用于匹配字符串的开头和结尾位置,或者在多行模式下匹配每一行的开头和结尾。

例如,我们要匹配以 “Hello” 开头的行。代码如下:

import re

text = "Hello, world\nGoodbye, world"
matches = re.findall(r'^Hello,.*', text, re.MULTILINE)
print(matches)

在这个例子中,re.findall(r'^Hello,.*', text, re.MULTILINE) 中的 ^ 表示行首,re.MULTILINE 标志表示在多行模式下匹配。所以它会匹配以 “Hello” 开头的行,输出结果为 ['Hello, world']

如果要匹配以 “world” 结尾的行,代码如下:

import re

text = "Hello, world\nGoodbye, world"
matches = re.findall(r'.*world$', text, re.MULTILINE)
print(matches)

这里 re.findall(r'.*world$', text, re.MULTILINE) 中的 $ 表示行尾,输出结果为 ['Hello, world', 'Goodbye, world'],因为这两行都以 “world” 结尾。

环视

环视其实就是前面提到的零宽断言,包括正向肯定断言 (?=pattern)、正向否定断言 (?!pattern)、反向肯定断言 (?<=pattern) 和反向否定断言 (?<!pattern)。它们通过在不实际消耗字符的情况下对前后内容进行断言匹配,在很多复杂匹配场景中非常有用。

例如,在验证密码强度时,可能要求密码必须包含至少一个数字,并且不能以数字开头。可以使用如下代码:

import re

password = "abc123"
if re.search(r'^(?!\d)(?=.*\d).*$', password):
    print("Password meets the requirements")
else:
    print("Password does not meet the requirements")

在这个例子中,^(?!\d) 是反向否定断言,确保密码不以数字开头;(?=.*\d) 是正向肯定断言,确保密码中包含至少一个数字。.*$ 匹配密码的其余部分。

正则表达式的修饰符

re.I(忽略大小写)

re.I 修饰符用于使正则表达式匹配时忽略大小写。

例如,我们要匹配 “python”,无论它是大写还是小写形式。代码如下:

import re

text = "Python is a great language, PYTHON is popular"
matches = re.findall(r'python', text, re.I)
print(matches)

在上述代码中,re.findall(r'python', text, re.I) 中的 re.I 修饰符使匹配忽略大小写。所以输出结果为 ['Python', 'PYTHON']

re.M(多行模式)

re.M 修饰符用于多行模式匹配。在默认情况下,^$ 只匹配整个字符串的开头和结尾。而在多行模式下,它们还会匹配每一行的开头和结尾。

例如,我们有一个文本包含多行内容,要匹配每一行以 “Start” 开头的内容。代码如下:

import re

text = "Start line 1\nNot start line 2\nStart line 3"
matches = re.findall(r'^Start.*', text, re.M)
print(matches)

在这个例子中,re.findall(r'^Start.*', text, re.M) 中的 re.M 修饰符开启了多行模式。所以 ^ 不仅匹配整个字符串的开头,还匹配每一行的开头。输出结果为 ['Start line 1', 'Start line 3']

re.S(点任意匹配模式)

re.S 修饰符用于使点号 . 可以匹配包括换行符在内的任意字符。在默认情况下,点号 . 不匹配换行符。

例如,我们有一个包含换行符的文本,要匹配所有内容直到最后一个 “end”。代码如下:

import re

text = "This is line 1\nThis is line 2\nend"
matches = re.findall(r'.*end', text, re.S)
print(matches)

在上述代码中,re.findall(r'.*end', text, re.S) 中的 re.S 修饰符使 . 可以匹配换行符。所以输出结果为 ['This is line 1\nThis is line 2\nend'],如果没有 re.S,则只能匹配到 “This is line 1”。

re.X(详细模式)

re.X 修饰符允许在正则表达式中添加注释和空白字符,以提高正则表达式的可读性。

例如,我们有一个复杂的正则表达式来匹配邮箱地址,使用 re.X 修饰符可以这样写:

import re

email_pattern = r"""
    [a-zA-Z0-9._%+-]+  # 用户名部分
    @                 # 必须有 @ 符号
    [a-zA-Z0-9.-]+    # 域名部分
    \.                # 必须有一个点号
    [a-zA-Z]{2,}      # 顶级域名部分
"""
text = "test@example.com"
match = re.search(email_pattern, text, re.X)
if match:
    print("Valid email")
else:
    print("Invalid email")

在这个例子中,re.search(email_pattern, text, re.X) 中的 re.X 修饰符使正则表达式中的空白字符和注释被忽略。这样我们可以把复杂的正则表达式写得更易读。

复杂应用场景示例

从HTML中提取链接

假设我们有一个HTML页面,要从中提取所有的链接(<a> 标签的 href 属性值)。代码如下:

import re

html = """
<html>
    <body>
        <a href="https://www.example.com">Example</a>
        <a href="https://www.google.com">Google</a>
    </body>
</html>
"""
matches = re.findall(r'<a href="([^"]+)">', html)
print(matches)

在上述代码中,re.findall(r'<a href="([^"]+)">', html) 用于匹配 <a> 标签中 href 属性的值。([^"]+) 是一个捕获组,用于捕获双引号之间的链接地址。输出结果为 ['https://www.example.com', 'https://www.google.com']

验证电话号码格式

假设我们要验证电话号码格式,电话号码格式为 “三位区号 - 八位电话号码” 或者 “八位电话号码”。代码如下:

import re

phone_numbers = ["123-45678901", "45678901"]
for number in phone_numbers:
    if re.search(r'^\d{3}-\d{8}$|^\d{8}$', number):
        print(f"{number} is a valid phone number")
    else:
        print(f"{number} is not a valid phone number")

在这个例子中,re.search(r'^\d{3}-\d{8}$|^\d{8}$', number) 使用了 “或” 逻辑(|)。^\d{3}-\d{8}$ 用于匹配 “三位区号 - 八位电话号码” 的格式,^\d{8}$ 用于匹配 “八位电话号码” 的格式。

解析日志文件

假设我们有一个日志文件,格式如下:

[2023-05-15 10:00:00] INFO Starting application
[2023-05-15 10:01:00] ERROR Failed to connect to database

我们要提取日志中的时间、日志级别和日志信息。代码如下:

import re

log = """
[2023-05-15 10:00:00] INFO Starting application
[2023-05-15 10:01:00] ERROR Failed to connect to database
"""
matches = re.findall(r'\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\] (\w+) (.*)', log)
for match in matches:
    time_stamp, log_level, log_message = match
    print(f"Time: {time_stamp}, Level: {log_level}, Message: {log_message}")

在上述代码中,re.findall(r'\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\] (\w+) (.*)', log) 使用了三个捕获组。第一个捕获组 (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) 用于捕获时间,第二个捕获组 (\w+) 用于捕获日志级别,第三个捕获组 (.*) 用于捕获日志信息。通过遍历匹配结果,我们可以分别获取时间、日志级别和日志信息。

通过以上对Python正则表达式高级匹配模式的详细介绍和示例,希望能帮助你在处理文本时更灵活、高效地使用正则表达式。在实际应用中,需要根据具体的需求来选择合适的匹配模式和修饰符,以达到最佳的匹配效果。同时,要注意正则表达式的性能问题,尤其是在处理大量文本时,复杂的正则表达式可能会导致性能下降。