正则表达式 HOWTO

作者:

A.M. Kuchling <amk@amk.ca>

简介

正则表达式(称为 RE、regex 或 regex 模式)本质上是一种嵌入在 Python 中并通过 re 模块提供的微型、高度专业化的编程语言。使用这种小语言,您可以指定要匹配的可能字符串集合的规则;此集合可能包含英语句子、电子邮件地址、TeX 命令或您喜欢的任何内容。然后,您可以提出诸如“此字符串是否与模式匹配?”或“此字符串中是否有任何位置与模式匹配?”之类的问题。您还可以使用 RE 修改字符串或以各种方式将其拆分。

正则表达式模式被编译成一系列字节码,然后由用 C 编写的匹配引擎执行。对于高级使用,可能需要仔细注意引擎将如何执行给定的 RE,并以某种方式编写 RE,以便生成运行速度更快的字节码。本文档不介绍优化,因为它要求您对匹配引擎的内部结构有很好的了解。

正则表达式语言相对较小且受限,因此并非所有可能的字符串处理任务都可以使用正则表达式完成。还有一些任务可以使用正则表达式完成,但表达式最终会变得非常复杂。在这些情况下,您最好编写 Python 代码来进行处理;虽然 Python 代码比复杂的正则表达式慢,但它可能也更容易理解。

简单模式

我们将从学习最简单的正则表达式开始。由于正则表达式用于处理字符串,我们将从最常见的任务开始:匹配字符。

有关正则表达式背后的计算机科学(确定性和非确定性有限自动机)的详细说明,您可以参考几乎任何关于编写编译器的教科书。

匹配字符

大多数字母和字符将简单地匹配自身。例如,正则表达式 test 将完全匹配字符串 test。(您可以启用不区分大小写的模式,让此 RE 也匹配 TestTEST;稍后会详细介绍。)

此规则有一些例外;某些字符是特殊的元字符,它们不匹配自身。相反,它们表示应该匹配一些不同寻常的内容,或者它们通过重复它们或更改其含义来影响 RE 的其他部分。本文档的大部分内容都致力于讨论各种元字符及其作用。

以下是元字符的完整列表;它们的含义将在本 HOWTO 的其余部分中讨论。

. ^ $ * + ? { } [ ] \ | ( )

我们将看到的第一个元字符是 []。它们用于指定字符类,这是一组您希望匹配的字符。可以单独列出字符,也可以通过给出两个字符并用 '-' 分隔来指示字符范围。例如,[abc] 将匹配字符 abc 中的任何一个;这与 [a-c] 相同,后者使用范围来表示同一组字符。如果您只想匹配小写字母,您的 RE 将是 [a-z]

元字符(除了 \)在类内部不活动。例如,[akm$] 将匹配字符 'a''k''m''$' 中的任何一个;'$' 通常是一个元字符,但在字符类内部,它会失去其特殊性质。

您可以通过补全该集合来匹配类中未列出的字符。这通过在类的第一个字符中包含 '^' 来表示。例如,[^5] 将匹配除 '5' 以外的任何字符。如果插入符号出现在字符类中的其他位置,它没有特殊含义。例如:[5^] 将匹配 '5''^'

也许最重要的元字符是反斜杠 \。与 Python 字符串文字一样,反斜杠后面可以跟各种字符来表示各种特殊序列。它也用于转义所有元字符,以便您仍然可以在模式中匹配它们;例如,如果您需要匹配 [\,您可以在它们前面加上反斜杠以删除其特殊含义:\[\\

'\' 开头的一些特殊序列表示通常有用的预定义字符集,例如数字集、字母集或任何不是空格的字符集。

让我们举一个例子:\w 匹配任何字母数字字符。如果正则表达式模式以字节表示,则它等效于类 [a-zA-Z0-9_]。如果正则表达式模式是一个字符串,\w 将匹配 unicodedata 模块提供的 Unicode 数据库中标记为字母的所有字符。您可以通过在编译正则表达式时提供 re.ASCII 标志,在字符串模式中使用 \w 的更严格定义。

以下特殊序列列表并不完整。有关 Unicode 字符串模式的完整序列列表和扩展类定义,请参阅标准库参考中的 正则表达式语法 的最后一部分。通常,Unicode 版本匹配 Unicode 数据库中相应类别中的任何字符。

\d

匹配任何十进制数字;这等效于类 [0-9]

\D

匹配任何非数字字符;这等效于类 [^0-9]

\s

匹配任何空格字符;这等效于类 [ \t\n\r\f\v]

\S

匹配任何非空格字符;这等效于类 [^ \t\n\r\f\v]

\w

匹配任何字母数字字符;这等效于类 [a-zA-Z0-9_]

\W

匹配任何非字母数字字符;这等效于字符类 [^a-zA-Z0-9_]

这些序列可以包含在字符类中。例如,[\s,.] 是一个字符类,它将匹配任何空白字符,或者 ',' 或者 '.'

本节的最后一个元字符是 .。它匹配除换行符之外的任何字符,并且有一种替代模式(re.DOTALL),在这种模式下,它甚至会匹配换行符。. 通常用于你想匹配“任何字符”的情况。

重复匹配

能够匹配不同字符集是正则表达式的第一项能力,而这种能力是字符串的现有方法无法实现的。然而,如果这只是正则表达式的唯一额外能力,它们就不会有太大的进步。另一个能力是你可以指定 RE 的某些部分必须重复一定的次数。

我们将看到的第一个用于重复匹配的元字符是 ** 不匹配字面字符 '*';相反,它指定前一个字符可以匹配零次或多次,而不是恰好一次。

例如,ca*t 将匹配 'ct'(0 个 'a' 字符)、'cat'(1 个 'a')、'caaat'(3 个 'a' 字符),等等。

诸如 * 之类的重复是贪婪的;当重复 RE 时,匹配引擎会尝试尽可能多地重复它。如果模式的后面部分不匹配,匹配引擎将回溯并尝试减少重复次数。

一个逐步的例子将使这一点更加明显。让我们考虑表达式 a[bcd]*b。它匹配字母 'a',零个或多个来自字符类 [bcd] 的字母,最后以 'b' 结尾。现在想象一下将此 RE 与字符串 'abcbd' 进行匹配。

步骤

已匹配

解释

1

a

RE 中的 a 匹配。

2

abcbd

引擎匹配 [bcd]*,尽可能地匹配到字符串的末尾。

3

失败

引擎尝试匹配 b,但当前位置在字符串的末尾,因此匹配失败。

4

abcb

回溯,使 [bcd]* 少匹配一个字符。

5

失败

再次尝试匹配 b,但当前位置在最后一个字符,这是一个 'd'

6

abc

再次回溯,使 [bcd]* 仅匹配 bc

6

abcb

再次尝试匹配 b。这次当前位置的字符是 'b',因此匹配成功。

现在已经到达 RE 的末尾,并且它已经匹配了 'abcb'。这说明了匹配引擎首先会尽可能地匹配,如果没有找到匹配项,则会逐步回溯并一次又一次地重试 RE 的其余部分。它会回溯,直到尝试对 [bcd]* 进行零次匹配,如果随后失败,引擎将得出结论,该字符串根本不匹配 RE。

另一个重复元字符是 +,它匹配一次或多次。请仔细注意 *+ 之间的区别;* 匹配次或多次,因此重复的任何内容可能根本不存在,而 + 要求至少出现一次。使用类似的例子,ca+t 将匹配 'cat'(1 个 'a')、'caaat'(3 个 'a'),但不会匹配 'ct'

还有两个重复运算符或量词。问号字符 ? 匹配一次或零次;你可以认为它将某事物标记为可选的。例如,home-?brew 匹配 'homebrew''home-brew'

最复杂的量词是 {m,n},其中 mn 是十进制整数。此量词表示必须至少重复 m 次,最多重复 n 次。例如,a/{1,3}b 将匹配 'a/b''a//b''a///b'。它不会匹配没有斜杠的 'ab',也不会匹配有四个斜杠的 'a////b'

你可以省略 mn;在这种情况下,会为缺失的值假定一个合理的值。省略 m 被解释为下限为 0,而省略 n 则导致上限为无穷大。

最简单的情况 {m} 精确匹配前一项 m 次。例如,a/{2}b 将仅匹配 'a//b'

具有还原论倾向的读者可能会注意到,其他三个量词都可以使用此表示法表示。{0,}* 相同,{1,} 等效于 +,而 {0,1}? 相同。最好尽可能使用 *+?,只是因为它们更短且更易于阅读。

使用正则表达式

现在我们已经了解了一些简单的正则表达式,如何在 Python 中实际使用它们呢?re 模块提供了正则表达式引擎的接口,允许你将 RE 编译成对象,然后使用它们执行匹配。

编译正则表达式

正则表达式被编译成模式对象,这些对象具有各种操作的方法,例如搜索模式匹配或执行字符串替换。

>>> import re
>>> p = re.compile('ab*')
>>> p
re.compile('ab*')

re.compile() 还接受一个可选的 flags 参数,用于启用各种特殊功能和语法变体。我们稍后将介绍可用的设置,但现在一个简单的例子就足够了

>>> p = re.compile('ab*', re.IGNORECASE)

RE 作为字符串传递给 re.compile()。RE 被处理为字符串,因为正则表达式不是 Python 核心语言的一部分,也没有创建特殊的语法来表达它们。(有些应用程序根本不需要 RE,因此没有必要通过包含它们来膨胀语言规范。)相反,re 模块只是 Python 中包含的 C 扩展模块,就像 socketzlib 模块一样。

将 RE 放在字符串中可以使 Python 语言更简单,但有一个缺点,这是下一节的主题。

反斜杠瘟疫

如前所述,正则表达式使用反斜杠字符 ('\') 来指示特殊形式,或允许在不调用其特殊含义的情况下使用特殊字符。这与 Python 在字符串文字中使用相同的字符用于相同目的相冲突。

假设你想编写一个 RE 来匹配字符串 \section,该字符串可能会在 LaTeX 文件中找到。要弄清楚在程序代码中写什么,请从要匹配的所需字符串开始。接下来,你必须通过在反斜杠前面加上反斜杠来转义任何反斜杠和其他元字符,从而得到字符串 \\section。必须传递给 re.compile() 的结果字符串必须是 \\section。但是,要将其表达为 Python 字符串文字,必须再次转义两个反斜杠。

字符

阶段

\section

要匹配的文本字符串

\\section

re.compile() 转义的反斜杠

"\\\\section"

为字符串文字转义的反斜杠

简而言之,要匹配字面反斜杠,必须将 '\\\\' 写为 RE 字符串,因为正则表达式必须是 \\,并且每个反斜杠必须在常规 Python 字符串文字中表示为 \\。在反复出现反斜杠的 RE 中,这会导致大量重复的反斜杠,并使生成的字符串难以理解。

解决方案是在正则表达式中使用 Python 的原始字符串表示法;在以 'r' 为前缀的字符串字面量中,反斜杠不会被特殊处理,因此 r"\n" 是一个包含 '\''n' 两个字符的字符串,而 "\n" 是一个包含换行符的单字符字符串。在 Python 代码中,正则表达式通常会使用这种原始字符串表示法编写。

此外,在正则表达式中有效,但作为 Python 字符串字面量无效的特殊转义序列,现在会导致 DeprecationWarning,并最终变为 SyntaxError,这意味着如果没有使用原始字符串表示法或转义反斜杠,这些序列将无效。

普通字符串

原始字符串

"ab*"

r"ab*"

"\\\\section"

r"\\section"

"\\w+\\s+\\1"

r"\w+\s+\1"

执行匹配

一旦你拥有了一个表示已编译正则表达式的对象,你该如何使用它呢?模式对象有几个方法和属性。这里只介绍最重要的几个;请查阅 re 文档以获取完整列表。

方法/属性

目的

match()

确定 RE 是否在字符串的开头匹配。

search()

扫描整个字符串,查找 RE 匹配的任何位置。

findall()

查找所有 RE 匹配的子字符串,并将它们作为列表返回。

finditer()

查找所有 RE 匹配的子字符串,并将它们作为 迭代器 返回。

match()search() 如果找不到匹配项,则返回 None。如果它们成功,则会返回一个 匹配对象 实例,其中包含有关匹配的信息:它开始和结束的位置、它匹配的子字符串等等。

你可以通过交互式地试验 re 模块来了解这一点。

本 HOWTO 使用标准的 Python 解释器作为示例。首先,运行 Python 解释器,导入 re 模块,并编译一个 RE

>>> import re
>>> p = re.compile('[a-z]+')
>>> p
re.compile('[a-z]+')

现在,你可以尝试用各种字符串来匹配 RE [a-z]+。空字符串应该完全不匹配,因为 + 表示“一次或多次重复”。在这种情况下,match() 应该返回 None,这将导致解释器不打印输出。你可以显式地打印 match() 的结果来明确这一点。

>>> p.match("")
>>> print(p.match(""))
None

现在,让我们尝试一个它应该匹配的字符串,例如 tempo。在这种情况下,match() 将返回一个 匹配对象,因此你应该将结果存储在变量中以供以后使用。

>>> m = p.match('tempo')
>>> m
<re.Match object; span=(0, 5), match='tempo'>

现在,你可以查询 匹配对象 以获取有关匹配字符串的信息。匹配对象实例也有几个方法和属性;最重要的几个是

方法/属性

目的

group()

返回 RE 匹配的字符串

start()

返回匹配的起始位置

end()

返回匹配的结束位置

span()

返回一个包含匹配的(起始位置、结束位置)的元组

尝试这些方法很快就会明确它们的含义

>>> m.group()
'tempo'
>>> m.start(), m.end()
(0, 5)
>>> m.span()
(0, 5)

group() 返回 RE 匹配的子字符串。start()end() 返回匹配的起始和结束索引。span() 返回单个元组中的起始和结束索引。由于 match() 方法仅检查 RE 是否在字符串的开头匹配,因此 start() 将始终为零。但是,模式的 search() 方法会扫描整个字符串,因此在这种情况下,匹配可能不会从零开始。

>>> print(p.match('::: message'))
None
>>> m = p.search('::: message'); print(m)
<re.Match object; span=(4, 11), match='message'>
>>> m.group()
'message'
>>> m.span()
(4, 11)

在实际程序中,最常见的样式是将 匹配对象 存储在变量中,然后检查它是否为 None。这通常看起来像这样

p = re.compile( ... )
m = p.match( 'string goes here' )
if m:
    print('Match found: ', m.group())
else:
    print('No match')

有两个模式方法返回模式的所有匹配项。findall() 返回一个匹配字符串的列表

>>> p = re.compile(r'\d+')
>>> p.findall('12 drummers drumming, 11 pipers piping, 10 lords a-leaping')
['12', '11', '10']

此示例中需要 r 前缀,使字面量成为原始字符串字面量,因为普通“烹饪过的”字符串字面量中未被 Python 识别的转义序列,而不是正则表达式,现在会导致 DeprecationWarning,并最终变为 SyntaxError。请参阅 反斜杠瘟疫

findall() 必须先创建整个列表,然后才能将其作为结果返回。finditer() 方法将 匹配对象 实例的序列作为 迭代器 返回

>>> iterator = p.finditer('12 drummers drumming, 11 ... 10 ...')
>>> iterator  
<callable_iterator object at 0x...>
>>> for match in iterator:
...     print(match.span())
...
(0, 2)
(22, 24)
(29, 31)

模块级函数

你无需创建模式对象并调用其方法;re 模块还提供了顶层函数,名为 match()search()findall()sub() 等等。这些函数接受与相应的模式方法相同的参数,并将 RE 字符串作为第一个参数添加,并且仍然返回 None匹配对象 实例。

>>> print(re.match(r'From\s+', 'Fromage amk'))
None
>>> re.match(r'From\s+', 'From amk Thu May 14 19:12:10 1998')  
<re.Match object; span=(0, 5), match='From '>

在底层,这些函数只是为你创建一个模式对象并在其上调用适当的方法。它们还将编译后的对象存储在缓存中,因此将来使用相同 RE 的调用无需一遍又一遍地解析模式。

你应该使用这些模块级函数,还是应该获取模式并自己调用其方法?如果你在循环中访问正则表达式,则预编译它可以节省一些函数调用。在循环之外,由于内部缓存,差异不大。

编译标志

编译标志允许你修改正则表达式工作方式的某些方面。标志在 re 模块中以两个名称提供,一个长名称,例如 IGNORECASE,和一个短的单字母形式,例如 I。(如果你熟悉 Perl 的模式修饰符,单字母形式使用相同的字母;re.VERBOSE 的简写形式是 re.X,例如。)可以通过按位 OR 运算来指定多个标志;例如,re.I | re.M 设置 IM 标志。

以下是可用标志的表格,随后是对每个标志的更详细解释。

标志

含义

ASCII, A

使诸如 \w\b\s\d 等转义符仅匹配具有相应属性的 ASCII 字符。

DOTALL, S

使 . 匹配任何字符,包括换行符。

IGNORECASE, I

执行不区分大小写的匹配。

LOCALE, L

执行与区域设置相关的匹配。

MULTILINE, M

多行匹配,影响 ^$

VERBOSE, X (表示“扩展”)

启用详细的 RE,它可以组织得更清晰易懂。

re.I
re.IGNORECASE

执行不区分大小写的匹配;字符类和字面字符串将通过忽略大小写来匹配字母。例如,[A-Z] 也会匹配小写字母。除非使用 ASCII 标志禁用非 ASCII 匹配,否则完整的 Unicode 匹配也有效。当 Unicode 模式 [a-z][A-Z]IGNORECASE 标志结合使用时,它们将匹配 52 个 ASCII 字母和 4 个额外的非 ASCII 字母:‘İ’(U+0130,带有点的拉丁大写字母 I),‘ı’(U+0131,无点的拉丁小写字母 i),‘ſ’(U+017F,拉丁小写长 s)和 ‘K’(U+212A,开尔文符号)。 Spam 将匹配 'Spam''spam''spAM''ſpam' (后者仅在 Unicode 模式下匹配)。此小写转换不考虑当前区域设置;如果同时设置 LOCALE 标志,则会考虑。

re.L
re.LOCALE

使 \w\W\b\B 和不区分大小写的匹配依赖于当前区域设置,而不是 Unicode 数据库。

区域设置是 C 库的一项功能,旨在帮助编写考虑语言差异的程序。例如,如果您正在处理编码的法语文本,您希望能够编写 \w+ 来匹配单词,但 \w 在字节模式下仅匹配字符类 [A-Za-z]; 它不会匹配对应于 éç 的字节。如果您的系统配置正确并选择了法语区域设置,某些 C 函数将告诉程序,对应于 é 的字节也应被视为字母。在编译正则表达式时设置 LOCALE 标志将导致生成的编译对象对 \w 使用这些 C 函数;这速度较慢,但也使 \w+ 能够按照预期匹配法语单词。不建议在 Python 3 中使用此标志,因为区域设置机制非常不可靠,它一次只处理一种“文化”,并且仅适用于 8 位区域设置。在 Python 3 中,Unicode(str)模式默认已启用 Unicode 匹配,并且它能够处理不同的区域设置/语言。

re.M
re.MULTILINE

^$ 尚未解释;它们将在 更多元字符 部分中介绍。)

通常,^ 仅在字符串的开头匹配,而 $ 仅在字符串的结尾和紧靠字符串末尾的换行符(如果有)之前匹配。指定此标志后,^ 在字符串的开头以及字符串内每行的开头(紧随每个换行符之后)匹配。同样,$ 元字符在字符串的末尾以及每行的末尾(紧接在每个换行符之前)匹配。

re.S
re.DOTALL

使 '.' 特殊字符匹配任何字符,包括换行符;如果没有此标志,'.' 将匹配换行符以外的任何内容。

re.A
re.ASCII

使 \w\W\b\B\s\S 执行仅限 ASCII 的匹配,而不是完整的 Unicode 匹配。这仅对 Unicode 模式有意义,对字节模式会被忽略。

re.X
re.VERBOSE

此标志允许您编写更易读的正则表达式,因为它在格式化方式上提供了更大的灵活性。指定此标志后,RE 字符串中的空白将被忽略,除非空白位于字符类中或以未转义的反斜杠开头;这使您可以更清晰地组织和缩进 RE。此标志还允许您在 RE 中放置会被引擎忽略的注释;注释由 '#' 标记,该标记既不在字符类中,也没有以未转义的反斜杠开头。

例如,以下是一个使用 re.VERBOSE 的 RE;看看它是否更易于阅读?

charref = re.compile(r"""
 &[#]                # Start of a numeric entity reference
 (
     0[0-7]+         # Octal form
   | [0-9]+          # Decimal form
   | x[0-9a-fA-F]+   # Hexadecimal form
 )
 ;                   # Trailing semicolon
""", re.VERBOSE)

如果没有详细设置,RE 将如下所示

charref = re.compile("&#(0[0-7]+"
                     "|[0-9]+"
                     "|x[0-9a-fA-F]+);")

在上面的示例中,Python 的自动连接字符串文字功能已被用于将 RE 分解为较小的部分,但它仍然比使用 re.VERBOSE 的版本更难理解。

更多模式能力

到目前为止,我们只介绍了正则表达式的部分功能。在本节中,我们将介绍一些新的元字符,以及如何使用组来检索匹配文本的部分内容。

更多元字符

还有一些我们尚未介绍的元字符。其中大部分将在本节中介绍。

接下来要讨论的一些剩余的元字符是零宽度断言。它们不会导致引擎在字符串中前进;相反,它们根本不消耗任何字符,只是成功或失败。例如,\b 是一个断言,表示当前位置位于单词边界;\b 本身不会改变位置。这意味着零宽度断言不应该重复,因为如果它们在给定位置匹配一次,显然可以无限次匹配。

|

选择,或“或”运算符。如果 AB 是正则表达式,A|B 将匹配任何匹配 AB 的字符串。| 的优先级非常低,以便在您选择多字符字符串时使其合理地工作。Crow|Servo 将匹配 'Crow''Servo',而不是 'Cro'、一个 'w' 或一个 'S' 以及 'ervo'

要匹配字面值 '|',请使用 \|,或将其包含在字符类中,如 [|] 中。

^

匹配行首。除非设置了 MULTILINE 标志,否则这将仅匹配字符串的开头。在 MULTILINE 模式下,这也会匹配字符串中每个换行符之后的紧邻位置。

例如,如果您只想匹配行首的单词 From,则使用的正则表达式是 ^From

>>> print(re.search('^From', 'From Here to Eternity'))  
<re.Match object; span=(0, 4), match='From'>
>>> print(re.search('^From', 'Reciting From Memory'))
None

要匹配字面值 '^',请使用 \^

$

匹配行尾,定义为字符串的结尾或后跟换行符的任何位置。

>>> print(re.search('}$', '{block}'))  
<re.Match object; span=(6, 7), match='}'>
>>> print(re.search('}$', '{block} '))
None
>>> print(re.search('}$', '{block}\n'))  
<re.Match object; span=(6, 7), match='}'>

要匹配字面值 '$',请使用 \$ 或将其包含在字符类中,如 [$] 中。

\A

仅匹配字符串的开头。当不在 MULTILINE 模式下时,\A^ 实际上是相同的。在 MULTILINE 模式下,它们是不同的:\A 仍然只匹配字符串的开头,而 ^ 可以在字符串中任何位于换行符之后的位置匹配。

\Z

仅匹配字符串的结尾。

\b

单词边界。这是一个零宽度断言,仅在单词的开头或结尾处匹配。单词被定义为字母数字字符的序列,因此单词的结尾由空格或非字母数字字符表示。

以下示例仅在 class 是一个完整的单词时匹配;当它包含在另一个单词中时,它将不匹配。

>>> p = re.compile(r'\bclass\b')
>>> print(p.search('no class at all'))
<re.Match object; span=(3, 8), match='class'>
>>> print(p.search('the declassified algorithm'))
None
>>> print(p.search('one subclass is'))
None

在使用这个特殊序列时,应该记住两个微妙之处。首先,这是 Python 字符串字面值和正则表达式序列之间最糟糕的冲突。在 Python 的字符串字面值中,\b 是退格字符,ASCII 值为 8。如果您不使用原始字符串,则 Python 会将 \b 转换为退格符,并且您的正则表达式将不会按预期匹配。以下示例看起来与我们之前的正则表达式相同,但在正则表达式字符串前面省略了 'r'

>>> p = re.compile('\bclass\b')
>>> print(p.search('no class at all'))
None
>>> print(p.search('\b' + 'class' + '\b'))
<re.Match object; span=(0, 7), match='\x08class\x08'>

其次,在字符类中,这里没有使用此断言的需要,为了与 Python 的字符串字面值兼容,\b 代表退格字符。

\B

另一个零宽度断言,它与 \b 相反,仅在当前位置不在单词边界时匹配。

分组

通常,您需要获取的信息不仅仅是正则表达式是否匹配。正则表达式通常用于通过编写一个分为几个子组的正则表达式来剖析字符串,这些子组匹配不同的感兴趣的组件。例如,RFC-822 标头行分为标头名称和一个值,用 ':' 分隔,如下所示

From: [email protected]
User-Agent: Thunderbird 1.5.0.9 (X11/20061227)
MIME-Version: 1.0
To: [email protected]

可以通过编写一个匹配整个标头行的正则表达式来处理此问题,该正则表达式具有一个匹配标头名称的组和另一个匹配标头值的组。

组由 '('')' 元字符标记。'('')' 的含义与它们在数学表达式中的含义非常相似;它们将包含在其中的表达式组合在一起,您可以使用量词(例如 *+?{m,n})重复组的内容。例如,(ab)* 将匹配零个或多个 ab 的重复项。

>>> p = re.compile('(ab)*')
>>> print(p.match('ababababab').span())
(0, 10)

'('')' 表示的组还会捕获它们匹配的文本的起始和结束索引;可以通过将参数传递给 group()start()end()span() 来检索这些索引。组从 0 开始编号。组 0 始终存在;它是整个正则表达式,因此 match object 方法都将组 0 作为其默认参数。稍后我们将看到如何表达不捕获其匹配文本范围的组。

>>> p = re.compile('(a)b')
>>> m = p.match('ab')
>>> m.group()
'ab'
>>> m.group(0)
'ab'

子组从左到右从 1 向上编号。组可以嵌套;要确定编号,只需从左到右计算左括号字符。

>>> p = re.compile('(a(b)c)d')
>>> m = p.match('abcd')
>>> m.group(0)
'abcd'
>>> m.group(1)
'abc'
>>> m.group(2)
'b'

group() 可以一次传递多个组号,在这种情况下,它将返回一个元组,其中包含这些组的对应值。

>>> m.group(2,1,2)
('b', 'abc', 'b')

groups() 方法返回一个元组,其中包含所有子组的字符串,从 1 到有多少个子组。

>>> m.groups()
('abc', 'b')

模式中的反向引用允许您指定,较早的捕获组的内容也必须在字符串中的当前位置找到。例如,如果可以在当前位置找到组 1 的确切内容,则 \1 将成功,否则将失败。请记住,Python 的字符串字面值也使用反斜杠后跟数字来允许在字符串中包含任意字符,因此请确保在将反向引用合并到正则表达式中时使用原始字符串。

例如,以下正则表达式检测字符串中重复的单词。

>>> p = re.compile(r'\b(\w+)\s+\1\b')
>>> p.search('Paris in the the spring').group()
'the the'

像这样的反向引用对于仅搜索字符串通常没有用处 - 很少有文本格式以这种方式重复数据 - 但是您很快就会发现它们在执行字符串替换时非常有用。

非捕获组和命名组

精细的正则表达式可以使用许多组,既可以捕获感兴趣的子字符串,也可以对正则表达式本身进行分组和结构化。在复杂的正则表达式中,很难跟踪组号。有两个功能可以帮助解决此问题。它们都使用正则表达式扩展的通用语法,因此我们将首先查看该语法。

Perl 5 因其对标准正则表达式的强大添加而闻名。对于这些新功能,Perl 开发人员不能选择新的单键元字符或以 \ 开头的新特殊序列,而不会使 Perl 的正则表达式与标准正则表达式混淆地不同。例如,如果他们选择 & 作为新的元字符,则旧的表达式会假设 & 是一个常规字符,并且不会通过编写 \&[&] 来转义它。

Perl 开发人员选择的解决方案是使用 (?...) 作为扩展语法。紧跟在括号后的 ? 是一个语法错误,因为 ? 没有可重复的内容,因此这不会引入任何兼容性问题。紧跟在 ? 之后的字符指示正在使用哪个扩展,因此 (?=foo) 是一回事(正向先行断言),而 (?:foo) 是另一回事(包含子表达式 foo 的非捕获组)。

Python 支持 Perl 的几个扩展,并为 Perl 的扩展语法添加了扩展语法。如果问号后的第一个字符是 P,那么您就知道它是特定于 Python 的扩展。

现在我们已经了解了通用的扩展语法,我们可以回到简化在复杂正则表达式中使用组的功能。

有时,您可能想使用组来表示正则表达式的一部分,但不希望检索组的内容。您可以使用非捕获组来明确表示这一事实:(?:...),其中可以将 ... 替换为任何其他正则表达式。

>>> m = re.match("([abc])+", "abc")
>>> m.groups()
('c',)
>>> m = re.match("(?:[abc])+", "abc")
>>> m.groups()
()

除了无法检索组匹配的内容之外,非捕获组的行为与捕获组完全相同;你可以将任何内容放入其中,使用诸如 * 之类的重复元字符重复它,并在其他组(捕获或非捕获)中嵌套它。当你修改现有模式时,(?:...) 特别有用,因为你可以添加新的组,而无需更改所有其他组的编号。应该提到的是,在搜索中捕获组和非捕获组之间没有性能差异;两种形式都不比另一种快。

一个更重要的特性是命名组:组可以通过名称引用,而不是通过数字引用。

命名组的语法是 Python 特有的扩展之一:(?P<name>...)name 显然是组的名称。命名组的行为与捕获组完全相同,并且还为组关联一个名称。处理捕获组的 匹配对象 方法都接受整数(通过数字引用组)或包含所需组名称的字符串。命名组仍然被赋予数字,因此你可以通过两种方式检索有关组的信息。

>>> p = re.compile(r'(?P<word>\b\w+\b)')
>>> m = p.search( '(((( Lots of punctuation )))' )
>>> m.group('word')
'Lots'
>>> m.group(1)
'Lots'

此外,你可以使用 groupdict() 将命名组作为字典检索。

>>> m = re.match(r'(?P<first>\w+) (?P<last>\w+)', 'Jane Doe')
>>> m.groupdict()
{'first': 'Jane', 'last': 'Doe'}

命名组很方便,因为它们允许你使用容易记住的名称,而不必记住数字。以下是 imaplib 模块中的一个 RE 示例。

InternalDate = re.compile(r'INTERNALDATE "'
        r'(?P<day>[ 123][0-9])-(?P<mon>[A-Z][a-z][a-z])-'
        r'(?P<year>[0-9][0-9][0-9][0-9])'
        r' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'
        r' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
        r'"')

显然,检索 m.group('zonem') 比记住检索组 9 要容易得多。

诸如 (...)\1 之类的表达式中反向引用的语法是指组的编号。自然地,有一种使用组名称而不是数字的变体。这是另一个 Python 扩展:(?P=name) 表示应在当前位置再次匹配名为 name 的组的内容。用于查找重复单词的正则表达式 \b(\w+)\s+\1\b 也可以写成 \b(?P<word>\w+)\s+(?P=word)\b

>>> p = re.compile(r'\b(?P<word>\w+)\s+(?P=word)\b')
>>> p.search('Paris in the the spring').group()
'the the'

前瞻断言

另一种零宽度断言是前瞻断言。前瞻断言有肯定和否定两种形式,如下所示:

(?=...)

肯定前瞻断言。如果此处以 ... 表示的包含的正则表达式在当前位置成功匹配,则此断言成功,否则失败。但是,一旦尝试了包含的表达式,匹配引擎根本不会前进;模式的其余部分将在断言开始的位置尝试。

(?!...)

否定前瞻断言。这与肯定断言相反;如果包含的表达式与字符串中的当前位置匹配,则此断言成功。

为了使这个具体化,让我们看一个前瞻有用的例子。考虑一个简单的模式来匹配文件名,并将其拆分为基本名称和扩展名,用 . 分隔。例如,在 news.rc 中,news 是基本名称,rc 是文件名的扩展名。

匹配此内容的模式非常简单:

.*[.].*$

请注意,. 需要特殊处理,因为它是一个元字符,因此它在字符类中仅匹配该特定字符。另请注意尾部的 $;添加此项是为了确保字符串的其余部分必须包含在扩展名中。此正则表达式匹配 foo.barautoexec.batsendmail.cfprinters.conf

现在,考虑将问题复杂化一点;如果你想匹配扩展名不是 bat 的文件名,该怎么办?以下是一些不正确的尝试:

.*[.][^b].*$ 上面的第一次尝试试图通过要求扩展名的第一个字符不是 b 来排除 bat。这是错误的,因为该模式也不匹配 foo.bar

.*[.]([^b]..|.[^a].|..[^t])$

当你尝试通过要求匹配以下情况之一来修补第一个解决方案时,表达式会变得更加混乱:扩展名的第一个字符不是 b;第二个字符不是 a;或者第三个字符不是 t。这接受 foo.bar 并拒绝 autoexec.bat,但它需要一个三字母的扩展名,并且不会接受带有两个字母扩展名的文件名,例如 sendmail.cf。我们将再次使模式复杂化,以努力修复它。

.*[.]([^b].?.?|.[^a]?.?|..?[^t]?)$

在第三次尝试中,第二个和第三个字母都是可选的,以便允许匹配短于三个字符的扩展名,例如 sendmail.cf

模式现在变得非常复杂,这使得它难以阅读和理解。更糟糕的是,如果问题发生变化,并且你想同时排除 batexe 作为扩展名,则模式会变得更加复杂和混乱。

负前瞻可以消除所有这些混乱:

.*[.](?!bat$)[^.]*$ 负前瞻意味着:如果表达式 bat 在此点不匹配,请尝试模式的其余部分;如果 bat$ 确实匹配,则整个模式将失败。需要尾部的 $ 来确保允许类似 sample.batch 的内容,其中扩展名仅以 bat 开头。[^.]* 确保当文件名中存在多个点时模式有效。

现在排除另一个文件名扩展名很容易;只需将其作为断言中的替代项添加即可。以下模式排除以 batexe 结尾的文件名:

.*[.](?!bat$|exe$)[^.]*$

修改字符串

到目前为止,我们只是对静态字符串执行搜索。正则表达式通常也用于通过以下模式方法以各种方式修改字符串:

方法/属性

目的

split()

将字符串拆分为列表,并在 RE 匹配的任何位置进行拆分。

sub()

查找 RE 匹配的所有子字符串,并将其替换为不同的字符串。

subn()

sub() 执行相同的操作,但返回新字符串和替换次数。

拆分字符串

模式的 split() 方法会在 RE 匹配的任何位置拆分字符串,并返回各个部分的列表。它类似于字符串的 split() 方法,但在你可以拆分的分隔符中提供了更多的通用性;字符串 split() 仅支持按空格或固定字符串拆分。正如你所期望的那样,也有一个模块级 re.split() 函数。

.split(string[, maxsplit=0])

通过正则表达式的匹配项拆分 *string*。如果在 RE 中使用捕获括号,则它们的内容也将作为结果列表的一部分返回。如果 *maxsplit* 非零,则最多执行 *maxsplit* 次拆分。

你可以通过传递 *maxsplit* 的值来限制拆分次数。当 *maxsplit* 非零时,最多执行 *maxsplit* 次拆分,并且字符串的其余部分将作为列表的最后一个元素返回。在以下示例中,分隔符是任何非字母数字字符序列。

>>> p = re.compile(r'\W+')
>>> p.split('This is a test, short and sweet, of split().')
['This', 'is', 'a', 'test', 'short', 'and', 'sweet', 'of', 'split', '']
>>> p.split('This is a test, short and sweet, of split().', 3)
['This', 'is', 'a', 'test, short and sweet, of split().']

有时候,你不仅对分隔符之间的文本感兴趣,还需要知道分隔符是什么。如果在正则表达式中使用了捕获括号,那么它们的值也会作为列表的一部分返回。比较以下调用:

>>> p = re.compile(r'\W+')
>>> p2 = re.compile(r'(\W+)')
>>> p.split('This... is a test.')
['This', 'is', 'a', 'test', '']
>>> p2.split('This... is a test.')
['This', '... ', 'is', ' ', 'a', ' ', 'test', '.', '']

模块级别的函数 re.split() 将要用作第一个参数的正则表达式添加进去,其他方面都相同。

>>> re.split(r'[\W]+', 'Words, words, words.')
['Words', 'words', 'words', '']
>>> re.split(r'([\W]+)', 'Words, words, words.')
['Words', ', ', 'words', ', ', 'words', '.', '']
>>> re.split(r'[\W]+', 'Words, words, words.', 1)
['Words', 'words, words.']

搜索和替换

另一个常见的任务是找到模式的所有匹配项,并用不同的字符串替换它们。sub() 方法接受一个替换值,它可以是字符串或函数,以及要处理的字符串。

.sub(replacement, string[, count=0])

返回通过将string中最左侧不重叠的 RE 出现的地方替换为替换值replacement而获得的字符串。如果未找到模式,则返回未更改的string

可选参数count是要替换的最大模式出现次数;count 必须是非负整数。默认值 0 表示替换所有出现项。

这是一个使用 sub() 方法的简单示例。它用单词 colour 替换颜色名称

>>> p = re.compile('(blue|white|red)')
>>> p.sub('colour', 'blue socks and red shoes')
'colour socks and colour shoes'
>>> p.sub('colour', 'blue socks and red shoes', count=1)
'colour socks and red shoes'

subn() 方法执行相同的工作,但返回一个包含新字符串值和执行的替换次数的 2 元组

>>> p = re.compile('(blue|white|red)')
>>> p.subn('colour', 'blue socks and red shoes')
('colour socks and colour shoes', 2)
>>> p.subn('colour', 'no colours at all')
('no colours at all', 0)

仅当空匹配项与之前的空匹配项不相邻时,才会被替换。

>>> p = re.compile('x*')
>>> p.sub('-', 'abxd')
'-a-b--d-'

如果 *replacement* 是一个字符串,则会处理其中的任何反斜杠转义。也就是说,\n 会转换为单个换行符,\r 会转换为回车符,依此类推。未知的转义符(例如 \&)会被保留原样。反向引用(例如 \6)会被替换为 RE 中相应组匹配的子字符串。这使你可以将原始文本的部分内容并入生成的替换字符串中。

此示例匹配单词 section,后跟用 {} 括起来的字符串,并将 section 更改为 subsection

>>> p = re.compile('section{ ( [^}]* ) }', re.VERBOSE)
>>> p.sub(r'subsection{\1}','section{First} section{second}')
'subsection{First} subsection{second}'

还有一种语法可以引用由 (?P<name>...) 语法定义的命名组。\g<name> 将使用名为 name 的组匹配的子字符串,而 \g<number> 使用相应的组编号。\g<2> 因此等效于 \2,但在诸如 \g<2>0 之类的替换字符串中没有歧义。(\20 将被解释为对组 20 的引用,而不是对组 2 的引用,后跟文字字符 '0'。)以下替换都是等效的,但使用了替换字符串的所有三种变体。

>>> p = re.compile('section{ (?P<name> [^}]* ) }', re.VERBOSE)
>>> p.sub(r'subsection{\1}','section{First}')
'subsection{First}'
>>> p.sub(r'subsection{\g<1>}','section{First}')
'subsection{First}'
>>> p.sub(r'subsection{\g<name>}','section{First}')
'subsection{First}'

replacement 也可以是一个函数,这会给你更多的控制权。如果 replacement 是一个函数,则对于 pattern 的每次不重叠的出现都会调用该函数。在每次调用时,该函数会传递一个用于匹配的 匹配对象 参数,并且可以使用此信息来计算所需的替换字符串并返回它。

在以下示例中,替换函数将十进制数转换为十六进制数

>>> def hexrepl(match):
...     "Return the hex string for a decimal number"
...     value = int(match.group())
...     return hex(value)
...
>>> p = re.compile(r'\d+')
>>> p.sub(hexrepl, 'Call 65490 for printing, 49152 for user code.')
'Call 0xffd2 for printing, 0xc000 for user code.'

使用模块级 re.sub() 函数时,模式将作为第一个参数传递。模式可以作为对象或字符串提供;如果你需要指定正则表达式标志,则必须使用模式对象作为第一个参数,或者在模式字符串中使用嵌入式修饰符,例如,sub("(?i)b+", "x", "bbbb BBBB") 返回 'x x'

常见问题

正则表达式是一些应用程序的强大工具,但在某些方面,它们的行为并不直观,有时它们的行为方式可能与你期望的不同。本节将指出一些最常见的陷阱。

使用字符串方法

有时使用 re 模块是错误的。如果要匹配固定字符串或单个字符类,并且未使用任何 re 功能(例如 IGNORECASE 标志),则可能不需要正则表达式的全部功能。字符串有几种方法可以执行固定字符串的操作,它们通常更快,因为实现是一个为该目的而优化的单个小型 C 循环,而不是大型、更通用的正则表达式引擎。

一个示例可能是用另一个字符串替换单个固定字符串;例如,你可以用 deed 替换 wordre.sub() 看起来是用于此目的的函数,但请考虑 replace() 方法。请注意,replace() 也会替换单词内的 word,将 swordfish 变为 sdeedfish,但朴素的 RE word 也会这样做。(为了避免对单词的部分执行替换,模式必须是 \bword\b,以便要求 word 在两侧都有单词边界。这使得该工作超出了 replace() 的能力范围。)

另一个常见的任务是从字符串中删除单个字符的每次出现,或将其替换为另一个单个字符。你可以使用类似 re.sub('\n', ' ', S) 的方法来执行此操作,但 translate() 能够执行这两项任务,并且比任何正则表达式操作都快。

简而言之,在转向 re 模块之前,请考虑是否可以使用更快更简单的字符串方法解决你的问题。

贪婪与非贪婪

重复正则表达式时(如 a* 中所示),结果操作是尽可能多地使用模式。当你尝试匹配一对平衡的分隔符(例如 HTML 标签周围的尖括号)时,此事实通常会困扰你。匹配单个 HTML 标签的简单模式不起作用,原因是 .* 的贪婪性质。

>>> s = '<html><head><title>Title</title>'
>>> len(s)
32
>>> print(re.match('<.*>', s).span())
(0, 32)
>>> print(re.match('<.*>', s).group())
<html><head><title>Title</title>

正则表达式会匹配 '<''<html>' 中的部分,并且 .* 会消耗掉字符串的其余部分。然而,正则表达式中还有剩余的部分,而 > 无法匹配字符串的结尾,因此正则表达式引擎不得不逐个字符地回溯,直到找到 > 的匹配项。最终的匹配会从 '<''<html>' 中的部分延伸到 '>''</title>' 中的部分,但这并不是你想要的结果。

在这种情况下,解决方案是使用非贪婪量词 *?+???{m,n}?,它们会匹配尽可能的文本。在上面的例子中,在第一个 '<' 匹配后,会立即尝试匹配 '>',当匹配失败时,引擎会逐个字符地前进,并在每一步都重试 '>' 的匹配。这样会产生正确的结果。

>>> print(re.match('<.*?>', s).group())
<html>

(请注意,使用正则表达式解析 HTML 或 XML 会很痛苦。快速而粗糙的模式可以处理常见的情况,但 HTML 和 XML 有一些特殊情况会破坏显而易见的正则表达式;当你编写一个可以处理所有可能情况的正则表达式时,这些模式将变得非常复杂。对于此类任务,请使用 HTML 或 XML 解析器模块。)

使用 re.VERBOSE

到现在您可能已经注意到,正则表达式是一种非常紧凑的表示法,但它们的可读性并不好。中等复杂度的正则表达式可能会变成一长串的反斜杠、括号和元字符,使其难以阅读和理解。

对于这样的正则表达式,在编译正则表达式时指定 re.VERBOSE 标志可能会有所帮助,因为它允许您更清晰地格式化正则表达式。

re.VERBOSE 标志有几个作用。正则表达式中在字符类中的空格会被忽略。这意味着像 dog | cat 这样的表达式等同于可读性较差的 dog|cat,但是 [a b] 仍然会匹配字符 'a''b' 或一个空格。此外,您还可以在正则表达式中添加注释;注释会从 # 字符延伸到下一个换行符。当与三引号字符串一起使用时,这使得正则表达式可以被更整齐地格式化。

pat = re.compile(r"""
 \s*                 # Skip leading whitespace
 (?P<header>[^:]+)   # Header name
 \s* :               # Whitespace, and a colon
 (?P<value>.*?)      # The header's value -- *? used to
                     # lose the following trailing whitespace
 \s*$                # Trailing whitespace to end-of-line
""", re.VERBOSE)

这比以下方式更具可读性:

pat = re.compile(r"\s*(?P<header>[^:]+)\s*:(?P<value>.*?)\s*$")

反馈

正则表达式是一个复杂的主题。本文档是否帮助您理解了它们?是否有不清楚的部分,或者您遇到的问题在这里没有涵盖?如果是这样,请向作者发送改进建议。

关于正则表达式最完整的书籍几乎可以肯定是 Jeffrey Friedl 的《精通正则表达式》,由 O'Reilly 出版。不幸的是,它完全专注于 Perl 和 Java 风格的正则表达式,并且根本不包含任何 Python 材料,因此它不能作为 Python 编程的参考。(第一版涵盖了 Python 现在已移除的 regex 模块,这对您没有太大帮助。)请考虑从您的图书馆借阅它。