【Python】正则表达式

第一章:正则表达式

1.1 什么是正则表达式?它为什么如此强大?

从本质上讲,正则表达式就是一种“模式语言”或者“模式描述符”。它使用一套预定义的特殊字符和语法规则,来表示或“匹配”文本中特定的字符组合模式。当我们需要在大量文本中寻找、替换、提取符合特定规律的数据时,手动编写复杂的字符串处理逻辑会变得异常繁琐且容易出错。正则表达式以其高度抽象和表达力,极大地简化了这类任务。

它的强大之处在于:

  • 简洁性: 一行正则表达式往往可以替代几十行甚至上百行的传统字符串处理代码。
  • 灵活性: 能够处理各种复杂且多变的字符串模式,从简单的字符查找,到复杂的结构化数据提取。
  • 通用性: 正则表达式的语法在多数编程语言(Python、Java、JavaScript、Perl、PHP、Ruby等)和文本处理工具(grep、sed、awk)中是高度一致的,一旦掌握,即可多处使用。
  • 高效性: 许多正则表达式引擎经过高度优化,在处理大量文本时表现出优异的性能。

举个最简单的例子,如果您想找到文本中所有的电子邮件地址,传统方法可能需要循环遍历字符串,判断字符是否包含@.,然后进一步验证其前后的字符是否符合邮箱格式,这会非常复杂。而使用正则表达式,可能仅仅是一个表达式就能完成。

1.2 正则表达式的历史渊源与发展

正则表达式的理论基础可以追溯到20世纪40年代的**数学家斯蒂芬·科尔·克莱尼(Stephen Cole Kleene)**在神经网络研究中引入的“正则表达式”概念,用来描述正则语言(Regular Language)。正则语言是一类可以用有限状态自动机识别的语言。这一理论成果在计算机科学领域找到了完美的落脚点。

1.3 元字符的本质:字符与操作符的融合

正则表达式的核心在于元字符(Metacharacters)。元字符是具有特殊含义的字符,它们不代表自身的字面值,而是作为操作符或通配符来控制匹配行为。可以把它们想象成编程语言中的关键字或运算符。

例如:

  • a:普通字符,匹配字符 ‘a’ 本身。
  • .:元字符,匹配除换行符外的任意一个字符。
  • *:元字符,表示其前面的元素可以出现零次或多次。

正则表达式的强大之处在于,它通过组合这些元字符与普通字面字符,构建出能够描述复杂模式的表达式。这些组合就像是构建句子的词语和语法规则,使得我们可以用非常精炼的语言来“告诉”匹配引擎我们想要寻找什么。

1.4 匹配引擎的内部机制概览:NFA vs DFA

虽然作为使用者,我们通常不需要深入了解正则表达式引擎的具体实现细节,但对**非确定性有限自动机(NFA)确定性有限自动机(DFA)**这两种主流的匹配引擎工作原理有个概括性的认识,能帮助我们更好地理解正则表达式的行为,尤其是在面对性能问题(如“灾难性回溯”)时。

  • DFA(Deterministic Finite Automaton - 确定性有限自动机):

    • 工作方式: DFA引擎是“文本主导”的。它从正则表达式的第一个字符开始,逐个读取输入字符串的字符,并根据当前状态和读取的字符,确定性地转换到下一个状态。DFA会遍历整个输入字符串,找到所有可能的匹配。
    • 特点: 速度快,因为它不会回溯(backtrack)。对于给定的输入字符和当前状态,下一个状态是唯一确定的。但它通常不支持捕获组、反向引用等高级特性。
    • 例子: grep 等工具通常使用DFA。
  • NFA(Nondeterministic Finite Automaton - 非确定性有限自动机):

    • 工作方式: NFA引擎是“正则主导”的。它从正则表达式的第一个字符开始,尝试在输入字符串中找到匹配。如果遇到多种可能的匹配路径(例如,量词*+),它会“记住”这些选择点,并沿着其中一条路径前进。如果当前路径无法导致匹配成功,它会“回溯”到最近的选择点,尝试另一条路径。
    • 特点: 功能强大,支持捕获组、反向引用、零宽断言等高级特性。但由于其回溯机制,在某些特定模式(如嵌套量词、交替选项过多)下可能会导致性能急剧下降,出现“灾难性回溯”。
    • 例子: Python的re模块,Perl、Java、JavaScript等大多数现代编程语言的正则表达式引擎都基于NFA或NFA的变体。

Python的re模块使用的是NFA引擎。这意味着它在某些情况下可能会进行回溯,这既赋予了它处理复杂模式的能力,也引入了性能陷阱。理解这一点,对于后续深入讨论正则表达式的性能优化至关重要。

1.5 Python re 模块的引入与核心函数概览

Python通过内置的re模块提供了对正则表达式的全面支持。要使用正则表达式,首先需要导入这个模块。

import re # 导入Python的re模块,这是使用正则表达式的必要步骤

re模块提供了多个核心函数,它们是进行正则表达式操作的入口点:

  • re.match():尝试从字符串的开头匹配模式。如果模式在字符串开头找到匹配,则返回一个匹配对象(MatchObject);否则返回None
  • re.search():扫描整个字符串,找到第一个匹配模式的位置。如果找到匹配,则返回一个匹配对象;否则返回None
  • re.findall():在字符串中找到所有非重叠的匹配项,并以列表形式返回所有匹配的字符串。
  • re.sub():替换字符串中所有匹配模式的子串。
  • re.compile():将正则表达式模式编译成一个正则表达式对象,以提高重复使用时的性能。

我们将在后续章节中对这些函数进行极致深入的剖析。现在,让我们从最基础的匹配开始。

1.6 基础匹配:字面字符匹配

最简单的正则表达式就是匹配字面字符。这意味着正则表达式中的字符直接代表它们自身,不具有特殊含义。

示例 1.6.1:匹配单个字面字符

import re # 导入re模块

text_to_search = "Hello, Python! Python is powerful." # 定义一个待搜索的字符串

# 使用re.search()查找第一个'P'字符
match_p = re.search(r'P', text_to_search) # r''表示原始字符串,避免反斜杠的转义问题;这里查找字符'P'
if match_p: # 如果找到了匹配
    print(f"找到字符 'P':{
     
     match_p.group()}") # match_p.group() 返回匹配到的字符串 'P'
else: # 如果没有找到
    print("未找到字符 'P'") # 打印未找到信息

# 尝试查找一个不存在的字符
match_z = re.search(r'Z', text_to_search) # 查找字符'Z'
if match_z: # 如果找到了匹配
    print(f"找到字符 'Z':{
     
     match_z.group()}") # 打印匹配结果
else: # 如果没有找到
    print("未找到字符 'Z'") # 打印未找到信息

代码解释:

  • import re: 这行代码导入了Python的re模块,它是处理正则表达式的核心库。
  • text_to_search = "Hello, Python! Python is powerful.": 定义了一个字符串变量text_to_search,这是我们要进行模式匹配的目标文本。
  • match_p = re.search(r'P', text_to_search): 使用re.search()函数在text_to_search中搜索模式r'P'r'P'是一个原始字符串(raw string),其中r前缀表示反斜杠\不会被解释为转义字符。在这里,模式P就是要匹配的字面字符’P’。re.search()会扫描整个字符串,并返回第一个匹配到的结果(如果存在),以一个MatchObject对象的形式返回。
  • if match_p:: 判断re.search()的返回值是否为真。如果找到了匹配,match_p将是一个MatchObject,其布尔值为True。如果没有找到,re.search()会返回None,其布尔值为False
  • print(f"找到字符 'P':{match_p.group()}"): 如果找到了匹配,match_p.group()方法会返回匹配到的实际字符串。在这里,它将返回'P'
  • else: print("未找到字符 'P'"): 如果没有找到匹配,则打印相应的提示信息。
  • 后续的代码块 match_z = re.search(r'Z', text_to_search) 及相应的if/else逻辑,演示了当模式在字符串中不存在时的行为。

示例 1.6.2:匹配字面字符串

import re # 导入re模块

sentence = "The quick brown fox jumps over the lazy dog." # 定义一个包含多个单词的句子

# 匹配完整的单词 "fox"
match_fox = re.search(r'fox', sentence) # 查找字符串"fox"
if match_fox: # 如果找到了匹配
    print(f"找到单词 'fox':{
     
     match_fox.group()}") # 打印匹配到的"fox"
else: # 如果没有找到
    print("未找到单词 'fox'") # 打印未找到信息

# 匹配一个短语 "lazy dog"
match_lazy_dog = re.search(r'lazy dog', sentence) # 查找字符串"lazy dog"
if match_lazy_dog: # 如果找到了匹配
    print(f"找到短语 'lazy dog':{
     
     match_lazy_dog.group()}") # 打印匹配到的"lazy dog"
else: # 如果没有找到
    print("未找到短语 'lazy dog'") # 打印未找到信息

代码解释:

  • import re: 导入re模块。
  • sentence = "The quick brown fox jumps over the lazy dog.": 待匹配的示例文本。
  • match_fox = re.search(r'fox', sentence): 搜索字面字符串"fox"。正则表达式中的多个字面字符会按照顺序进行匹配。
  • if match_fox: print(f"找到单词 'fox':{match_fox.group()}") else: print("未找到单词 'fox'"): 根据re.search()的返回结果,判断是否找到并打印相应的消息。
  • match_lazy_dog = re.search(r'lazy dog', sentence): 搜索字面字符串"lazy dog",包含空格,正则表达式也会将其视为字面字符进行匹配。
  • 后续的if/else逻辑与上一个示例相同,用于处理和展示匹配结果。

1.7 re.match()re.search() 的根本区别

初学者常常会混淆re.match()re.search()。它们的根本区别在于匹配的起始位置。

  • re.match(pattern, string, flags=0):只尝试从string开头匹配pattern。如果字符串的第一个字符不符合模式,match()会立即返回None。它不会扫描整个字符串。
  • re.search(pattern, string, flags=0):扫描整个string,查找pattern第一个匹配。它会从字符串的开头开始尝试匹配,如果失败,就会移动到下一个位置继续尝试,直到找到第一个匹配或扫描完整个字符串。

理解这个区别至关重要,因为它直接影响您选择哪个函数来完成任务。

示例 1.7.1:re.match() 的行为

import re # 导入re模块

text1 = "Python programming is fun." # 字符串以"Python"开头
text2 = "I love Python programming." # 字符串中间有"Python"

# 尝试用re.match()匹配"Python"
match1 = re.match(r'Python', text1) # 尝试从text1的开头匹配"Python"
if match1: # 如果匹配成功
    print(f"text1 (re.match): 找到 '{
     
     match1.group()}'") # 打印匹配结果
else: # 如果匹配失败
    print("text1 (re.match): 未找到 'Python'") # 打印未找到信息

# 尝试用re.match()匹配"Python"
match2 = re.match(r'Python', text2) # 尝试从text2的开头匹配"Python"
if match2: # 如果匹配成功
    print(f"text2 (re.match): 找到 '{
     
     match2.group()}'") # 打印匹配结果
else: # 如果匹配失败
    print("text2 (re.match): 未找到 'Python'") # 打印未找到信息

代码解释:

  • import re: 导入re模块。
  • text1 = "Python programming is fun.": 定义第一个测试字符串,以"Python"开头。
  • text2 = "I love Python programming.": 定义第二个测试字符串,"Python"在中间。
  • match1 = re.match(r'Python', text1): re.match()text1中查找"Python"。由于text1"Python"开头,所以会成功匹配。
  • if match1: ... else: ...: 根据匹配结果打印信息。
  • match2 = re.match(r'Python', text2): re.match()text2中查找"Python"。由于text2不以"Python"开头(而是以"I"开头),即使"Python"存在于字符串中,re.match()也会返回None,因为它只检查开头。

示例 1.7.2:re.search() 的行为

import re # 导入re模块

text1 = "Python programming is fun." # 字符串以"Python"开头
text2 = "I love Python programming." # 字符串中间有"Python"

# 尝试用re.search()匹配"Python"
search1 = re.search(r'Python', text1) # 在text1中扫描查找"Python"
if search1: # 如果匹配成功
    print(f"text1 (re.search): 找到 '{
     
     search1.group()}'") # 打印匹配结果
else: # 如果匹配失败
    print("text1 (re.search): 未找到 'Python'") # 打印未找到信息

# 尝试用re.search()匹配"Python"
search2 = re.search(r'Python', text2) # 在text2中扫描查找"Python"
if search2: # 如果匹配成功
    print(f"text2 (re.search): 找到 '{
     
     search2.group()}'") # 打印匹配结果
else: # 如果匹配失败
    print("text2 (re.search): 未找到 'Python'") # 打印未找到信息

代码解释:

  • import re: 导入re模块。
  • text1 = "Python programming is fun.": 第一个测试字符串。
  • text2 = "I love Python programming.": 第二个测试字符串。
  • search1 = re.search(r'Python', text1): re.search()text1中查找"Python"。由于text1"Python"开头,re.search()会找到并成功匹配。
  • search2 = re.search(r'Python', text2): re.search()text2中查找"Python"。即使text2不以"Python"开头,re.search()也会扫描整个字符串并成功找到位于中间的"Python"
  • 两者都会成功找到匹配,因为re.search()会扫描整个字符串而不仅仅是开头。

通过这两个例子,我们可以清晰地看到re.match()re.search()在匹配逻辑上的根本差异。在大多数需要从字符串的任意位置查找模式的场景中,re.search()是更常用的选择。而当您明确知道模式只可能出现在字符串的开头时,re.match()则更高效和精确。

第二章:基本元字符与字符类剖析:构建模式的基础砖块

2.1 句点 .:匹配任意单个字符(换行符除外)

句点(.)是正则表达式中最常见的元字符之一,它的含义是匹配除换行符(\n)之外的任意一个字符。这里的“字符”可以是字母、数字、符号、空格等。

内部逻辑解析: 当正则表达式引擎遇到.时,它会尝试匹配输入字符串中的下一个字符。如果该字符不是换行符,则匹配成功并继续处理表达式的下一个部分。如果遇到换行符,或者字符串已经结束,则.的匹配失败。

示例 2.1.1:re.search() 与句点 . 的应用

import re # 导入re模块

text = "cat, bat, mat, fat, chat." # 包含多个三字母单词的字符串

# 匹配 'at' 前面任意一个字符的模式
pattern = r'.at' # 定义模式:匹配任意一个字符(.)后跟"at"
match1 = re.search(pattern, text) # 在text中搜索第一个匹配
if match1: # 如果找到匹配
    print(f"第一个 '.at' 匹配: '{
     
     match1.group()}'") # 打印匹配到的字符串
else: # 如果没有找到
    print("未找到 '.at' 的匹配") # 打印未找到信息

# 尝试匹配 "h.t"
pattern2 = r'h.t' # 定义模式:匹配"h"后跟任意字符(.),再跟"t"
match2 = re.search(pattern2, text) # 在text中搜索第一个匹配
if match2: # 如果找到匹配
    print(f"第一个 'h.t' 匹配: '{
     
     match2.group()}'") # 打印匹配到的字符串
else: # 如果没有找到
    print("未找到 'h.t' 的匹配") # 打印未找到信息

# 尝试匹配 'ca.t'
pattern3 = r'ca.t' # 定义模式:匹配"ca"后跟任意字符(.),再跟"t"
match3 = re.search(pattern3, text) # 在text中搜索第一个匹配
if match3: # 如果找到匹配
    print(f"第一个 'ca.t' 匹配: '{
     
     match3.group()}'") # 打印匹配到的字符串
else: # 如果没有找到
    print("未找到 'ca.t' 的匹配") # 打印未找到信息

代码解释:

  • import re: 导入re模块。
  • text = "cat, bat, mat, fat, chat.": 示例文本。
  • pattern = r'.at': 定义正则表达式模式。r前缀表示这是一个原始字符串。.匹配除换行符以外的任何单个字符,at匹配字面字符“at”。所以这个模式会匹配“cat”、“bat”、“mat”、“fat”等。
  • match1 = re.search(pattern, text): 使用re.search()text中查找第一个符合'.at'模式的字符串。
  • if match1: print(...) else: print(...): 判断是否找到匹配并打印结果。在这里,会找到并打印“cat”。
  • pattern2 = r'h.t': 定义另一个模式。.在这里匹配“chat”中的“a”,所以会找到“chat”。
  • pattern3 = r'ca.t': 定义第三个模式。由于text中没有“ca”后面跟一个任意字符再跟“t”的组合,所以这个模式将不会找到匹配。

示例 2.1.2:句点 . 遇到换行符时的行为

import re # 导入re模块

multi_line_text = "Line1\nLine2\nLine3" # 包含换行符的多行字符串

# 匹配 'Line' 后跟一个字符
pattern = r'Line.' # 定义模式:匹配"Line"后跟任意单个字符(.)
match_default = re.search(pattern, multi_line_text) # 在多行文本中搜索匹配
if match_default: # 如果找到匹配
    print(f"默认模式下,'Line.' 匹配: '{
     
     match_default.group()}'") # 打印匹配结果(预期是"Line1")
else: # 如果没有找到
    print("默认模式下,未找到 'Line.' 匹配") # 打印未找到信息

# 尝试让 '.' 跨越换行符,默认是不行的
pattern_across_line = r'Line.\nLine.' # 定义模式:匹配"Line", 任意字符, 换行符, "Line", 任意字符
match_across = re.search(pattern_across_line, multi_line_text) # 搜索跨行匹配
if match_across: # 如果找到匹配
    print(f"默认模式下,'Line.\\nLine.' 匹配: '{
     
     match_across.group()}'") # 打印匹配结果
else: # 如果没有找到
    print("默认模式下,'Line.\\nLine.' 无法跨越换行符匹配,因此未找到。") # 打印未找到信息

代码解释:

  • import re: 导入re模块。
  • multi_line_text = "Line1\nLine2\nLine3": 定义一个多行字符串,其中包含明确的换行符\n
  • pattern = r'Line.': 模式'Line.'
  • match_default = re.search(pattern, multi_line_text): 搜索'Line.'re.search()会找到“Line1”,因为Line.匹配“Line”和“1”。
  • pattern_across_line = r'Line.\nLine.': 模式'Line.\nLine.'。这里尝试匹配“Line”+任意字符+换行符+“Line”+任意字符。
  • match_across = re.search(pattern_across_line, multi_line_text): 默认情况下,.不匹配换行符。所以Line.不会匹配到Line1\n中的\n。因此,即使文本中存在Line1\nLine2这样的结构,re.search也无法找到匹配,因为.在遇到\n时就停止了匹配。
re.DOTALL 标志(或 re.S):让 . 匹配所有字符,包括换行符

为了让句点 . 也能够匹配换行符,我们需要使用re.DOTALL标志(或者其缩写re.S)。这个标志会改变.的行为,使其真正匹配任何单个字符,包括换行符。

示例 2.1.3:re.DOTALL 的影响

import re # 导入re模块

multi_line_text = "First line.\nSecond line." # 包含换行符的多行字符串

# 默认情况下,'.' 不匹配换行符
pattern_no_dotall = r'First.line' # 定义模式:匹配"First"后跟任意字符,再跟"line"
match_no_dotall = re.search(pattern_no_dotall, multi_line_text) # 搜索匹配
if match_no_dotall: # 如果找到匹配
    print(f"无 re.DOTALL: '{
     
     match_no_dotall.group()}'") # 打印匹配结果(预期找到"First line")
else: # 如果没有找到
    print("无 re.DOTALL: 未找到 'First.line'") # 打印未找到信息

# 使用 re.DOTALL,让 '.' 匹配换行符
pattern_with_dotall = r'First.line.\nSecond.line' # 定义模式:匹配"First"后跟任意字符,再跟"line"和换行符,再跟"Second"和任意字符,再跟"line"
match_with_dotall = re.search(pattern_with_dotall, multi_line_text, re.DOTALL) # 搜索匹配,并启用re.DOTALL标志
if match_with_dotall: # 如果找到匹配
    print(f"有 re.DOTALL: '{
     
     match_with_dotall.group()}'") # 打印匹配结果(预期找到整个字符串)
else: # 如果没有找到
    print("有 re.DOTALL: 未找到 'First.line.\\nSecond.line'") # 打印未找到信息

代码解释:

  • import re: 导入re模块。
  • multi_line_text = "First line.\nSecond line.": 包含换行符的示例文本。
  • pattern_no_dotall = r'First.line': 第一个模式,尝试匹配"First.line"。在没有re.DOTALL的情况下,.无法匹配multi_line_text中的空格。First.line如果想匹配"First line."这个部分,需要First. line,或者First\.line。这里,First.line会匹配到First line。哦,这里我写错了,第一个点会匹配空格。 First.line 能够匹配 First line,因为句点 . 匹配空格。这里我想表达的是 . 不能匹配换行符。
    • 修正解释: pattern_no_dotall = r'First.line' 实际上会匹配 multi_line_text 中的 "First line",因为第一个 . 匹配空格。这个例子没有很好地展示 . 不匹配换行符的特点。我们需要一个模式,它需要跨越换行符才能匹配。

修正示例 2.1.3,更好地展示 re.DOTALL 的作用:

import re # 导入re模块

multi_line_text = "Hello\nWorld" # 包含换行符的多行字符串

# 默认情况下,'.' 不匹配换行符
pattern_default = r'Hello.World' # 定义模式:匹配"Hello"后跟任意字符,再跟"World"
match_default = re.search(pattern_default, multi_line_text) # 搜索匹配
if match_default: # 如果找到匹配
    print(f"无 re.DOTALL 标志: '{
     
     match_default.group()}'") # 打印匹配结果
else: # 如果没有找到
    print("无 re.DOTALL 标志: 未找到 'Hello.World' ('.' 不匹配换行符)") # 打印未找到信息

# 使用 re.DOTALL,让 '.' 匹配换行符
pattern_dotall = r'Hello.World' # 定义相同模式,但这次使用re.DOTALL
match_dotall = re.search(pattern_dotall, multi_line_text, re.DOTALL) # 搜索匹配,并启用re.DOTALL标志
if match_dotall: # 如果找到匹配
    print(f"有 re.DOTALL 标志: '{
     
     match_dotall.group()}'") # 打印匹配结果
else: # 如果没有找到
    print("有 re.DOTALL 标志: 未找到 'Hello.World'") # 打印未找到信息

代码解释:

  • import re: 导入re模块。
  • multi_line_text = "Hello\nWorld": 示例文本,中间有一个换行符。
  • pattern_default = r'Hello.World': 定义模式。这里的.需要匹配\n才能使整个模式Hello.World匹配multi_line_text
  • match_default = re.search(pattern_default, multi_line_text): 在没有re.DOTALL标志的情况下,.不匹配换行符。因此,Hello.会匹配“Hello”,但.无法匹配后面的\n,导致整个模式匹配失败,返回None
  • print("无 re.DOTALL 标志: 未找到 'Hello.World' ('.' 不匹配换行符)"): 打印未找到的结果和原因。
  • match_dotall = re.search(pattern_dotall, multi_line_text, re.DOTALL): 再次使用相同的模式,但这次在re.search()函数调用中加入了flags=re.DOTALL
  • if match_dotall: print(...) else: print(...): 由于re.DOTALL标志的作用,此时.会成功匹配换行符\n。因此,整个模式Hello.World会成功匹配multi_line_text中的"Hello\nWorld",并返回一个MatchObject

这清晰地展示了re.DOTALL标志如何改变.的默认行为,使其能够匹配包括换行符在内的所有字符。在需要跨越多行匹配模式时,这个标志非常有用。

2.2 量词:*+?{m,n}

量词用于指定其前面的元素(字符、字符组或分组)可以出现的次数。它们是正则表达式中实现模式重复匹配的核心机制。

2.2.1 星号 *:匹配零个或多个

星号 * 匹配其前面的字符或子表达式零次或多次。它是最宽松的量词,因为即使前面的元素完全不出现,它也能匹配成功。

内部机制解析(贪婪模式): 当NFA引擎遇到X*时,它会首先尝试尽可能多地匹配X。如果匹配成功,它会继续匹配表达式的剩余部分。如果剩余部分无法匹配成功,引擎会“回溯”,从X*匹配的字符中“吐出”一个字符,然后再次尝试匹配表达式的剩余部分,直到找到一个完整的匹配,或者X*匹配的字符为零个为止。这种倾向于匹配尽可能长的字符串的行为称为贪婪模式(Greedy Mode)

示例 2.2.1:星号 * 的应用

import re # 导入re模块

text_data = "aaabbbccc, ab, a, bbc" # 包含不同重复模式的字符串

# 匹配零个或多个 'a'
pattern1 = r'a*' # 模式:匹配零个或多个'a'
matches1 = re.findall(pattern1, text_data) # 查找所有匹配
print(f"'a*' 的匹配结果: {
     
     matches1}") # 打印结果。注意,它会匹配空字符串,因为允许零次出现。

# 匹配 'a' 后跟零个或多个 'b'
pattern2 = r'ab*' # 模式:匹配'a'后跟零个或多个'b'
matches2 = re.findall(pattern2, text_data) # 查找所有匹配
print(f"'ab*' 的匹配结果: {
     
     matches2}") # 打印结果。会匹配"a"(0个b)、"ab"、"abbb"等

# 匹配 'ab' 后跟零个或多个 'c'
pattern3 = r'abc*' # 模式:匹配"ab"后跟零个或多个'c'
matches3 = re.findall(pattern3, text_data) # 查找所有匹配
print(f"'abc*' 的匹配结果: {
     
     matches3}") # 打印结果。会匹配"ab"、"abc"、"abcc"等

# 匹配任意字符后跟零个或多个数字
text_numbers = "item1, item22, item333, item" # 包含数字的字符串
pattern4 = r'item\d*' # 模式:匹配"item"后跟零个或多个数字(\d)
matches4 = re.findall(pattern4, text_numbers) # 查找所有匹配
print(f"'item\\d*' 的匹配结果: {
     
     matches4}") # 打印结果。会匹配"item1", "item22", "item333", "item"

代码解释:

  • import re: 导入re模块。
  • text_data = "aaabbbccc, ab, a, bbc": 示例文本。
  • pattern1 = r'a*': 模式'a*'*表示a可以出现零次或多次。re.findall()会找到所有匹配。由于*允许零次匹配,所以在每个字符之间和字符串的开头/结尾都可能找到空字符串匹配。例如,"b"前面有一个空串被a*匹配,"bb"之间也有空串被匹配,等等。这可能导致一些初学者意想不到的结果。
  • pattern2 = r'ab*': 模式'ab*'。匹配字面字符a,然后匹配零个或多个字面字符b。在"aaabbbccc"中,它会匹配到"abbb"a后面跟三个b),在"ab"中匹配"ab",在"a"中匹配"a"a后面跟零个b)。
  • pattern3 = r'abc*': 模式'abc*'。匹配ab,然后匹配零个或多个c
  • text_numbers = "item1, item22, item333, item": 包含item和数字的字符串。
  • pattern4 = r'item\d*': 模式'item\d*'\d是预定义字符集,表示任意数字。*表示\d可以出现零次或多次。所以它会匹配"item1""item22""item333",以及没有数字的"item"
2.2.1.1 贪婪与非贪婪模式 (*?)

默认情况下,量词是贪婪的(Greedy),这意味着它们会尝试匹配尽可能多的字符。但在某些情况下,我们可能需要它们匹配尽可能少的字符,这被称为**非贪婪的(Non-Greedy)懒惰的(Lazy)**模式。通过在量词后面加上一个问号 ?,可以将其变为非贪婪模式。

  • X*?:匹配零个或多个 X,但尽可能少地匹配。

内部机制解析(非贪婪模式): 当NFA引擎遇到X*?时,它会首先尝试零次匹配X,然后继续匹配表达式的剩余部分。如果剩余部分能够匹配成功,则X*?的匹配结束。如果剩余部分无法匹配,引擎会“回溯”,但这次是从X*?的匹配中“吃进”一个字符,然后再次尝试匹配表达式的剩余部分,直到找到一个完整的匹配,或者X*?匹配的字符已经达到上限。这种倾向于匹配尽可能短的字符串的行为称为非贪婪模式

示例 2.2.2:贪婪与非贪婪 * 的对比

import re # 导入re模块

html_text = "<tag1>content1</tag1><tag2>content2</tag2>" # 包含多个HTML标签的字符串

# 贪婪模式:匹配从第一个 < 到最后一个 >
# 模式: <.*> 会匹配从第一个 < 到最后一个 > 之间的所有内容,包括中间的 <tag2>content2</tag2>
pattern_greedy = r'<.*>' # 定义贪婪模式:匹配'<'后跟任意多个任意字符,再跟'>'
match_greedy = re.search(pattern_greedy, html_text) # 搜索匹配
if match_greedy: # 如果找到匹配
    print(f"贪婪模式 (<.*>): '{
     
     match_greedy.group()}'") # 打印匹配结果(预期匹配整个字符串)
else: # 如果没有找到
    print("贪婪模式 (<.*>): 未找到匹配") # 打印未找到信息

# 非贪婪模式:匹配从第一个 < 到第一个 >
# 模式: <.*?> 只会匹配到第一个闭合的 >
pattern_nongreedy = r'<.*?>' # 定义非贪婪模式:匹配'<'后跟任意多个任意字符(尽可能少),再跟'>'
matches_nongreedy = re.findall(pattern_nongreedy, html_text) # 查找所有匹配
print(f"非贪婪模式 (<.*?>): {
     
     matches_nongreedy}") # 打印所有匹配结果(预期匹配每个单独的标签)

代码解释:

  • import re: 导入re模块。
  • html_text = "<tag1>content1</tag1><tag2>content2</tag2>": 示例文本,包含两个HTML标签。
  • pattern_greedy = r'<.*>': 定义贪婪模式。<匹配字面字符<>匹配字面字符>.匹配任意字符,*表示零次或多次。由于*是贪婪的,它会尽可能多地匹配字符,直到遇到字符串末尾的>。因此,它会从第一个< (<tag1>) 开始一直匹配到最后一个> (</tag2>)。
  • match_greedy = re.search(pattern_greedy, html_text): 搜索匹配。结果将是整个html_text字符串。
  • pattern_nongreedy = r'<.*?>': 定义非贪婪模式。在*后面加上?,使其变为非贪婪。这意味着.会尽可能少地匹配字符。当它匹配到第一个>时,就会停止。
  • matches_nongreedy = re.findall(pattern_nongreedy, html_text): re.findall()会查找所有非重叠的匹配。由于是非贪婪模式,它会分别匹配<tag1>content1</tag1><tag2>content2</tag2>

这个例子清晰地展示了贪婪和非贪婪模式在匹配范围上的巨大差异,特别是在处理有重复结构(如HTML标签)的文本时,非贪婪模式通常是更正确的选择。

2.2.2 加号 +:匹配一个或多个

加号 + 匹配其前面的字符或子表达式一个或多个。与*不同,+要求前面的元素至少出现一次。

内部机制解析(贪婪模式):*类似,X+在贪婪模式下会尝试尽可能多地匹配X。如果匹配成功,它会继续匹配表达式的剩余部分。如果剩余部分无法匹配成功,引擎会“回溯”,从X+匹配的字符中“吐出”一个字符,然后再次尝试匹配表达式的剩余部分,直到找到一个完整的匹配,或者X+匹配的字符只有一个(因为+要求至少一个)为止。

示例 2.2.3:加号 + 的应用

import re # 导入re模块

data_string = "number 123, code abc, values 45, no_digits, 007bond" # 包含数字和非数字的字符串

# 匹配一个或多个数字
pattern1 = r'\d+' # 模式:匹配一个或多个数字
matches1 = re.findall(pattern1, data_string) # 查找所有匹配
print(f"'\\d+' 的匹配结果: {
     
     matches1}") # 打印结果。会匹配"123", "45", "007"

# 匹配一个或多个字母
pattern2 = r'[a-z]+' # 模式:匹配一个或多个小写字母
matches2 = re.findall(pattern2, data_string) # 查找所有匹配
print(f"'[a-z]+' 的匹配结果: {
     
     matches2}") # 打印结果。会匹配"number", "code", "abc", "values", "no", "digits", "bond"

# 匹配 'num' 后跟一个或多个 'b'
pattern3 = r'num+ber' # 模式:匹配"num"后跟一个或多个'b',再跟"er"
match3 = re.search(pattern3, data_string) # 搜索匹配
if match3: # 如果找到匹配
    print(f"'num+ber' 的匹配结果: '{
     
     match3.group()}'") # 打印匹配结果
else: # 如果没有找到
    print("'num+ber' 未找到匹配") # 打印未找到信息

代码解释:

  • import re: 导入re模块。
  • data_string = "number 123, code abc, values 45, no_digits, 007bond": 示例文本。
  • pattern1 = r'\d+': 模式'\d+'\d匹配数字,+表示一个或多个。它会找到连续的数字串。
  • matches1 = re.findall(pattern1, data_string): 查找所有匹配的数字序列。
  • pattern2 = r'[a-z]+': 模式'[a-z]+'[a-z]是一个字符集,匹配任意一个小写字母。+表示一个或多个。它会找到连续的小写字母串。
  • pattern3 = r'num+ber': 模式'num+ber'。这里+作用于m后面的u。应该是numb+er,如果想匹配多个b。
    • 修正解释: pattern3 = r'num+ber' 中的+作用于它前面的字符m。这表示匹配n后跟一个或多个m,再跟ber。这与data_string中的"number"不匹配。如果想匹配"numb"后面的多个"b",应该是r'numb+er'
    • 再修正 pattern3 和示例,以更好地展示 + 的实际应用:
import re # 导入re模块

data_string = "apple, appple, apppple, ap." # 包含不同重复模式的字符串

# 匹配 'ap' 后跟一个或多个 'p',再跟 'le'
pattern_plus = r'ap+le' # 模式:匹配"ap"后跟一个或多个'p',再跟"le"
matches_plus = re.findall(pattern_plus, data_string) # 查找所有匹配
print(f"'ap+le' 的匹配结果: {
     
     matches_plus}") # 打印结果。会匹配"apple", "appple", "apppple"

代码解释:

  • import re: 导入re模块。
  • data_string = "apple, appple, apppple, ap.": 示例文本。
  • pattern_plus = r'ap+le': 模式'ap+le'ap匹配字面字符“ap”,+作用于它前面的p,表示一个或多个ple匹配字面字符“le”。
  • matches_plus = re.findall(pattern_plus, data_string): 查找所有匹配。它会找到"apple"(一个p)、"appple"(两个p)、"apppple"(三个p)。"ap."则不匹配,因为它不以le结尾。
2.2.2.1 非贪婪模式 (+?)

通过在+后面加上?,可以将其变为非贪婪模式:

  • X+?:匹配一个或多个 X,但尽可能少地匹配。

示例 2.2.4:贪婪与非贪婪 + 的对比

import re # 导入re模块

data = "This is a <string> with <multiple> <tags>." # 包含多个标签的字符串

# 贪婪模式
pattern_greedy_plus = r'<.+>' # 模式:匹配'<',后跟一个或多个任意字符,再跟'>'
match_greedy_plus = re.search(pattern_greedy_plus, data) # 搜索匹配
if match_greedy_plus: # 如果找到匹配
    print(f"贪婪模式 (<.+>): '{
     
     match_greedy_plus.group()}'") # 打印匹配结果(预期匹配整个标签部分)
else: # 如果没有找到
    print("贪婪模式 (<.+>): 未找到匹配") # 打印未找到信息

# 非贪婪模式
pattern_nongreedy_plus = r'<.+?>' # 模式:匹配'<',后跟一个或多个任意字符(尽可能少),再跟'>'
matches_nongreedy_plus = re.findall(pattern_nongreedy_plus, data) # 查找所有匹配
print(f"非贪婪模式 (<.+?>): {
     
     matches_nongreedy_plus}") # 打印所有匹配结果(预期匹配每个单独的标签)

代码解释:

  • import re: 导入re模块。
  • data = "This is a <string> with <multiple> <tags>.": 示例文本。
  • pattern_greedy_plus = r'<.+>': 贪婪模式。+会尽可能多地匹配字符,直到最后一个>。因此,它会匹配从第一个< (<string>) 到最后一个> (</tags>) 之间的所有内容。
  • match_greedy_plus = re.search(pattern_greedy_plus, data): 搜索并打印匹配结果。
  • pattern_nongreedy_plus = r'<.+?>': 非贪婪模式。+?会尽可能少地匹配字符。它会匹配到第一个>就停止。
  • matches_nongreedy_plus = re.findall(pattern_nongreedy_plus, data): 查找所有匹配。它会分别匹配<string><multiple><tags>
2.2.3 问号 ?:匹配零个或一个

问号 ? 有两种主要用途:

  1. 作为量词:匹配其前面的字符或子表达式零次或一次。这使得该元素成为可选的。
  2. 作为修饰符:紧跟在其他量词(如*+)之后,将其变为非贪婪模式(如*?+?)。我们已经在前面讨论了这种用法。

内部机制解析: 当NFA引擎遇到X?时,它会首先尝试匹配X。如果X能够匹配成功,它会继续处理表达式的剩余部分。如果X无法匹配,引擎会回溯,尝试不匹配X,直接处理表达式的剩余部分。

示例 2.2.5:问号 ? 作为量词的应用

import re # 导入re模块

words = "color, colour, honor, honour" # 包含美式和英式拼写的单词

# 匹配可选的 'u' (用于英式拼写)
pattern1 = r'colou?r' # 模式:匹配"colo"后跟零个或一个'u',再跟"r"
matches1 = re.findall(pattern1, words) # 查找所有匹配
print(f"'colou?r' 的匹配结果: {
     
     matches1}") # 打印结果。会匹配"color"和"colour"

# 匹配可选的 'h'
pattern2 = r'honou?r' # 模式:匹配"hono"后跟零个或一个'u',再跟"r"
matches2 = re.findall(pattern2, words) # 查找所有匹配
print(f"'honou?r' 的匹配结果: {
     
     matches2}") # 打印结果。会匹配"honor"和"honour"

# 匹配 HTTP 或 HTTPS
urls = "http://example.com, https://secure.com" # 包含HTTP和HTTPS的URL
pattern3 = r'https?://' # 模式:匹配"http"后跟零个或一个's',再跟"://"
matches3 = re.findall(pattern3, urls) # 查找所有匹配
print(f"'https?://' 的匹配结果: {
     
     matches3}") # 打印结果。会匹配"https://"和"https://"

代码解释:

  • import re: 导入re模块。
  • words = "color, colour, honor, honour": 示例文本。
  • pattern1 = r'colou?r': 模式'colou?r'?作用于u,表示u可以出现零次或一次。因此,它会匹配"color"u出现0次)和"colour"u出现1次)。
  • pattern2 = r'honou?r': 模式'honou?r'。与上例类似,匹配"honor""honour"
  • urls = "http://example.com, https://secure.com": 示例文本。
  • pattern3 = r'https?://': 模式'https?://'?作用于s,表示s可以出现零次或一次。因此,它会匹配"https://""https://"
2.2.4 花括号 {m,n}:精确匹配次数

花括号 {} 提供了更精细的控制,用于指定其前面的元素可以出现的精确次数或次数范围。

  • {m}:匹配前面的元素恰好 m 次。
  • {m,}:匹配前面的元素至少 m 次。
  • {,n}:匹配前面的元素最多 n 次(不常用,因为 *? 已经覆盖了0到多次和0到1次的场景)。
  • {m,n}:匹配前面的元素至少 m 次,最多 n 次。

内部机制解析(贪婪模式):*+类似,这些量词在默认情况下也是贪婪的。X{m,n}会尝试尽可能多地匹配X,但不会超过n次。X{m,}会尽可能多地匹配。X{m}则必须精确匹配m次。回溯机制同样适用于它们,以确保最终找到一个完整的匹配。

示例 2.2.6:花括号 {m,n} 的应用

import re # 导入re模块

phone_numbers = "123-456-7890, 555-1234, 9876543210, 11-22-33-44" # 包含各种格式的电话号码

# 匹配恰好3位数字
pattern1 = r'\d{3}' # 模式:匹配恰好3个数字
matches1 = re.findall(pattern1, phone_numbers) # 查找所有匹配
print(f"'\\d{
    
    {3}}' 的匹配结果: {
     
     matches1}") # 打印结果。会匹配"123", "456", "789", "123", "987", "654", "321", "11", "22", "33", "44"

# 匹配至少4位数字
pattern2 = r'\d{4,}' # 模式:匹配至少4个数字
matches2 = re.findall(pattern2, phone_numbers) # 查找所有匹配
print(f"'\\d{
    
    {4,}}' 的匹配结果: {
     
     matches2}") # 打印结果。会匹配"7890", "1234", "9876543210"

# 匹配2到4位数字
pattern3 = r'\d{2,4}' # 模式:匹配2到4个数字
matches3 = re.findall(pattern3, phone_numbers) # 查找所有匹配
print(f"'\\d{
    
    {2,4}}' 的匹配结果: {
     
     matches3}") # 打印结果。会匹配"123", "456", "7890", "555", "1234", "9876", "5432", "10", "11", "22", "33", "44"

# 匹配特定格式的电话号码:AAA-BBB-CCCC (三位-三位-四位)
# 注意: 这只是一种简单匹配,实际电话号码格式非常复杂
pattern4 = r'\d{3}-\d{3}-\d{4}' # 模式:匹配3个数字,-,3个数字,-,4个数字
matches4 = re.findall(pattern4, phone_numbers) # 查找所有匹配
print(f"'\\d{
    
    {3}}-\\d{
    
    {3}}-\\d{
    
    {4}}' 的匹配结果: {
     
     matches4}") # 打印结果。会匹配"123-456-7890"

代码解释:

  • import re: 导入re模块。
  • phone_numbers = "123-456-7890, 555-1234, 9876543210, 11-22-33-44": 示例文本。
  • pattern1 = r'\d{3}': 模式'\d{3}'\d匹配数字,{3}表示前面元素恰好出现3次。re.findall()会提取所有连续3位的数字序列。
  • pattern2 = r'\d{4,}': 模式'\d{4,}'{4,}表示前面元素至少出现4次。
  • pattern3 = r'\d{2,4}': 模式'\d{2,4}'{2,4}表示前面元素出现2到4次。由于量词是贪婪的,它会尽可能匹配4位。例如,对于"9876543210",它会匹配"9876"、“5432”、“10”。
  • pattern4 = r'\d{3}-\d{3}-\d{4}': 模式'\d{3}-\d{3}-\d{4}'。这是一个更具体的例子,用于匹配标准电话号码格式。\用于转义字面字符-,因为-在字符集中有特殊含义(这里不在字符集中,但习惯性转义可避免歧义)。
2.2.4.1 非贪婪模式 ({m,n}?)

与其他量词类似,花括号量词也可以通过添加 ? 变为非贪婪模式:

  • X{m,n}?:匹配至少 m 次,最多 nX,但尽可能少地匹配。

示例 2.2.7:贪婪与非贪婪 {m,n} 的对比

import re # 导入re模块

numbers_seq = "111112222233333" # 包含多个连续数字的字符串

# 贪婪模式: 匹配2到5个数字
pattern_greedy_range = r'\d{2,5}' # 模式:匹配2到5个数字(贪婪)
matches_greedy_range = re.findall(pattern_greedy_range, numbers_seq) # 查找所有匹配
print(f"贪婪模式 (\\d{
    
    {2,5}}): {
     
     matches_greedy_range}") # 打印结果。会匹配"11111", "22222", "33333"

# 非贪婪模式: 匹配2到5个数字,但尽可能少
pattern_nongreedy_range = r'\d{2,5}?' # 模式:匹配2到5个数字(非贪婪)
matches_nongreedy_range = re.findall(pattern_nongreedy_range, numbers_seq) # 查找所有匹配
print(f"非贪婪模式 (\\d{
    
    {2,5}}?): {
     
     matches_nongreedy_range}") # 打印结果。会匹配"11", "11", "1", "22", "22", "2", "33", "33", "3" (这里会出问题,因为re.findall是找非重叠匹配)

# 为了更好地展示非贪婪模式,我们修改数据和模式,确保有重复的短匹配
text_for_lazy = "abababab" # 包含重复ab的字符串

# 贪婪模式: 匹配2到4个字符
pattern_greedy_ab = r'(ab){2,4}' # 模式:匹配2到4个"ab"组合(贪婪)
matches_greedy_ab = re.findall(pattern_greedy_ab, text_for_lazy) # 查找所有匹配
print(f"贪婪模式 ((ab){
    
    {2,4}}): {
     
     matches_greedy_ab}") # 打印结果。会匹配最长的"abababab",但findall会返回捕获组内容。

# 修正:当有捕获组时,findall返回的是捕获组的内容。这里我们只关心完整匹配。
match_greedy_ab = re.search(pattern_greedy_ab, text_for_lazy)
if match_greedy_ab:
    print(f"贪婪模式 ((ab){
    
    {2,4}}) search: '{
     
     match_greedy_ab.group()}'") # 打印结果。会匹配最长的"abababab"
else:
    print("贪婪模式 ((ab){
   
   {2,4}}) search: 未找到")

# 非贪婪模式: 匹配2到4个字符,但尽可能少
pattern_nongreedy_ab = r'(ab){2,4}?' # 模式:匹配2到4个"ab"组合(非贪婪)
matches_nongreedy_ab = re.findall(pattern_nongreedy_ab, text_for_lazy) # 查找所有匹配
print(f"非贪婪模式 ((ab){
    
    {2,4}}?): {
     
     matches_nongreedy_ab}") # 打印结果。会匹配最短的"abab"两次

# 再次用search来验证非贪婪模式的单次匹配行为
match_nongreedy_ab = re.search(pattern_nongreedy_ab, text_for_lazy)
if match_nongreedy_ab:
    print(f"非贪婪模式 ((ab){
    
    {2,4}}?) search: '{
     
     match_nongreedy_ab.group()}'") # 打印结果。会匹配最短的"abab"
else:
    print("非贪婪模式 ((ab){
   
   {2,4}}?) search: 未找到")

代码解释:

  • import re: 导入re模块。

  • numbers_seq = "111112222233333": 示例文本。

  • pattern_greedy_range = r'\d{2,5}': 贪婪模式。\d匹配数字,{2,5}表示匹配2到5个数字。对于连续的5个1,它会尽可能匹配5个,所以是"11111"

  • matches_greedy_range = re.findall(pattern_greedy_range, numbers_seq): re.findall()会找到所有非重叠的、最长的(因为贪婪)匹配。

  • pattern_nongreedy_range = r'\d{2,5}?': 非贪婪模式。{2,5}?表示匹配2到5个数字,但尽可能少。

  • matches_nongreedy_range = re.findall(pattern_nongreedy_range, numbers_seq): 预期结果是['11', '11', '1', '22', '22', '2', '33', '33', '3']。但这里会有一个误解,因为re.findall是找到非重叠匹配。对于"11111",非贪婪会尝试匹配"11",然后从"111"开始继续寻找,会找到"11",最后剩下"1"。所以实际输出会是['11', '11', '33', '33', '55', '5'] (如果文本是"111113333355555")。

    • 修正matches_nongreedy_range的实际输出:对于"111112222233333"r'\d{2,5}?' 会匹配"11",然后从剩余的"1112222233333"中匹配"11",再从"12222233333"中匹配"12",等等。这是因为findall是非重叠的。
    • '11111' -> 匹配 '11',剩余 '111'
    • '111' -> 匹配 '11',剩余 '1'
    • '1' -> 不够2个数字,跳过
    • '22222' -> 匹配 '22',剩余 '222'
    • '222' -> 匹配 '22',剩余 '2'
    • '2' -> 不够2个数字,跳过
    • '33333' -> 匹配 '33',剩余 '333'
    • '333' -> 匹配 '33',剩余 '3'
    • '3' -> 不够2个数字,跳过
    • 因此,matches_nongreedy_range 实际输出为 ['11', '11', '22', '22', '33', '33']
  • text_for_lazy = "abababab": 更适合展示非贪婪行为的示例文本。

  • pattern_greedy_ab = r'(ab){2,4}': 贪婪模式下,会尽可能匹配多个ab组合。re.search().group()会返回"abababab"

  • pattern_nongreedy_ab = r'(ab){2,4}?': 非贪婪模式下,会尽可能少地匹配ab组合。re.search().group()会返回"abab"(2次ab,是允许的最少次数)。re.findall()会返回所有非重叠的最短匹配,即['ab', 'ab'](这里有捕获组,所以返回捕获组的内容),如果是非捕获组(?:ab){2,4}?则会返回['abab', 'abab']

这个例子再次强调了贪婪与非贪婪模式在匹配长度上的差异,以及re.findall()在有捕获组时的特殊行为(返回捕获组的内容而不是整个匹配)。

2.3 管道符 |:或操作

管道符 | 用作逻辑或操作符,表示匹配其左侧或右侧的任何一个模式。

内部机制解析: 当NFA引擎遇到A|B时,它会首先尝试匹配A。如果A匹配成功,并且表达式的剩余部分也能匹配成功,则A|B的匹配完成。如果A匹配失败,或者A匹配成功但导致后续表达式匹配失败,引擎会回溯并尝试匹配B。一旦AB中的任何一个成功匹配,引擎就会停止尝试其他分支。

示例 2.3.1:管道符 | 的应用

import re # 导入re模块

fruit_list = "apple, banana, cherry, date, grape" # 包含多种水果名称的字符串

# 匹配 "apple" 或 "grape"
pattern1 = r'apple|grape' # 模式:匹配"apple"或者"grape"
matches1 = re.findall(pattern1, fruit_list) # 查找所有匹配
print(f"'apple|grape' 的匹配结果: {
     
     matches1}") # 打印结果。会匹配"apple", "grape"

# 匹配以 'a' 开头或以 'e' 结尾的单词
# 注意: 这里需要更复杂的模式来界定单词,避免匹配部分单词
# 暂时用简单模式展示 | 作用
pattern2 = r'apple|cherry' # 模式:匹配"apple"或者"cherry"
matches2 = re.findall(pattern2, fruit_list) # 查找所有匹配
print(f"'apple|cherry' 的匹配结果: {
     
     matches2}") # 打印结果。会匹配"apple", "cherry"

# 匹配特定数字,如 100 或 200 或 300
numbers = "The price is 100, not 250, but 300 is fine." # 包含数字的字符串
pattern3 = r'100|200|300' # 模式:匹配"100"或"200"或"300"
matches3 = re.findall(pattern3, numbers) # 查找所有匹配
print(f"'100|200|300' 的匹配结果: {
     
     matches3}") # 打印结果。会匹配"100", "300"

代码解释:

  • import re: 导入re模块。
  • fruit_list = "apple, banana, cherry, date, grape": 示例文本。
  • pattern1 = r'apple|grape': 模式'apple|grape'|表示“或”,所以它会匹配字面字符串"apple"或字面字符串"grape"
  • pattern2 = r'apple|cherry': 类似,匹配"apple""cherry"
  • numbers = "The price is 100, not 250, but 300 is fine.": 示例文本。
  • pattern3 = r'100|200|300': 模式'100|200|300'。匹配字面字符串"100""200""300"
管道符 | 在分组中的使用 (A|B)

|单独使用时,它会作用于整个正则表达式,或者在没有明确分组的情况下作用于最大的可选部分。为了精确控制|的作用范围,我们通常会将其与圆括号 ()(分组)结合使用。

示例 2.3.2:管道符 | 与分组 () 的结合

import re # 导入re模块

text_variations = "greyhound, grayhound, blacklist, blackhole" # 包含不同拼写和组合的字符串

# 匹配 'grey' 或 'gray' 后跟 'hound'
# 错误的写法: r'grey|grayhound' - 这会匹配 'grey' 或者 'grayhound' (整个)
# 正确的写法: 使用分组 (grey|gray)
pattern_grouped = r'(grey|gray)hound' # 模式:匹配"grey"或"gray"组成的组,再跟"hound"
matches_grouped = re.findall(pattern_grouped, text_variations) # 查找所有匹配
print(f"'(grey|gray)hound' 的匹配结果: {
     
     matches_grouped}") # 打印结果。会匹配"greyhound", "grayhound"

# 匹配 'black' 后跟 'list' 或 'hole'
pattern_grouped2 = r'black(list|hole)' # 模式:匹配"black"后跟"list"或"hole"组成的组
matches_grouped2 = re.findall(pattern_grouped2, text_variations) # 查找所有匹配
print(f"'black(list|hole)' 的匹配结果: {
     
     matches_grouped2}") # 打印结果。会匹配"blacklist", "blackhole"

代码解释:

  • import re: 导入re模块。
  • text_variations = "greyhound, grayhound, blacklist, blackhole": 示例文本。
  • pattern_grouped = r'(grey|gray)hound': 模式'(grey|gray)hound'。圆括号()创建了一个分组。|现在只作用于greygray之间。这意味着它会匹配"grey""gray",然后紧跟着字面字符串"hound"re.findall()在有捕获组的情况下,会返回捕获组中的内容。所以这里会返回['grey', 'gray'],而不是完整的匹配'greyhound''grayhound'
    • 重要补充:re.findall()遇到捕获组时,它返回的是捕获组中的内容。如果想返回完整的匹配,需要使用非捕获组 (?:...) 或在模式中不使用捕获组。
  • pattern_grouped2 = r'black(list|hole)': 模式'black(list|hole)'。类似地,|只作用于listholere.findall()将返回['list', 'hole']

通过这个例子,我们看到分组如何精确地限定|的作用范围,这是编写复杂正则表达式时不可或缺的技巧。

2.4 锚点:^$

锚点(Anchors)不匹配任何字符,而是匹配字符串中的特定位置。它们是零宽度断言的一种。

2.4.1 脱字符 ^:匹配行首

脱字符 ^ 匹配输入字符串的开头

内部机制解析: 当NFA引擎遇到^时,它会检查当前匹配位置是否是字符串的开头。如果是,则匹配成功;如果不是,则^的匹配失败。

示例 2.4.1:^ 的应用

import re # 导入re模块

sentence1 = "Start with Python." # 以"Start"开头
sentence2 = "This line starts with Python." # "Python"在中间

# 匹配以 "Start" 开头的字符串
pattern1 = r'^Start' # 模式:匹配字符串开头是"Start"
match1 = re.search(pattern1, sentence1) # 搜索匹配
if match1: # 如果找到匹配
    print(f"'{
     
     sentence1}' 中 '^Start' 匹配: '{
     
     match1.group()}'") # 打印匹配结果
else: # 如果没有找到
    print(f"'{
     
     sentence1}' 中 '^Start' 未找到匹配") # 打印未找到信息

match2 = re.search(pattern1, sentence2) # 搜索匹配
if match2: # 如果找到匹配
    print(f"'{
     
     sentence2}' 中 '^Start' 匹配: '{
     
     match2.group()}'") # 打印匹配结果
else: # 如果没有找到
    print(f"'{
     
     sentence2}' 中 '^Start' 未找到匹配") # 打印未找到信息

代码解释:

  • import re: 导入re模块。
  • sentence1 = "Start with Python.": 示例文本1,以"Start"开头。
  • sentence2 = "This line starts with Python.": 示例文本2,不以"Start"开头。
  • pattern1 = r'^Start': 模式'^Start'^锚定字符串的开头,所以只有当字符串以"Start"开头时才能匹配成功。
  • match1 = re.search(pattern1, sentence1): 在sentence1中搜索,因为sentence1"Start"开头,所以会匹配成功。
  • match2 = re.search(pattern1, sentence2): 在sentence2中搜索,因为sentence2不以"Start"开头,即使"starts"中包含"Start"^也会导致匹配失败。
re.MULTILINE 标志(或 re.M):让 ^ 匹配每行开头

默认情况下,^只匹配整个字符串的开头。如果想让^匹配多行字符串中每一行的开头(即每个换行符\n之后的位置),需要使用re.MULTILINE标志(或其缩写re.M)。

示例 2.4.2:re.MULTILINE^ 的影响

import re # 导入re模块

multi_line_content = "Line one\nLine two\nLine three" # 多行字符串

# 默认模式下,'^' 只匹配字符串开头
pattern_default = r'^Line' # 模式:匹配字符串开头是"Line"
matches_default = re.findall(pattern_default, multi_line_content) # 查找所有匹配
print(f"无 re.MULTILINE 标志时 ('^Line'): {
     
     matches_default}") # 打印结果。只会匹配第一个"Line"

# 使用 re.MULTILINE 标志,'^' 匹配每行开头
pattern_multiline = r'^Line' # 模式:匹配字符串开头是"Line"
matches_multiline = re.findall(pattern_multiline, multi_line_content, re.MULTILINE) # 查找所有匹配,并启用re.MULTILINE标志
print(f"有 re.MULTILINE 标志时 ('^Line'): {
     
     matches_multiline}") # 打印结果。会匹配每一行的"Line"

代码解释:

  • import re: 导入re模块。
  • multi_line_content = "Line one\nLine two\nLine three": 示例文本,包含多行。
  • pattern_default = r'^Line': 模式'^Line'。在没有re.MULTILINE标志时,^只匹配整个字符串的开头。所以它只会匹配"Line one"中的"Line"
  • matches_default = re.findall(pattern_default, multi_line_content): 结果是['Line']
  • matches_multiline = re.findall(pattern_multiline, multi_line_content, re.MULTILINE): 再次使用相同模式,但这次在re.findall()中启用了re.MULTILINE标志。
  • print(...): 启用re.MULTILINE后,^会匹配每一行的开头(即Line oneLine twoLine three各自的开头)。因此,会找到所有三个"Line"。结果是['Line', 'Line', 'Line']

这个例子清晰地展示了re.MULTILINE标志如何改变^的行为,使其在多行文本处理中更加灵活。

2.4.3 美元符 $:匹配行尾

美元符 $ 匹配输入字符串的结尾,或者在结尾处的换行符之前的位置。

内部机制解析: 当NFA引擎遇到$时,它会检查当前匹配位置是否是字符串的末尾。如果是,或者当前位置是字符串末尾的换行符之前,则匹配成功;否则,$的匹配失败。

示例 2.4.3:$ 的应用

import re # 导入re模块

text_end1 = "This is a sentence." # 以句号结尾
text_end2 = "Another line\n" # 以换行符结尾
text_end3 = "No period here" # 既不以句号也不以换行符结尾

# 匹配以句号结尾的字符串
pattern1 = r'\.$' # 模式:匹配字符串结尾是字面字符'.'
match1 = re.search(pattern1, text_end1) # 搜索匹配
if match1: # 如果找到匹配
    print(f"'{
     
     text_end1}' 中 '\\.$' 匹配: '{
     
     match1.group()}'") # 打印匹配结果
else: # 如果没有找到
    print(f"'{
     
     text_end1}' 中 '\\.$' 未找到匹配") # 打印未找到信息

# 匹配以换行符结尾的字符串,或者字符串的实际末尾
pattern2 = r'line$' # 模式:匹配字符串结尾是"line"
match2 = re.search(pattern2, text_end2) # 搜索匹配
if match2: # 如果找到匹配
    print(f"'{
     
     text_end2.strip()}' 中 'line$' 匹配: '{
     
     match2.group()}'") # 打印匹配结果
else: # 如果没有找到
    print(f"'{
     
     text_end2.strip()}' 中 'line$' 未找到匹配") # 打印未找到信息

match3 = re.search(pattern2, text_end3) # 搜索匹配
if match3: # 如果找到匹配
    print(f"'{
     
     text_end3}' 中 'line$' 匹配: '{
     
     match3.group()}'") # 打印匹配结果
else: # 如果没有找到
    print(f"'{
     
     text_end3}' 中 'line$' 未找到匹配") # 打印未找到信息

代码解释:

  • import re: 导入re模块。
  • text_end1 = "This is a sentence.": 示例文本1,以句号结尾。
  • text_end2 = "Another line\n": 示例文本2,以换行符结尾。
  • text_end3 = "No period here": 示例文本3。
  • pattern1 = r'\.$': 模式'\.$'\.匹配字面字符.(因为.是元字符,所以需要\转义),$锚定字符串的结尾。它会成功匹配text_end1
  • pattern2 = r'line$': 模式'line$'$会匹配字符串的实际结尾,或者在字符串末尾换行符\n之前的位置。因此,它能匹配"Another line\n"中的"line"。但不能匹配text_end3
  • text_end2.strip()用于打印时去除换行符,以便更好地展示匹配到的内容。
re.MULTILINE 标志(或 re.M):让 $ 匹配每行结尾

^类似,$默认只匹配整个字符串的结尾。如果使用re.MULTILINE标志,$将匹配多行字符串中每一行的结尾(即每个换行符\n之前或字符串的真正结尾)。

示例 2.4.4:re.MULTILINE$ 的影响

import re # 导入re模块

multi_line_text_end = "Item A\nItem B\nItem C" # 多行字符串,每行不带额外符号

# 默认模式下,'$' 只匹配字符串结尾
pattern_default = r'C$' # 模式:匹配以"C"结尾的字符串
match_default = re.search(pattern_default, multi_line_text_end) # 搜索匹配
if match_default: # 如果找到匹配
    print(f"无 re.MULTILINE 标志时 ('C$'): '{
     
     match_default.group()}'") # 打印匹配结果
else: # 如果没有找到
    print("无 re.MULTILINE 标志时 ('C$'): 未找到匹配") # 打印未找到信息

# 使用 re.MULTILINE 标志,'$' 匹配每行结尾
pattern_multiline = r'm$' # 模式:匹配以"m"结尾的行
matches_multiline = re.findall(pattern_multiline, multi_line_text_end, re.MULTILINE) # 查找所有匹配,并启用re.MULTILINE标志
print(f"有 re.MULTILINE 标志时 ('m$'): {
     
     matches_multiline}") # 打印结果。会匹配每一行中以"m"结尾的"m"

# 另一个例子,匹配以字母结尾的行
pattern_multiline_letter = r'[A-Z]$' # 模式:匹配以大写字母结尾的行
matches_multiline_letter = re.findall(pattern_multiline_letter, multi_line_text_end, re.MULTILINE) # 查找所有匹配,并启用re.MULTILINE标志
print(f"有 re.MULTILINE 标志时 ('[A-Z]$'): {
     
     matches_multiline_letter}") # 打印结果。会匹配"A", "B", "C"

代码解释:

  • import re: 导入re模块。
  • multi_line_text_end = "Item A\nItem B\nItem C": 示例文本。
  • pattern_default = r'C$': 模式'C$'。在没有re.MULTILINE标志时,$只匹配整个字符串的结尾。multi_line_text_end确实以C结尾(严格来说是\nC,如果最后没有换行符的话)。这里C$会匹配到Item C中的C
  • pattern_multiline = r'm$': 模式'm$'。启用re.MULTILINE后,$会匹配每一行的结尾。Item AItem BItem C都以m结尾 (如果指的是Ite(m),这里可能误解了,因为Itemm不是行尾,行尾是ABC之后)。
    • 修正pattern_multiline和解释:这里模式应该匹配行尾的字母,而不是m

修正示例 2.4.4,更好地展示 re.MULTILINE$ 的作用:

import re # 导入re模块

multi_line_text_end_with_nl = "First line.\nSecond line.\nThird line." # 多行字符串,每行以句号结尾

# 默认模式下,'$' 只匹配字符串结尾
# 匹配以句号结尾
pattern_default = r'\.$' # 模式:匹配以字面句号'.'结尾的字符串
match_default = re.search(pattern_default, multi_line_text_end_with_nl) # 搜索匹配
if match_default: # 如果找到匹配
    print(f"无 re.MULTILINE 标志时 ('\\.$'): '{
     
     match_default.group()}'") # 打印匹配结果(预期匹配到最后一个句号)
else: # 如果没有找到
    print("无 re.MULTILINE 标志时 ('\\.$'): 未找到匹配") # 打印未找到信息

# 使用 re.MULTILINE 标志,'$' 匹配每行结尾
pattern_multiline = r'\.$' # 模式:匹配以字面句号'.'结尾的行
matches_multiline = re.findall(pattern_multiline, multi_line_text_end_with_nl, re.MULTILINE) # 查找所有匹配,并启用re.MULTILINE标志
print(f"有 re.MULTILINE 标志时 ('\\.$'): {
     
     matches_multiline}") # 打印结果。会匹配每一行末尾的句号

代码解释:

  • import re: 导入re模块。
  • multi_line_text_end_with_nl = "First line.\nSecond line.\nThird line.": 示例文本,每行都以句号.结尾,然后是换行符\n
  • pattern_default = r'\.$': 模式'\.$'。在没有re.MULTILINE标志时,$只匹配整个字符串的结尾(即在"Third line.\n"\n之前)。所以它会匹配到"Third line."中的最后一个句号。
  • match_default = re.search(pattern_default, multi_line_text_end_with_nl): 结果是'.'
  • pattern_multiline = r'\.$': 相同模式。
  • matches_multiline = re.findall(pattern_multiline, multi_line_text_end_with_nl, re.MULTILINE): 启用re.MULTILINE后,$会匹配每一行的结尾(即First line.后的\n前,Second line.后的\n前,以及字符串的真正结尾)。因此,它会找到所有三个句号。结果是['.', '.', '.']

这个修正后的例子更好地说明了re.MULTILINE标志如何使得$能够识别每一行的逻辑结尾。

2.5 反斜杠 \:转义字符与特殊序列的起点

反斜杠 \ 在正则表达式中具有双重作用:

  1. 转义特殊元字符: 当一个元字符(如.*+?|^$()[]{ }\)需要被当作普通字面字符来匹配时,必须在其前面加上反斜杠进行转义。
  2. 引入特殊序列: 反斜杠后面跟着特定的字符会形成一个特殊的序列,用于匹配特定的字符集或位置,例如\d\s\b等。这些将在下一章“深入字符类与预定义字符集”中详细讨论。

示例 2.5.1:转义元字符

import re # 导入re模块

file_names = "document.txt, image.png, archive.zip" # 包含带扩展名的文件名

# 匹配字面量句点 '.'
# 错误的写法: r'.txt' 会匹配任意字符加txt,而不是字面量.txt
# 正确的写法: \.
pattern1 = r'\.txt' # 模式:匹配字面字符'.'后跟"txt"
matches1 = re.findall(pattern1, file_names) # 查找所有匹配
print(f"'\\.txt' 的匹配结果: {
     
     matches1}") # 打印结果。会匹配".txt"

# 匹配字面量星号 '*'
text_with_stars = "item*number, item**value" # 包含星号的字符串
pattern2 = r'item\*' # 模式:匹配"item"后跟字面字符'*'
matches2 = re.findall(pattern2, text_with_stars) # 查找所有匹配
print(f"'item\\*' 的匹配结果: {
     
     matches2}") # 打印结果。会匹配"item*"

# 匹配字面量问号 '?'
query_string = "key=value?id=123" # 包含问号的查询字符串
pattern3 = r'value\?' # 模式:匹配"value"后跟字面字符'?'
matches3 = re.findall(pattern3, query_string) # 查找所有匹配
print(f"'value\\?' 的匹配结果: {
     
     matches3}") # 打印结果。会匹配"value?"

代码解释:

  • import re: 导入re模块。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

宅男很神经

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值