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

Python正则匹配多个字符串的策略

2024-05-153.8k 阅读

一、Python 正则表达式基础回顾

在深入探讨匹配多个字符串的策略之前,我们先来简单回顾一下 Python 正则表达式的基础。正则表达式(Regular Expression)是一种用于描述、匹配和操作文本模式的强大工具。在 Python 中,通过 re 模块来使用正则表达式。

1.1 基本语法

  • 字符匹配:普通字符(如字母、数字和标点符号)匹配自身。例如,正则表达式 abc 会匹配字符串中出现的 “abc” 子串。
  • 元字符:具有特殊含义的字符。例如:
    • .:匹配除换行符 \n 之外的任意单个字符。例如,正则表达式 a.c 可以匹配 “abc”、“a1c” 等。
    • ^:匹配字符串的开始位置。例如,^abc 只会匹配以 “abc” 开头的字符串。
    • $:匹配字符串的结束位置。例如,abc$ 只会匹配以 “abc” 结尾的字符串。
    • *:匹配前一个字符 0 次或多次。例如,ab*c 可以匹配 “ac”(b 出现 0 次)、“abc”(b 出现 1 次)、“abbc”(b 出现 2 次)等。
    • +:匹配前一个字符 1 次或多次。例如,ab+c 可以匹配 “abc”、“abbc” 等,但不能匹配 “ac”。
    • ?:匹配前一个字符 0 次或 1 次。例如,ab?c 可以匹配 “ac” 或 “abc”。
    • []:字符集,匹配方括号中的任意一个字符。例如,[abc] 可以匹配 “a”、“b” 或 “c”。
    • ():分组,将括号内的内容作为一个整体进行操作。例如,(ab)+ 会匹配 “ab”、“abab” 等。

1.2 Python re 模块常用函数

  • re.search(pattern, string):在字符串中搜索匹配正则表达式 pattern 的第一个位置,返回一个匹配对象(Match Object),如果没有找到匹配则返回 None
import re
match = re.search('abc', '12abc34')
if match:
    print('找到匹配:', match.group())
  • re.findall(pattern, string):查找字符串中所有匹配正则表达式 pattern 的子串,并以列表形式返回。
import re
matches = re.findall('abc', 'abc12abc34')
print('所有匹配:', matches)
  • re.sub(pattern, repl, string):将字符串中所有匹配正则表达式 pattern 的子串替换为 repl
import re
new_string = re.sub('abc', 'xyz', 'abc12abc34')
print('替换后的字符串:', new_string)

二、匹配多个字符串的简单场景

2.1 使用 | 运算符(逻辑或)

在正则表达式中,| 运算符表示逻辑或的关系。通过它,我们可以轻松地匹配多个不同的字符串。例如,我们要匹配字符串中的 “apple” 或 “banana”:

import re
text = "I like apple and banana"
matches = re.findall('apple|banana', text)
print(matches)

在这个例子中,re.findall 函数会在 text 字符串中查找 “apple” 或者 “banana”,并将找到的结果以列表形式返回。

2.2 匹配多个相似模式的字符串

假设我们有一系列以 “file_” 开头,后面跟着数字的文件名,如 “file_1.txt”、“file_2.txt” 等,并且我们想匹配所有这样的文件名。我们可以利用正则表达式的模式匹配能力:

import re
filenames = "file_1.txt file_2.txt file_abc.txt file_3.jpg"
pattern = r'file_\d+\.\w+'
matches = re.findall(pattern, filenames)
print(matches)

在上述代码中,r'file_\d+\.\w+' 这个正则表达式的含义是:

  • file_:匹配字符串 “file_”。
  • \d+:匹配一个或多个数字。
  • \.:匹配点号(因为点号在正则表达式中有特殊含义,所以需要转义)。
  • \w+:匹配一个或多个字母、数字或下划线。

三、复杂场景下匹配多个字符串的策略

3.1 分组与捕获

分组是正则表达式中一个非常重要的概念。通过 () 可以将多个字符组合成一个逻辑单元。例如,我们想匹配日期格式为 “YYYY - MM - DD” 或者 “YYYY/MM/DD” 的字符串:

import re
dates = "2023 - 01 - 01 2023/02/02 2023.03.03"
pattern = r'(\d{4})[-/](\d{2})[-/](\d{2})'
matches = re.findall(pattern, dates)
for match in matches:
    print('年:', match[0], '月:', match[1], '日:', match[2])

在这个例子中,(\d{4})(\d{2}) 分别是捕获组。re.findall 函数返回的结果是一个元组列表,每个元组中的元素依次对应各个捕获组匹配到的内容。

3.2 非捕获组

有时候我们只是想进行分组操作,但不想捕获分组内的内容。这时候可以使用非捕获组 (?:pattern)。例如,我们想匹配 “color” 或者 “colour”,但不想单独捕获中间的 “o” 或 “ou”:

import re
text = "The color of the sky is blue, and the colour of the grass is green."
pattern = r'col(?:ou?|o)r'
matches = re.findall(pattern, text)
print(matches)

这里的 (?:ou?|o) 就是一个非捕获组,它只起到逻辑分组的作用,不会被单独捕获。

3.3 零宽断言

零宽断言用于在特定位置进行匹配,但不消耗字符。常见的零宽断言有:

  • 正向前瞻断言(?=pattern),断言当前位置之后能匹配 pattern。例如,我们想匹配以 “.txt” 结尾的文件名,但不包括 “.txt” 部分:
import re
filenames = "file1.txt file2.doc file3.txt"
pattern = r'\w+(?=\.txt)'
matches = re.findall(pattern, filenames)
print(matches)

在这个例子中,(?=\.txt) 断言当前位置之后能匹配 “.txt”,但它本身不消耗字符,所以 re.findall 函数返回的结果不包含 “.txt”。

  • 负向前瞻断言(?!pattern),断言当前位置之后不能匹配 pattern。例如,我们想匹配不以 “.txt” 结尾的文件名:
import re
filenames = "file1.txt file2.doc file3.txt"
pattern = r'\w+(?!\.txt)'
matches = re.findall(pattern, filenames)
print(matches)
  • 正向后瞻断言(?<=pattern),断言当前位置之前能匹配 pattern。例如,我们想匹配前面有 “http://” 或 “https://” 的网址:
import re
urls = "http://example.com https://test.net ftp://ftp.example.org"
pattern = r'(?<=http://|https://)\w+\.\w+'
matches = re.findall(pattern, urls)
print(matches)
  • 负向后瞻断言(?<!pattern),断言当前位置之前不能匹配 pattern。例如,我们想匹配前面没有 “http://” 或 “https://” 的网址:
import re
urls = "http://example.com https://test.net ftp://ftp.example.org"
pattern = r'(?<!http://|https://)\w+\.\w+'
matches = re.findall(pattern, urls)
print(matches)

3.4 贪婪与非贪婪匹配

在正则表达式中,*+ 等量词默认是贪婪的,即尽可能多地匹配字符。例如:

import re
text = "<div>content1</div><div>content2</div>"
pattern = r'<div>.*</div>'
match = re.search(pattern, text)
if match:
    print(match.group())

在这个例子中,.* 会贪婪地匹配尽可能多的字符,所以它会匹配整个 <div>content1</div><div>content2</div>

如果我们想让它只匹配第一个 <div> 和对应的 </div> 之间的内容,可以使用非贪婪匹配。在量词后面加上 ? 就可以实现非贪婪匹配:

import re
text = "<div>content1</div><div>content2</div>"
pattern = r'<div>.*?</div>'
match = re.search(pattern, text)
if match:
    print(match.group())

这里的 .*? 会尽可能少地匹配字符,所以只会匹配 <div>content1</div>

四、处理多行文本中的多个字符串匹配

4.1 re.MULTILINE 标志

当我们处理多行文本时,^$ 默认只匹配字符串的开头和结尾。如果我们想让它们匹配每一行的开头和结尾,可以使用 re.MULTILINE 标志。例如,我们有一个包含多行的文本,每行格式为 “name: age”,我们想匹配所有的名字:

import re
text = """John: 25
Alice: 30
Bob: 28"""
pattern = r'^(\w+):'
matches = re.findall(pattern, text, re.MULTILINE)
print(matches)

在这个例子中,re.MULTILINE 标志使得 ^ 匹配每一行的开头,这样就能正确地匹配出每一行的名字。

4.2 跨行匹配

有时候我们需要匹配跨越多行的内容。例如,我们有一个 XML 格式的文本,想匹配 <tag>...</tag> 标签内跨越多行的内容:

import re
xml_text = """<tag>
    some content
    across multiple lines
</tag>"""
pattern = r'<tag>.*?</tag>'
match = re.search(pattern, xml_text, re.DOTALL)
if match:
    print(match.group())

在这个例子中,re.DOTALL 标志使得 . 可以匹配包括换行符在内的任意字符,从而实现跨行匹配。

五、优化正则表达式匹配多个字符串的性能

5.1 避免不必要的捕获组

捕获组会增加匹配的开销,因为需要额外存储捕获的内容。如果我们不需要捕获某些分组的内容,尽量使用非捕获组。例如,前面匹配 “color” 或 “colour” 的例子中,使用非捕获组 (?:ou?|o) 比使用捕获组 (ou?|o) 性能更好。

5.2 简化复杂的正则表达式

复杂的正则表达式可能会导致性能下降。尽量将复杂的匹配逻辑拆分成多个简单的正则表达式,然后逐步处理。例如,如果我们要匹配一个复杂的日期和时间格式,并且还需要验证其格式的合法性,可以先使用一个简单的正则表达式匹配基本的格式,然后再通过程序逻辑验证具体的日期和时间是否合法。

5.3 使用预编译的正则表达式

在需要多次使用同一个正则表达式进行匹配时,可以先将其预编译。预编译可以提高匹配效率,因为编译过程只需要执行一次。例如:

import re
pattern = re.compile(r'\d{4}-\d{2}-\d{2}')
text1 = "2023 - 01 - 01"
text2 = "2023 - 02 - 02"
match1 = pattern.search(text1)
match2 = pattern.search(text2)
if match1:
    print('在 text1 中找到匹配:', match1.group())
if match2:
    print('在 text2 中找到匹配:', match2.group())

在这个例子中,re.compile 函数将正则表达式预编译成一个 Pattern 对象,然后可以多次使用这个对象进行匹配,而不需要每次都编译正则表达式。

六、实战案例

6.1 从 HTML 中提取链接

假设我们有一个 HTML 页面,需要提取其中所有的链接。HTML 链接通常以 <a href="url"> 的形式出现。我们可以使用正则表达式来匹配并提取链接:

import re
html = """<a href="http://example.com">Example</a>
<a href="https://test.net">Test</a>"""
pattern = r'<a href="([^"]+)">'
matches = re.findall(pattern, html)
for match in matches:
    print('提取到的链接:', match)

在这个例子中,([^"]+) 这个捕获组用于捕获 href 属性值中的链接。[^"]+ 表示匹配除双引号之外的一个或多个字符。

6.2 解析日志文件

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

2023 - 01 - 01 10:00:00 INFO Starting application
2023 - 01 - 01 10:01:00 ERROR Failed to connect to database

我们想提取日志中的时间、日志级别和消息内容。可以使用如下正则表达式:

import re
log_text = """2023 - 01 - 01 10:00:00 INFO Starting application
2023 - 01 - 01 10:01:00 ERROR Failed to connect to database"""
pattern = r'(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) (\w+) (.*)'
matches = re.findall(pattern, log_text)
for match in matches:
    print('时间:', match[0], '日志级别:', match[1], '消息:', match[2])

在这个例子中,通过三个捕获组分别捕获时间、日志级别和消息内容。(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) 匹配日期和时间,(\w+) 匹配日志级别,(.*) 匹配消息内容。

通过以上详细的讲解和丰富的代码示例,相信你对 Python 正则匹配多个字符串的策略有了更深入的理解和掌握。在实际应用中,根据具体的需求选择合适的策略和方法,能够高效地处理各种文本匹配任务。