书摘:Python 3 编程

作者:LJ Staff

阅读 Mark Summerfield 所著书籍 Programming in Python 3: A Complete Introduction to the Python Language 的第 12 章。

本章节摘自 Mark Summerfield 撰写,Addison-Wesley Professional Developer’s Library 于 2008 年 12 月出版的书籍 Programming in Python 3: A Complete Introduction to the Python Language ,ISBN 0137129297,版权 2009 Pearson Education, Inc. 更多信息请查看 Summerfield 的新 Digital Short Cut: Advanced Python 3 Programming Techniques


第 12 章:正则表达式

  • Python 的正则表达式语言
  • 正则表达式模块

正则表达式是一种用于表示字符串集合的紧凑符号。正则表达式如此强大的原因是,单个正则表达式可以表示无限数量的字符串——只要它们满足正则表达式的要求。正则表达式(我们之后大多称之为 “regexes”)是使用一种与 Python 完全不同的小型语言定义的——但 Python 包含了 re 模块,通过该模块我们可以无缝地创建和使用 regexes。1

Regexes 主要用于四个目的

  • 验证:检查一段文本是否满足某些条件,例如,包含货币符号后跟数字
  • 搜索:定位可以有多种形式的子字符串,例如,查找 “pet.png”、“pet.jpg”、“pet.jpeg” 或 “pet.svg” 中的任何一个,同时避免 “carpet.png” 和类似的
  • 搜索和替换:将正则表达式匹配到的所有位置替换为一个字符串,例如,查找 “bicycle” 或 “human powered vehicle” 并将两者都替换为 “bike”
  • 分割字符串:在正则表达式匹配到的每个位置分割字符串,例如,在遇到 “: ” 或 “=” 的所有位置进行分割

最简单的正则表达式是一个表达式(例如,一个字面字符),可选地后跟一个量词。更复杂的 regexes 由任意数量的量化表达式组成,并且可能包含断言,并可能受标志的影响。

本章的第一节介绍并解释了所有关键的正则表达式概念,并展示了纯粹的正则表达式语法——它极少提及 Python 本身。然后,第二节展示了如何在 Python 编程的上下文中使用正则表达式,并借鉴了前面章节中涵盖的所有材料。熟悉正则表达式且只想学习它们在 Python 中如何工作的读者可以跳到第二节。本章涵盖了 re 模块提供的完整 regex 语言,包括所有断言和标志。我们在文本中使用粗体指示正则表达式,使用下划线显示它们的匹配位置,并使用阴影显示捕获。

Python 的正则表达式语言

在本节中,我们将通过四个小节来了解正则表达式语言。第一小节介绍如何匹配单个字符或字符组,例如,匹配 a,或匹配 b,或匹配 ab 中的任何一个。第二小节介绍如何量化匹配,例如,匹配一次,或至少匹配一次,或尽可能多地匹配。第三小节介绍如何对子表达式进行分组以及如何捕获匹配的文本,最后一小节介绍如何使用该语言的断言和标志来影响正则表达式的工作方式。

字符和字符类

最简单的表达式只是字面字符,例如 a5,如果没有显式给出量词,则默认为“匹配一次”。例如,regex tune 由四个表达式组成,每个表达式都隐式量化为匹配一次,因此它匹配一个 t,后跟一个 u,后跟一个 n,后跟一个 e,因此匹配字符串 tuneattuned

虽然大多数字符可以用作字面量,但有些是“特殊字符”——这些是 regex 语言中的符号,因此必须在它们前面加上反斜杠 (\\) 来转义,才能将它们用作字面量。特殊字符有 \.^$?+*{}[]()|。Python 的大多数标准字符串转义符也可以在 regexes 中使用,例如,\n 表示换行符,\t 表示制表符,以及使用 \xHH\uHHHH\UHHHHHHHH 语法的字符的十六进制转义符。

在许多情况下,我们不是要匹配一个特定的字符,而是要匹配一组字符中的任何一个。这可以通过使用字符类来实现——一个或多个字符用方括号括起来。(这与 Python 类无关,只是 regex 中 “字符集” 的术语。)字符类是一个表达式,并且像任何其他表达式一样,如果未显式量化,它将精确匹配一个字符(可以是字符类中的任何字符)。例如,regex r[ea]d 同时匹配 redradar,但不匹配 read。类似地,要匹配单个数字,我们可以使用 regex [0123456789]。为了方便起见,我们可以使用连字符指定字符范围,因此 regex [0-9] 也匹配一个数字。可以通过在左方括号后跟一个插入符号来否定字符类的含义,因此 [^0-9] 匹配不是数字的任何字符。

请注意,在字符类内部,除了 \\ 之外,特殊字符会失去其特殊含义,尽管在 ^ 的情况下,如果它是字符类中的第一个字符,则会获得新的含义(否定),否则它只是一个字面插入符号。此外,- 表示字符范围,除非它是第一个字符,在这种情况下,它是一个字面连字符。

由于某些字符集非常常用,因此有几种简写形式——这些形式在表 12.1 中列出。除一种情况外,简写形式可以在字符集中使用,因此例如,regex [\dA-Fa-f] 匹配任何十六进制数字。例外情况是 .,它在字符类外部是简写形式,但在字符类内部匹配字面量 .。

表 12.1 字符类简写

符号

含义

. 

匹配除换行符以外的任何字符;或在 re.DOTALL 标志下匹配任何字符;或在字符类内部匹配字面量 .。

\d 

匹配 Unicode 数字;或在以下情况下匹配 [0-9]re.ASCII标志

\D 

匹配 Unicode 非数字;或在以下情况下匹配 [^0-9]re.ASCII标志

\s 

匹配 Unicode 空白字符;或在以下情况下匹配 [ \t\n\r\f\v] re.ASCII标志

\S 

匹配 Unicode 非空白字符;或在以下情况下匹配 [^ \t\n\r\f\v]re.ASCII标志

\w 

匹配 Unicode “单词” 字符;或在以下情况下匹配 [a-zA-Z0-9_]re.ASCII标志

\W 

匹配 Unicode 非“单词” 字符;或在以下情况下匹配 [^a-zA-Z0-9_]re.ASCII标志

量词

量词的形式为 {m,n},其中 mn 是量词应用于的表达式必须匹配的最小和最大次数。例如,e{1,1}e{1,1}e{2,2} 都匹配 feel,但都不匹配 felt

在每个表达式后都写量词很快就会变得繁琐,而且肯定难以阅读。幸运的是,regex 语言支持几种方便的简写形式。如果在量词中只给出一个数字,则将其视为最小值和最大值,因此 e{2}e{2,2} 相同。正如我们在上一节中指出的,如果没有显式给出量词,则假定为 1(即 {1,1}{1});因此,eee{1,1}e{1,1}e{1}e{1} 相同,因此 e{2} 和 ee 都匹配 feel,但不匹配 felt

具有不同的最小值和最大值通常很方便。例如,要匹配 travelled 和 traveled(都是合法的拼写),我们可以使用 travel{1,2}ed 或 travell{0,1}ed{0,1} 量化非常常用,以至于它有自己的简写形式 ?,因此编写 regex 的另一种方法(也是实践中最常用的方法)是 travell?ed

还提供了另外两个量化简写形式:+ 代表 {1,n}(“至少一个”),* 代表 {0,n}(“任意数量”);在这两种情况下,n 都是量词允许的最大可能数量,通常至少为 32767。所有量词都显示在表 12.2 中。

+ 量词非常有用。例如,要匹配整数,我们可以使用 \d+,因为它匹配一个或多个数字。此 regex 可以在字符串 4588.91 中的两个位置匹配,例如,4588.914588.91。有时,错别字是由于按键时间过长造成的。我们可以使用 regex bevel+ed 来匹配合法的 beveledbevelled,以及不正确的 bevellled。如果我们想标准化为单个 l 的拼写,并且只匹配出现两次或更多次 l 的情况,我们可以使用 bevell+ed 来查找它们。

* 量词不太有用,仅仅是因为它经常导致意外的结果。例如,假设我们想在 Python 文件中查找包含注释的行,我们可能会尝试搜索 #*。但是,此 regex 将匹配任何行,包括空行,因为其含义是“匹配任意数量的 #”——包括零个。对于 regex 的新手来说,一个经验法则是完全避免使用 *,如果您使用它(或者如果您使用 ?),请确保 regex 中至少有一个其他表达式具有非零量词——因此至少有一个量词不是 * 或 ?,因为这两者都可以将其表达式匹配零次。

通常可以将 * 的用法转换为 + 的用法,反之亦然。例如,我们可以使用 tassell*edtassel+ed 匹配至少有一个 l 的 “tasselled”,并使用 tasselll*edtassell+ed 匹配有两个或更多 l 的。

如果我们使用 regex \d+,它将匹配 136。但是,为什么它匹配所有数字,而不仅仅是第一个数字?默认情况下,所有量词都是贪婪的——它们尽可能多地匹配字符。我们可以通过在任何量词后跟一个 ? 符号来使其变为非贪婪(也称为最小)。(问号有两种不同的含义——单独使用时,它是 {0,1} 量词的简写形式,当它跟在量词后面时,它告诉量词变为非贪婪。)例如,\d+? 可以在字符串 136 中的三个不同位置匹配:136136136。这是另一个示例:\d?? 匹配零个或一个数字,但由于它是非贪婪的,因此倾向于不匹配任何数字——单独使用时,它会遇到与 * 相同的问题,即它将匹配任何内容,即任何文本。

表 12.2 正则表达式量词

语法

含义

e? or e{0,1} 

贪婪地匹配表达式 e 的零次或一次出现

e?? or e{0,1}? 

非贪婪地匹配表达式 e 的零次或一次出现

e+ or e{1,} 

贪婪地匹配表达式 e 的一次或多次出现

e+? or e{1,}? 

非贪婪地匹配表达式 e 的一次或多次出现

e* or e{0,} 

贪婪地匹配表达式 e 的零次或多次出现

e*? or e{0,}? 

非贪婪地匹配表达式 e 的零次或多次出现

e{m} 

精确匹配表达式 e 的 m 次出现

e{m,} 

贪婪地匹配表达式 e 的至少 m 次出现

e{m,}? 

非贪婪地匹配表达式 e 的至少 m 次出现

e{,n} 

贪婪地匹配表达式 e 的至多 n 次出现

e{,n}? 

非贪婪地匹配表达式 e 的至多 n 次出现

e{m,n} 

贪婪地匹配表达式 e 的至少 m 次且至多 n 次出现

e{m,n}? 

非贪婪地匹配表达式 e 的至少 m 次且至多 n 次出现

非贪婪量词对于快速而粗糙的 XML 和 HTML 解析非常有用。例如,要匹配所有图像标签,编写 <img.*>(匹配一个 “<”,然后一个 “i”,然后一个 “m”,然后一个 “g”,然后零个或多个除换行符以外的任何字符,然后一个 “>”)将不起作用,因为 .* 部分是贪婪的,并且将匹配包括标签的结束 > 在内的所有内容,并将一直持续到它到达整个文本中的最后一个 >。

提出了三种解决方案(除了使用适当的解析器)。一种是 <img[^>]*>(匹配 <img,然后是任意数量的非 > 字符,然后是标签的结束 > 字符),另一种是 <img.*?>(匹配 <img,然后是任意数量的字符,但非贪婪,因此它将在标签的结束 > 之前立即停止,然后是 >),第三种结合了两者,如 <img[^>]*?> 中所示。但是,它们都不正确,因为它们都可以匹配 <img>,这是无效的。由于我们知道图像标签必须具有 src 属性,因此更准确的 regex 是 <img\s+[^>]*?src=\w+[^>]*?>。这匹配字面字符 <img,然后是一个或多个空白字符,然后是非贪婪的零个或多个除 > 以外的任何内容(以跳过任何其他属性,例如 alt),然后是 src 属性(字面字符 src= ,然后是至少一个 “单词” 字符),然后是任何其他非 > 字符(包括无),以考虑任何其他属性,最后是结束 >。

分组和捕获

在实际应用中,我们经常需要可以匹配两个或多个替代项中任何一个的 regexes,并且我们经常需要捕获匹配项或匹配项的某些部分以进行进一步处理。此外,我们有时希望量词应用于多个表达式。所有这些都可以通过使用 () 分组来实现,在替代项的情况下,可以使用 | 交替。

当我们要匹配几个完全不同的替代项中的任何一个时,交替特别有用。例如,regex aircraft|airplane|jet 将匹配任何包含 “aircraft” 或 “airplane” 或 “jet” 的文本。可以使用 regex air(craft|plane)|jet 实现相同的功能。在这里,括号用于对表达式进行分组,因此我们有两个外部表达式,air(craft|plane)jet。第一个表达式具有内部表达式 craft|plane,并且由于它前面是 air,因此第一个外部表达式只能匹配 “aircraft” 或 “airplane”。

括号有两个不同的用途——对表达式进行分组和捕获与表达式匹配的文本。我们将使用术语来指代分组的表达式,无论它是否捕获,以及捕获捕获组来指代捕获的组。如果我们使用 regex (aircraft|airplane|jet),它不仅会匹配这三个表达式中的任何一个,而且还会捕获匹配到的表达式以供以后引用。将其与 regex (air(craft|plane)|jet) 进行比较,如果第一个表达式匹配,则它有两个捕获(“aircraft” 或 “airplane” 作为第一个捕获,“craft” 或 “plane” 作为第二个捕获),如果第二个表达式匹配,则它有一个捕获(“jet”)。我们可以通过在左括号后跟 ?: 来关闭捕获效果,因此例如,(air(?:craft|plane)|jet) 如果匹配(“aircraft” 或 “airplane” 或 “jet”),则只有一个捕获。

分组表达式是一个表达式,因此可以量化。像任何其他表达式一样,除非显式给出,否则数量假定为 1。例如,如果我们读取了一个文本文件,其中包含 key=value 形式的行,其中每个 key 都是字母数字,则 regex (\w+)=(.+) 将匹配每个具有非空键和非空值的行。(回想一下,. 匹配除换行符以外的任何内容。)对于每个匹配的行,都会进行两次捕获,第一个是键,第二个是值。

例如,key=value 正则表达式将匹配整行 topic= physical geography,其中两个捕获项显示为阴影。请注意,第二个捕获项包含一些空白字符,并且不接受 = 之前的空白字符。我们可以改进 regex,使其在接受空白字符方面更加灵活,并使用稍长的版本来去除不需要的空白字符

[ \t]*(\w+)[ \t]*=[ \t]*(.+)

这与之前的行以及 = 符号周围有空白字符的行相匹配,但第一个捕获项没有前导或尾随空白字符,第二个捕获项没有前导空白字符。例如:topic = physical geography

我们一直小心地将空白字符匹配部分放在捕获括号之外,并允许行根本没有空白字符。我们没有使用 \s 来匹配空白字符,因为这会匹配换行符 (\n),这可能会导致跨行的不正确匹配(例如,如果使用 re.MULTILINE 标志)。对于值,我们没有使用 \S 来匹配非空白字符,因为我们希望允许包含空白字符的值(例如,英语句子)。为了避免第二个捕获项具有尾随空白字符,我们需要更复杂的 regex;我们将在下一小节中看到这一点。

可以使用反向引用来引用捕获,也就是说,通过回溯引用较早的捕获组。2 regexes 本身内部的反向引用的一种语法是 \i,其中 i 是捕获编号。捕获从 1 开始编号,并在遇到每个新的(捕获)左括号时从左到右递增 1。例如,为了简单地匹配重复的单词,我们可以使用 regex (\w+)\s+\1,它匹配一个 “单词”,然后至少一个空白字符,然后是与捕获的单词相同的单词。(捕获编号 0 是自动创建的,无需括号;它保存整个匹配项,即我们用下划线显示的内容。)稍后我们将看到一种更复杂的方式来匹配重复的单词。

在长或复杂的 regexes 中,使用名称而不是数字进行捕获通常更方便。这也使维护更容易,因为添加或删除捕获括号可能会更改数字,但不会影响名称。要命名捕获,我们在左括号后跟 ?P<name>。例如,(?P<key>\w+)=(?P<value>.+) 有两个名为 "key""value" 的捕获。regex 中命名捕获的反向引用语法是 (?P=name)。例如,(?P<word>\w+)\s+(?P=word) 使用名为 "word" 的捕获来匹配重复的单词。

断言和标志

到目前为止,我们看到的许多 regexes 都存在一个问题,即它们可以匹配比我们预期更多或不同的文本。例如,regex aircraft|airplane|jet 将匹配 “waterjet” 和 “jetski” 以及 “jet”。这类问题可以通过使用断言来解决。断言不匹配任何文本,而是说明断言发生位置的文本的某些信息。

一个断言是 \b(单词边界),它断言它前面的字符必须是 “单词” (\w),而它后面的字符必须是非 “单词” (\W),反之亦然。例如,虽然 regex jet 可以在文本 jet and jetski are noisy 中匹配两次,即 jet and jetski are noisy,但 regex \bjet\b 将只匹配一次,jet and jetski are noisy。在原始 regex 的上下文中,我们可以将其写为 \baircraft\b|\bairplane\b|\bjet\b,或者更清楚地写为 \b(?:aircraft|airplane|jet)\b,即单词边界、非捕获表达式、单词边界。

支持许多其他断言,如表 12.3 所示。我们可以使用断言来提高 key=value regex 的清晰度,例如,通过将其更改为 ^(\w+)=([^\n]+) 并设置 re.MULTILINE 标志以确保每个 key=value 都取自单行,而没有跨行的可能性。(标志显示在第 460 页的表 12.5 中,使用它们的语法在本小节末尾描述,并在下一节中显示。)如果我们还想去除前导和尾随空白字符并使用命名捕获,则完整的 regex 变为

^[ \t]*(?P<key>\w+)[ \t]*=[ \t]*(?P<value>[^\n]+)(?<![ \t])

即使此 regex 是为相当简单的任务设计的,它看起来也相当复杂。使其更易于维护的一种方法是在其中包含注释。这可以通过使用语法 (?#the comment) 添加内联注释来完成,但在实践中,这样的注释很容易使 regex 更难以阅读。一个更好的解决方案是使用 re.VERBOSE 标志——这允许我们在 regexes 中自由使用空白字符和正常的 Python 注释,但有一个约束,如果我们需要匹配空白字符,我们必须使用 \s 或字符类,例如 []。以下是带有注释的 key=value regex

^[ \t]*           # start of line and optional leading whitespace
(?P<key>\w+)      # the key text
[ \t]*=[ \t]*     # the equals with optional surrounding whitespace
(?P<value>[^\n]+) # the value text
(?<![ \t])        # negative lookbehind to avoid trailing whitespace

在 Python 程序的上下文中,我们通常会将这样的 regex 编写在原始三引号字符串中——原始字符串是为了我们不必重复使用反斜杠,而三引号字符串是为了我们可以将其分布在多行上。

除了我们到目前为止讨论的断言之外,还有一些额外的断言会查看断言前面(或后面)的文本,以查看它是否匹配(或不匹配)我们指定的表达式。可用于后向断言的表达式必须是固定长度的(因此不能使用量词 ?+ 和 *,并且数字量词必须是固定大小的,例如,{3})。

表 12.3 正则表达式断言

符号

含义

^ 

在开头匹配;也在每个换行符后在 re.MULTILINE 标志下匹配

$ 

在结尾匹配;也在每个换行符前在 re.MULTILINE 标志下匹配

\A 

在字符串开头匹配

\b 

在 “单词” 边界匹配;受以下因素影响re.ASCII标志——在字符类内部,这是退格字符的转义符

\B 

在非 “单词” 边界匹配;受以下因素影响re.ASCII标志

\Z 

在字符串结尾匹配

(?=e) 

如果表达式 e 在此断言处匹配,但不向前推进,则匹配——称为前瞻正向前瞻

(?!e) 

如果表达式 e 在此断言处不匹配,且不向前推进,则匹配——称为负向前瞻

 (?<=e)

如果表达式 e 紧接在此断言之前匹配,则匹配——称为后顾正向后顾

(?<!e)

如果表达式 e 紧接在此断言之前不匹配,则匹配——称为负向后顾

对于 key=value regex,负向后顾断言意味着在其发生的位置,前面的字符不能是空格或制表符。这具有确保捕获到 “value” 捕获组中的最后一个字符不是空格或制表符的效果(但不会阻止空格或制表符出现在捕获的文本内部)。

让我们考虑另一个例子。假设我们正在读取一个多行文本,其中包含名称 “Helen Patricia Sharman”、“Jim Sharman”、“Sharman Joshi”、“Helen Kelly” 等等,并且我们想要匹配 “Helen Patricia”,但仅在引用 “Helen Patricia Sharman” 时。最简单的方法是使用 regex \b(Helen\s+Patricia)\s+Sharman\b。但是,我们也可以使用前瞻断言来实现相同的目的,例如,\b(Helen\s+Patricia)(?=\s+Sharman\b)。这将仅在 “Helen Patricia” 前面是单词边界,后面是空白字符和以单词边界结尾的 “Sharman” 时才匹配 “Helen Patricia”。

为了捕获使用的名字的特定变体(“Helen”、“Helen P.” 或 “Helen Patricia”),我们可以使 regex 稍微复杂一些,例如,\b(Helen(?:\s+(?:P\.|Patricia))?)\s+(?=Sharman\b)。这匹配一个单词边界,后跟名字形式之一——但仅当其后跟一些空白字符,然后是 “Sharman” 和单词边界时。

请注意,只有两种语法执行捕获,(e)(?P<name>e)。其他带括号的形式均不捕获。这对于前瞻和后顾断言来说是完全合理的,因为它们只对它们后面或前面的内容进行陈述——它们不是匹配的一部分,而是影响是否进行匹配。对于我们现在将要考虑的最后两种带括号的形式,这也是有道理的。

我们之前已经看到了如何通过数字(例如,\1)或名称(例如,(?P=name))在 regex 内部反向引用捕获。也可以根据是否发生较早的匹配来有条件地进行匹配。语法为 (?(id)yes_exp)(?(id)yes_exp|no_exp)。id 是我们引用的较早捕获的名称或编号。如果捕获成功,则 yes_exp 将在此处匹配。如果捕获失败,则将匹配 no_exp(如果已给出)。

让我们考虑一个例子。假设我们要提取 HTML img 标签中 src 属性引用的文件名。我们将首先尝试匹配 src 属性,但与我们之前的尝试不同,我们将考虑属性值可以采用的三种形式:单引号、双引号和无引号。这是一个初步尝试:src=(["'])([^"'>]+)\1([^"'>]+) 部分捕获至少一个非引号或 > 字符的贪婪匹配。此 regex 对于带引号的文件名效果很好,并且由于 \1,仅当开始和结束引号相同时才匹配。但是,它不允许使用无引号的文件名。为了解决这个问题,我们必须使开始引号成为可选的,因此仅在存在引号时才匹配它。这是修改后的 regex:src=(["'])?([^"'>]+)(?(1)\1)。我们没有提供 no_exp,因为如果没有给出引号,则没有什么可匹配的。现在我们准备将 regex 放入上下文中——这是使用命名组和注释的完整 img 标签 regex

<img\s+              # start of the tag
[^>]*?               # any attributes that precede the src
src=                 # start of the src attribute
(?P<quote>["'])?     # optional opening quote
(?P<image>[^"'>]+)   # image filename
(?(quote)(?P=quote)) # closing quote (matches opening quote if given)
[^>]*?               # any attributes that follow the src
>                    # end of the tag

文件名捕获名为 “image”(恰好是捕获编号 2)。

当然,还有一种更简单但更微妙的替代方案:src=(["']?)([^"'>]+)\1。在这里,如果存在起始引号字符,则将其捕获到捕获组 1 中,并在非引号字符之后匹配。如果没有起始引号字符,则组 1 仍将匹配——一个空字符串,因为它完全是可选的(其量词为零或一),在这种情况下,反向引用也将匹配一个空字符串。

Python 的正则表达式引擎提供的最后一个 regex 语法是设置标志的方法。通常,标志是通过在调用 re.compile() 函数时将它们作为附加参数传递来设置的,但有时将它们设置为 regex 本身的一部分更方便。语法很简单 (?flags),其中 flags 是 a 的一个或多个(与传递相同re.ASCII), i (re.IGNORECASE)m (re.MULTILINE)s (re.DOTALL)x (re.VERBOSE)3 如果以这种方式设置标志,则应将它们放在 regex 的开头;它们不匹配任何内容,因此它们对 regex 的影响仅是设置标志。

正则表达式模块

re 模块提供了两种使用 regexes 的方法。一种是使用表 12.4 中列出的函数,其中每个函数都将 regex 作为其第一个参数。每个函数都将 regex 转换为内部格式——这个过程称为编译——然后执行其工作。这对于一次性使用非常方便,但是如果我们需要重复使用相同的 regex,我们可以通过使用 re.compile() 函数编译一次来避免每次使用时编译它的成本。然后,我们可以根据需要多次调用已编译的 regex 对象上的方法。已编译的 regex 方法在表 12.6 中列出。

match = re.search(r"#[\dA-Fa-f]{6}\b", text)

此代码片段显示了 re 模块函数的使用。regex 匹配 HTML 样式的颜色(例如 #C0C0AB)。如果找到匹配项,则 re。search()函数返回一个匹配对象;否则,它返回 None。匹配对象提供的方法在表 12.7 中列出

如果我们要重复使用此 regex,我们可以将其编译一次,然后在需要时随时使用编译后的 regex

color_re = re.compile(r"#[\dA-Fa-f]{6}\b")
match = color_re.search(text)

正如我们之前指出的,我们使用原始字符串来避免转义反斜杠。编写此 regex 的另一种方法是使用字符类 [\dA-F] 并将 re.IGNORECASE 标志作为最后一个参数传递给 re.compile() 调用,或者使用以忽略大小写标志开头的 regex (?i)#[\dA-F]{6}\b

如果需要多个标志,可以使用 OR 运算符 (|) 将它们组合起来,例如,re.MULTILINE|re.DOTALL,或者如果嵌入在 regex 本身中,则可以使用 (?ms)

我们将通过回顾一些示例来结束本节,首先回顾前面章节中展示的一些正则表达式,以便说明 re 模块提供的最常用功能。让我们从一个用于查找重复单词的正则表达式开始

double_word_re = re.compile(r"\b(?P<word>\w+)\s+(?P=word)(?!\w)",
                            re.IGNORECASE)
for match in double_word_re.finditer(text):
       print("{0} is duplicated".format(match.group("word")))

这个正则表达式比我们之前创建的版本稍微复杂一些。它从单词边界开始(以确保每个匹配项都从单词的开头开始),然后贪婪地匹配一个或多个“单词”字符,然后是一个或多个空白字符,然后再次是相同的单词——但前提是该单词的第二次出现之后没有单词字符。

如果输入文本是“win in vain”,没有第一个断言,则会有一个匹配项和两个捕获组:win in vain。单词边界断言的使用确保了匹配的第一个单词是一个完整的单词,因此我们最终没有匹配项或捕获组,因为没有重复的单词。同样,如果输入文本是“one and and two let’s say”,没有最后一个断言,则会有两个匹配项和两个捕获组:one and and two let's say。前瞻断言的使用意味着匹配的第二个单词是一个完整的单词,因此我们最终得到一个匹配项和一个捕获组:one and and two let's say

for 循环遍历 finditer() 方法返回的每个匹配对象,我们使用匹配对象的 group() 方法来检索捕获组的文本。我们可以同样容易地(但可维护性较差)使用 group(1)——在这种情况下,我们根本不需要命名捕获组,只需使用正则表达式 (\w+)\s+\1(?!\w)。另一个需要注意的点是,我们可以使用单词边界 \b 在末尾,而不是 (?!\w)

我们之前介绍的另一个示例是用于查找 HTML 图像标签中文件名的正则表达式。以下是我们如何编译正则表达式的方法,添加标志使其不区分大小写,并允许我们包含注释

image_re = re.compile(r"""
               <img\s+               # start of tag
               [^>]*?                # non-src attributes
               src=                  # start of src attribute
               (?P<quote>["'])?      # optional opening quote
               (?P<image>[^"'>]+)    # image filename
               (?(quote)(?P=quote))  # closing quote
               [^>]*?                # non-src attributes
               >                     # end of the tag
               """, re.IGNORECASE|re.VERBOSE)
image_files = []
for match in image_re.finditer(text):
    image_files.append(match.group("image"))

我们再次使用 finditer() 方法来检索每个匹配项,并使用匹配对象的 group() 函数来检索捕获的文本。由于不区分大小写仅适用于 imgsrc,我们可以删除 re.IGNORECASE 标志,而使用 [Ii][Mm][Gg][Ss][Rr][Cc] 代替。虽然这会使正则表达式不太清晰,但可能会使其更快,因为它不需要将要匹配的文本设置为大写(或小写)——但这可能只有在正则表达式用于处理大量文本时才会产生差异。

一个常见的任务是获取 HTML 文本并仅输出它包含的纯文本。当然,我们可以使用 Python 的解析器之一来完成此操作,但可以使用正则表达式创建一个简单的工具。有三个任务需要完成:删除所有标签,将实体替换为它们代表的字符,并插入空行来分隔段落。这是一个执行此操作的函数(取自 html2text.py 程序)

def html2text(html_text):
    def char_from_entity(match):
        code = html.entities.name2codepoint.get(match.group(1), 0xFFFD)
        return chr(code)
    text = re.sub(r"<!--(?:.|\n)*?-->", "", html_text)          #1
    text = re.sub(r"<[Pp][^>]*?(?!</)>", "\n\n", text)          #2
    text = re.sub(r"<[^>]*?>", "", text)                        #3
    text = re.sub(r"&#(\d+);", lambda m: chr(int(m.group(1))), text)
    text = re.sub(r"&([A-Za-z]+);", char_from_entity, text)     #5
    text = re.sub(r"\n(?:[ \xA0\t]+\n)+", "\n", text)           #6
    return re.sub(r"\n\n+", "\n\n", text.strip())               #7

第一个正则表达式 <!--(?:.|\n)*?--> 匹配 HTML 注释,包括那些内部嵌套了其他 HTML 标签的注释。re.sub()函数用替换项替换它找到的尽可能多的匹配项——如果替换项是空字符串,则删除匹配项,就像这里的情况一样。(我们可以通过在末尾给出一个额外的整数参数来指定最大匹配数。)

我们小心地使用非贪婪(最小)匹配,以确保我们为每个匹配项删除一个注释;如果我们不这样做,我们将从第一个注释的开头删除到最后一个注释的结尾。

这个re.sub()函数不接受任何标志作为参数,因此 . 表示“除换行符外的任何字符”,因此我们必须查找 . 或 \n。我们必须使用交替而不是字符类来查找这些,因为在字符类内部 . 具有其字面意思,即句点。另一种方法是在正则表达式的开头嵌入标志,例如,(?s)<!--.*?-->,或者我们可以使用 re.DOTALL 标志编译一个正则表达式对象,在这种情况下,正则表达式将只是 <!--.*?-->

第二个正则表达式 <[Pp][^>]*?(?!</)> 匹配打开段落标签(例如 <P> <palign=center>)。它匹配打开的 <p(或 <P),然后是任何属性(使用非贪婪匹配),最后是结束的 >,前提是它前面没有 /(使用负向后视断言),因为这将表示一个关闭段落标签。第二次调用re.sub()函数使用此正则表达式将打开段落标签替换为两个换行符(在纯文本文件中分隔段落的标准方法)。

第三个正则表达式 <[^>]*?> 匹配任何标签,并在第三次 re.sub() 调用中用于删除所有剩余的标签。

HTML 实体是一种使用 ASCII 字符指定非 ASCII 字符的方法。它们有两种形式:&name;,其中 name 是字符的名称——例如,&copy; 表示 ©,以及 &#digits;,其中 digits 是标识 Unicode 代码点的十进制数字——例如,&#165; 表示 ¥。第四次调用re.sub()使用正则表达式 &#(\d+);,它匹配数字形式并将数字捕获到捕获组 1 中。我们没有使用文字替换文本,而是传递了一个 lambda 函数。当一个函数传递给re.sub()时,它为每次匹配调用该函数一次,并将匹配对象作为函数的唯一参数传递。在 lambda 函数内部,我们检索数字(作为字符串),使用内置的 int() 函数将其转换为整数,然后使用内置的chr()函数来获取给定代码点的 Unicode 字符。函数的返回值(或者在 lambda 表达式的情况下,表达式的结果)用作替换文本。

第五个re.sub()调用使用正则表达式 &([A-Za-z]+); 来捕获命名实体。标准库的 html.entities 模块包含实体字典,包括 name2codepointwhose 键是实体名称,值是整数代码点。re.sub()函数每次匹配时都会调用本地 char_from_entity() 函数。char_from_entity() 函数使用dict.get(),默认参数为 0xFFFD(标准 Unicode 替换字符的代码点——通常描绘为 ? )。这确保始终检索代码点,并将其与chr()函数一起使用,以返回合适的字符来替换命名实体——如果实体名称无效,则使用 Unicode 替换字符。

第六个re.sub()调用的正则表达式 \n(?:[ \xA0\t]+\n)+ 用于删除仅包含空白字符的行。我们使用的字符类包含空格、不间断空格(&nbsp; 实体在前一个正则表达式中被替换为不间断空格)和制表符。正则表达式匹配一个换行符(位于空白行之前的一行末尾),然后匹配至少一个(以及尽可能多的)仅包含空白字符的行。由于匹配项包含换行符,因此从空白行之前的行中,我们必须将匹配项替换为单个换行符;否则,我们不仅会删除仅包含空白字符的行,还会删除它们前面行的换行符。

第七个也是最后一个re.sub()调用的结果返回给调用者。此正则表达式 \n\n+ 用于将两个或多个换行符的序列替换为恰好两个换行符,即确保每个段落之间只有一个空行。

在 HTML 示例中,没有直接从匹配项中获取任何替换项(尽管使用了 HTML 实体名称和数字),但在某些情况下,替换项可能需要包含全部或部分匹配文本。例如,如果我们有一个名称列表,每个名称的格式为名字 中间名1 ... 中间名N 姓氏,其中可以有任意数量的中间名(包括没有),并且我们想要生成一个新版本的列表,其中每个项目的格式为姓氏,名字 中间名1...中间名N,我们可以使用正则表达式轻松地做到这一点

new_names = []
for name in names:
    name = re.sub(r"(\w+(?:\s+\w+)*)\s+(\w+)", r"\2, \1", name)
    new_names.append(name)

正则表达式的第一部分 (\w+(?:\s+\w+)*) 使用第一个 \w+ 表达式匹配名字,使用 (?:\s+\w+)* 表达式匹配零个或多个中间名。中间名表达式匹配零个或多个空格,后跟一个单词。正则表达式的第二部分 \s+(\w+) 匹配名字(和中间名)之后的空格和姓氏。

如果正则表达式看起来有点像行噪声,我们可以使用命名捕获组来提高可读性并使其更易于维护

name = re.sub(r"(?P<forenames>\w+(?:\s+\w+)*)"
              r"\s+(?P<surname>\w+)",
              r"\g<surname>, \g<forenames>", name)

捕获的文本可以在 sub()subn() 函数或方法中使用语法 \i\g<id> 引用,其中 i 是捕获组的编号,id 是捕获组的名称或编号——因此 \1\g<1> 相同,在本例中,与 \g<forenames> 相同。此语法也可以在传递给匹配对象的 expand() 方法的字符串中使用。

为什么正则表达式的第一部分没有抓取整个名称?毕竟,它使用的是贪婪匹配。事实上,它会抓取整个名称,但随后匹配会失败,因为尽管中间名部分可以匹配零次或多次,但姓氏部分必须恰好匹配一次,但贪婪的中间名部分已经抓取了所有内容。失败后,正则表达式引擎将回溯,放弃最后一个“中间名”,从而允许姓氏匹配。

表 12.4 正则表达式模块的函数

语法

描述

re.compile(r, f) 

返回编译后的正则表达式 r,如果指定了标志 f,则设置其标志

re.escape(s)

返回字符串 s,其中所有非字母数字字符都进行了反斜杠转义——因此,返回的字符串没有特殊的正则表达式字符

re.findall(s r, s, f)

返回字符串中正则表达式 r 的所有非重叠匹配项(受给定标志 f 的影响)。如果正则表达式具有捕获组,则每个匹配项都作为捕获组的元组返回。

re.finditer(r, s, f)

返回字符串 s 中正则表达式 r 的每个非重叠匹配项的匹配对象(受给定标志 f 的影响)

re.match(r, s, f) 

如果正则表达式 r 在字符串 s 的开头匹配,则返回匹配对象(受给定标志 f 的影响);否则,返回 None

re.search(r, s, f) 

如果正则表达式 r 在字符串 s 中的任何位置匹配,则返回匹配对象(受给定标志 f 的影响);否则,返回 None

re.split(r, s, m)

返回字符串列表,该列表是通过在正则表达式 r 的每次出现处拆分字符串 s 而产生的,最多执行 m 次拆分(如果未给出 m,则尽可能多地拆分)。如果正则表达式具有捕获组,则这些捕获组包含在它们拆分的部分之间的列表中。

re.sub(r, x, s, m) 

返回字符串 s 的副本,其中正则表达式 r 的每个(或最多 m 个,如果给定)匹配项都替换为 x——x 可以是字符串或函数;请参阅文本

re.subn(r, x, s m) 

re.sub()相同,不同之处在于它返回一个 2 元组,其中包含结果字符串和进行的替换次数

表 12.5 正则表达式模块的标志

标志

含义

re.Are.ASCII

使 \b、\B、\s、\S、\w 和 \W 假定字符串为 ASCII;默认情况下,这些字符类简写取决于 Unicode 规范

re.Ire.IGNORECASE

使正则表达式不区分大小写地匹配

re.Mre.MULTILINE

使 ^ 在开头和每个换行符后匹配,$ 在每个换行符前和末尾匹配

re.Sre.DOTALL

使 . 匹配每个字符,包括换行符

re.Xre.VERBOSE

允许包含空格和注释

表 12.6 正则表达式对象方法

语法

描述

rx.findall(s start, end)

返回正则表达式在字符串 s 中(或在 s 的 start:end 切片中)的所有非重叠匹配项。如果正则表达式具有捕获组,则每个匹配项都作为捕获组的元组返回。

rx.finditer(s start, end)

返回字符串 s 中(或在 s 的 start:end 切片中)的每个非重叠匹配项的匹配对象

rx.flags 

编译正则表达式时设置的标志

rx.groupindex

一个字典,其键是捕获组名称,值是组号;如果未使用名称,则为空

rx.match(s, start, end) 

如果正则表达式在字符串 s 的开头(或在 s 的 start:end 切片的开头)匹配,则返回匹配对象;否则,返回 None

rx.pattern 

从中编译正则表达式的字符串

rx.search(s, start, end)

如果正则表达式在字符串 s 中的任何位置(或在 s 的 start:end 切片中)匹配,则返回匹配对象;否则,返回 None

rx.split(s, m)

返回字符串列表,该列表是通过在正则表达式的每次出现处拆分字符串 s 而产生的,最多执行 m 次拆分(如果未给出 m,则尽可能多地拆分)。如果正则表达式具有捕获组,则这些捕获组包含在它们拆分的部分之间的列表中。

rx.sub(x, s, m)

返回字符串 s 的副本,其中每个(或最多 m 个,如果给定)匹配项都替换为 x——x 可以是字符串或函数;请参阅文本

rx.subn(x, s m)

re.sub()相同,不同之处在于它返回一个 2 元组,其中包含结果字符串和进行的替换次数

虽然贪婪匹配会尽可能多地匹配,但如果匹配更多会导致匹配失败,它们就会停止。

例如,如果名称是“James W. Loewen”,则正则表达式将首先匹配整个名称,即 James W. Loewen。这满足了正则表达式的第一部分,但没有留下任何内容供姓氏部分匹配,并且由于姓氏是强制性的(它具有 1 的隐式量词),因此正则表达式失败。由于中间名部分由 * 量化,它可以匹配零次或多次(目前它匹配两次,“ W.” 和 “ Loewen”),因此正则表达式引擎可以使其放弃其部分匹配,而不会导致其失败。因此,正则表达式回溯,放弃最后一个 \s+\w+(即 “ Loewen”),因此匹配变为 James W. Loewen,匹配满足整个正则表达式,并且两个匹配组包含正确的文本。

当我们使用交替 (|) 与两个或多个交替项捕获时,我们不知道哪个交替项匹配,因此我们不知道要从哪个捕获组中检索捕获的文本。我们当然可以遍历所有组以找到非空的组,但在这种情况下,匹配对象的 lastindex 属性通常可以为我们提供我们想要的组的编号。我们将看最后一个示例来说明这一点,并给我们更多正则表达式练习的机会。

假设我们想知道 HTML、XML 或 Python 文件使用的是什么编码。我们可以以二进制模式打开文件,并读取,例如,前 1000 个字节到一个字节对象中。然后我们可以关闭文件,在 bytes 中查找编码,并使用我们找到的编码或使用回退编码(例如 UTF-8)以文本模式重新打开文件。正则表达式引擎希望正则表达式以字符串形式提供,但正则表达式应用到的文本可以是 strbytesbytearray 对象,并且当使用 bytes 或 bytearray 对象时,所有函数和方法都返回字节而不是字符串,并且re.ASCII标志隐式地打开。

对于 HTML 文件,编码通常在 <meta> 标签中指定(如果指定了的话),例如,<meta http-equiv='Content-Type' content='text/html; charset=ISO-8859-1'/>。XML 文件默认使用 UTF-8 编码,但这可以被覆盖,例如,<?xml version="1.0"encoding="Shift_JIS"?>。Python 3 文件也默认使用 UTF-8 编码,但这也可以通过在 shebang 行之后立即包含一行代码来覆盖,例如 #encoding: latin1 或 #-*-coding: latin1 -*-

以下是我们如何查找编码的方法,假设变量 binary 是一个字节对象,其中包含 HTML、XML 或 Python 文件的前 1000 个字节

match = re.search(r"""(?<![-\w])                  #1
                      (?:(?:en)?coding|charset)   #2
                      (?:=(["'])?([-\w]+)(?(1)\1) #3
                      |:\s*([-\w]+))""".encode("utf8"),
                  binary, re.IGNORECASE|re.VERBOSE)
encoding = match.group(match.lastindex) if match else b"utf8"

要搜索字节对象,我们必须指定一个也是字节对象的模式。在这种情况下,我们希望方便地使用原始字符串,因此我们使用一个原始字符串并将其转换为 bytes 对象作为 re 的。search()函数的第一个参数。

表 12.7 匹配对象属性和方法

语法

描述

m.end(g) 

如果给定组 g(或组 0,整个匹配项),则返回匹配项在文本中的结束位置;如果组未参与匹配,则返回 -1

m.endpos 

搜索的结束位置(文本的末尾或给定的末尾match()search())

m.expand(s) 

返回字符串 s,其中捕获标记(\1、\2、\g<name> 等)替换为相应的捕获组

m.group(g, ...)

返回编号或命名捕获组 g;如果给出了多个,则返回相应捕获组的元组(整个匹配项是组 0)

m.groupdict(default)

返回所有命名捕获组的字典,其中名称作为键,捕获组作为值;如果给出了默认值,则这是用于未参与匹配的捕获组的值

m.groups(default)

返回从 1 开始的所有捕获组的元组;如果给出了默认值,则这是用于未参与匹配的捕获组的值

m.lastgroup 

匹配的最高编号捕获组的名称,如果没有,或者如果未使用名称,则为 None

m.lastindex 

匹配的最高捕获组的编号,如果没有,则为 None

m.pos 

要查找的起始位置(文本的开头或给定的起始位置match()search())

m.re 

生成此匹配对象的正则表达式对象

m.span(g) 

如果给定组 g(或组 0,整个匹配项),则返回匹配项在文本中的开始和结束位置;如果组未参与匹配,则返回 (-1, -1)

m.start(g) 

如果给定组 g(或组 0,整个匹配项),则返回匹配项在文本中的起始位置;如果组未参与匹配,则返回 -1

m.string 

传递给match()search()

的字符串。正则表达式本身的第一部分是一个后视断言,它表示匹配项前面不能有连字符或单词字符。第二部分匹配“encoding”、“coding”或“charset”,可以写成 (?:encoding|coding|charset)。我们将第三部分跨越两行,以强调它有两个交替部分的事实,=(["'])?([-\w]+)(?(1)\1):\s*([-\w]+),其中只有一个可以匹配。第一个匹配一个等号,后跟一个或多个单词或连字符字符(可选地使用条件匹配括在匹配的引号中),第二个匹配一个冒号,然后是可选的空格,后跟一个或多个单词或连字符字符。(回想一下,字符类内部的连字符如果是第一个字符,则被视为文字连字符;否则,它表示字符范围,例如,[0-9]。)

我们使用了 re.IGNORECASE 标志,以避免必须编写 (?:(?:[Ee][Nn])? [Cc][Oo][Dd][Ii][Nn][Gg]|[Cc][Hh][Aa][Rr][Ss][Ee][Tt]),并且我们使用了 re.VERBOSE 标志,以便我们可以整齐地排列正则表达式并包含注释(在本例中只是数字,以便于在本文中引用这些部分)。

第三部分中有三个捕获匹配组:(["'])? 捕获可选的开始引号,([-\w]+) 捕获等号后面的编码,以及第二个 ([-\w]+)(在下一行)捕获冒号后面的编码。我们只对编码感兴趣,因此我们想要检索第二个或第三个捕获组,它们是替代项,因此只能匹配其中一个。lastindex 属性保存最后一个匹配捕获组的索引(在本例中发生匹配时为 2 或 3),因此我们检索匹配的任何一个,或者如果没有进行匹配,则使用默认编码。

我们现在已经看到了所有最常用的 re 模块功能在实际应用中的情况,因此我们将通过提及最后一个函数来结束本节。re.split() 函数(或正则表达式对象的 split() 方法)可以根据正则表达式拆分字符串。一个常见的需求是在空格上拆分文本以获得单词列表。这可以使用 re.split(r"\s+", text) 完成,它返回单词列表(或更准确地说,字符串列表,每个字符串都与 \S+ 匹配)。正则表达式非常强大且有用,一旦学会了它们,很容易将所有文本问题都视为需要正则表达式解决方案。但有时使用字符串方法既足够又更合适。例如,我们可以通过使用 text.split() 来同样轻松地在空格上拆分,因为 str.split() 方法的默认行为(或者使用 None 的第一个参数)是在 \s+ 上拆分。

总结

正则表达式提供了一种强大的方法,用于在文本中搜索与特定模式匹配的字符串,以及用其他字符串替换此类字符串,这些字符串本身可以取决于匹配的内容。

在本章中,我们看到大多数字符都是按字面意思匹配的,并且隐式地由 {1} 量化。我们还学习了如何指定字符类——要匹配的字符集——以及如何否定此类集合,以及如何在其中包含字符范围,而无需单独编写每个字符。

我们学习了如何量化表达式以匹配特定次数或从给定的最小次数到给定的最大次数进行匹配,以及如何使用贪婪和非贪婪匹配。我们还学习了如何将一个或多个表达式组合在一起,以便可以将它们作为一个单元进行量化(和可选捕获)。

本章还展示了如何通过使用各种断言(例如正向和负向前瞻和后顾)以及各种标志(例如,控制句点的解释以及是否使用不区分大小写的匹配)来影响匹配的内容。

最后一部分展示了如何在 Python 程序的上下文中使用正则表达式。在本节中,我们学习了如何使用 re 模块提供的函数,以及从编译后的正则表达式和匹配对象中可用的方法。我们还学习了如何使用文字字符串、包含反向引用的文字字符串以及函数调用或 lambda 表达式的结果来替换匹配项,以及如何通过使用命名捕获组和注释使正则表达式更易于维护。

练习

  1. 在许多情况下(例如,在某些 Web 表单中),用户必须输入电话号码,其中一些表格因仅接受特定格式而激怒用户。编写一个程序,读取美国电话号码,其中三位区号和七位本地代码接受为十位数字,或者使用连字符或空格分隔成块,并且区号可选地用括号括起来。例如,以下所有号码都是有效的:555-555-5555、(555) 5555555、(555) 555 5555 和 5555555555。从 sys.stdin 读取电话号码,并为每个号码以“(555) 555 5555”的形式回显号码,或报告任何无效号码的错误。
  2. 匹配这些电话号码的正则表达式大约有八行长(在详细模式下),并且非常简单。phone.py 中提供了一个解决方案,大约有二十五行长。

  3. 编写一个小程序,读取命令行上指定的 XML 或 HTML 文件,并对于每个具有属性的标签,输出标签的名称,并在其下方显示其属性。例如,以下是从程序输出中摘录的一部分,当给定 Python 文档的 index.html 文件之一时
  4. html
        xmlns = http://www.w3.org/1999/xhtml
    meta
         http-equiv = Content-Type
         content = text/html; charset=utf-8
    li
         class = right
         style = margin-right: 10px

    一种方法是使用两个正则表达式,一个用于捕获带有其属性的标签,另一个用于提取每个属性的名称和值。属性值可以使用单引号或双引号引起来(在这种情况下,它们可能包含空格和未用于括起它们的引号),或者它们可能是不带引号的(在这种情况下,它们不能包含空格或引号)。最好首先创建一个正则表达式来分别处理带引号和不带引号的值,然后将两个正则表达式合并为一个正则表达式以涵盖两种情况。最好使用命名组来使正则表达式更具可读性。这并不容易,尤其是在字符类内部不能使用反向引用的情况下。

    extract_tags.py 中提供了一个解决方案,少于 35 行。标签和属性正则表达式只有一行。属性名称-值正则表达式有半打行,并使用交替、条件匹配(两次,其中一次嵌套在另一次内部)以及贪婪和非贪婪量词。


脚注

1. 一本关于正则表达式的好书是 Jeffrey E. F. Friedl 撰写的 Mastering Regular Expressions,ISBN 0596528124。它没有明确涵盖 Python,但 Python 的 re 模块提供的功能与本书深入介绍的 Perl 正则表达式引擎非常相似。

2. 请注意,反向引用不能在字符类内部使用,即在 [] 内部。

3. 用于标志的字母与 Perl 的正则表达式引擎使用的字母相同,这就是为什么 s 用于 re.DOTALL,而 x 用于 re.VERBOSE



© Copyright Pearson Education。保留所有权利。

加载 Disqus 评论