正则表达式 HOWTO

作者:

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

简介

正则表达式(称为 RE、regexes 或 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 在字符串文字中使用相同字符用于相同目的相冲突。

假设您想编写一个匹配字符串 \section 的 RE,该字符串可能出现在 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()

返回一个包含匹配的 (start, end) 位置的元组

尝试这些方法很快就会弄清楚它们的含义

>>> 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 位区域设置。Unicode 匹配在 Python 3 中已默认启用,用于 Unicode (str) 模式,并且它能够处理不同的区域设置/语言。

re.M
re.MULTILINE

^$ 尚未解释;它们将在 More Metacharacters 部分介绍。)

通常 ^ 仅匹配字符串的开头,而 $ 仅匹配字符串的结尾,并且在字符串结尾的换行符(如果有)之前立即匹配。当指定此标志时,^ 在字符串的开头和字符串中每行的开头(紧随每个换行符之后)匹配。类似地,$ 元字符在字符串的结尾和每行的结尾(紧接每个换行符之前)匹配。

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)

没有 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,则要使用的 RE 是^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 转换为退格符,并且您的 RE 不会像您期望的那样匹配。以下示例看起来与我们之前的 RE 相同,但省略了 RE 字符串前面的 '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'>

其次,在字符类内部,由于此断言没有用,\b 代表退格符,以与 Python 的字符串文字兼容。

\B

另一个零宽度断言,这是 \b 的反面,仅在当前位置不在单词边界时匹配。

分组

通常您需要获取的信息不仅仅是 RE 是否匹配。正则表达式通常用于通过编写一个分成几个子组的 RE 来剖析字符串,这些子组匹配感兴趣的不同组件。例如,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 始终存在;它是整个正则表达式,因此 匹配对象 方法都以组 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() 方法将字符串分割成多个部分,分割点为正则表达式匹配到的位置,并返回一个包含这些部分的列表。它类似于字符串的 split() 方法,但它在分割符方面提供了更大的通用性;字符串的 split() 方法只支持按空格或固定字符串分割。正如您所期望的那样,也存在一个模块级别的 re.split() 函数。

.split(string[, maxsplit=0])

根据正则表达式的匹配结果分割string。如果正则表达式中使用了捕获组,那么捕获组的内容也会作为结果列表的一部分返回。如果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中正则表达式匹配到的最左侧的非重叠部分替换为替换值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,将被替换为正则表达式中相应组匹配的子字符串。这使您可以在生成的替换字符串中合并原始文本的部分。

此示例匹配单词 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>

RE 匹配 '<''<html>' 中,而 .* 消耗了字符串的其余部分。但是,RE 中还有更多内容,而 > 无法在字符串末尾匹配,因此正则表达式引擎必须逐个字符回溯,直到找到 > 的匹配项。最终匹配从 '<''<html>' 中扩展到 '>''</title>' 中,这不是您想要的。

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

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

(请注意,使用正则表达式解析 HTML 或 XML 很痛苦。快速且肮脏的模式将处理常见情况,但 HTML 和 XML 具有会破坏明显正则表达式的特殊情况;当您编写处理所有可能情况的正则表达式时,模式将变得非常复杂。对于此类任务,请使用 HTML 或 XML 解析器模块。)

使用 re.VERBOSE

到目前为止,您可能已经注意到正则表达式是一种非常紧凑的表示法,但它们的可读性并不高。中等复杂度的 RE 可能变成反斜杠、括号和元字符的冗长集合,使它们难以阅读和理解。

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

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

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 的 Mastering Regular Expressions,由 O'Reilly 出版。不幸的是,它专门集中在 Perl 和 Java 的正则表达式风格上,并且根本不包含任何 Python 材料,因此它对 Python 编程没有用作参考。(第一版涵盖了 Python 现在已删除的 regex 模块,这对你没有太大帮助。)考虑从你的图书馆借阅它。