正则表达式HOWTO

作者:

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

引言

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

正则表达式模式被编译成一系列字节码,然后由一个用 C 编写的匹配引擎执行。对于高级用法,可能需要仔细注意引擎如何执行给定的 RE,并以某种方式编写 RE 以生成运行更快的字节码。本文档不涉及优化,因为它需要你对匹配引擎的内部原理有很好的理解。

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

简单模式

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

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

匹配字符

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

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

以下是元字符的完整列表;它们在本文档的其余部分将进行讨论。

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

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

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

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

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

一些以 '\' 开头的特殊序列表示预定义的字符集,这些字符集通常很有用,例如数字集、字母集或非空白字符集。

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

以下特殊序列列表不完整。有关序列的完整列表以及 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},其中 *m* 和 *n* 是十进制整数。此量词表示必须至少重复 *m* 次,最多重复 *n* 次。例如,a/{1,3}b 将匹配 'a/b''a//b''a///b'。它不会匹配没有斜杠的 'ab',也不会匹配有四个斜杠的 'a////b'

你可以省略 *m* 或 *n*;在这种情况下,会为缺失的值假设一个合理的值。省略 *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。)多个标志可以通过按位或运算来指定;例如,re.I | re.M 设置 IM 两个标志。

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

Flag

含义

ASCII, A

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

DOTALL, S

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

IGNORECASE, I

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

LOCALE, L

执行本地化感知的匹配。

MULTILINE, M

多行匹配,影响 ^$

VERBOSE, X (表示“扩展”)

启用详细 REs,可以更清晰、更易懂地组织它们。

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

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

通常,^ 只匹配字符串的开头,而 $ 只匹配字符串的结尾和紧邻字符串末尾的换行符(如果有)。当指定此标志时,^ 匹配字符串的开头以及字符串中每行的开头,紧随每个换行符之后。类似地,$ 元字符匹配字符串的结尾和每行的结尾(紧邻每个换行符之前)。

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 而改变。这意味着零宽度断言永远不应该重复,因为如果它们在给定位置匹配一次,那么它们显然可以匹配无限多次。

|

交替,或者“或”运算符。如果 *A* 和 *B* 是正则表达式,A|B 将匹配任何匹配 *A* 或 *B* 的字符串。| 具有非常低的优先级,以便在交替多字符字符串时使其合理工作。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

仅匹配字符串的末尾。

\Z

\z 相同。为了与旧版 Python 兼容。

\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: author@example.com
User-Agent: Thunderbird 1.5.0.9 (X11/20061227)
MIME-Version: 1.0
To: editor@example.com

这可以通过编写一个匹配整个报头行的正则表达式来处理,该正则表达式有一个组匹配报头名称,另一个组匹配报头的值。

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

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

'(', ')' 指示的分组还会捕获它们匹配文本的起始和结束索引;这可以通过向 group(), start(), end()span() 传递参数来检索。分组从 0 开始编号。分组 0 总是存在的;它是整个 RE,因此 匹配对象 方法都将分组 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 的字符串字面量也使用反斜杠后跟数字来允许在字符串中包含任意字符,因此在 RE 中包含反向引用时务必使用原始字符串。

例如,以下 RE 检测字符串中的重复单词。

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

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

非捕获组和命名组

复杂的 REs 可能会使用许多分组,既用于捕获感兴趣的子字符串,也用于对 RE 本身进行分组和结构化。在复杂的 REs 中,跟踪分组编号变得困难。有两个功能可以帮助解决这个问题。它们都使用通用的正则表达式扩展语法,所以我们先来看一下。

Perl 5 以其对标准正则表达式的强大补充而闻名。对于这些新功能,Perl 开发者无法选择新的单键元字符或以 \ 开头的新特殊序列,而不会使 Perl 的正则表达式与标准 REs 产生令人困惑的差异。例如,如果他们选择 & 作为新元字符,那么旧表达式会假定 & 是一个普通字符,并且不会通过编写 \&[&] 来转义它。

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

Python 支持 Perl 的几种扩展,并在 Perl 的扩展语法之上添加了一个扩展语法。如果问号后的第一个字符是 P,你就知道它是 Python 特有的扩展。

现在我们已经了解了通用扩展语法,我们可以回到简化复杂 REs 中分组操作的功能。

有时你会想用一个组来表示正则表达式的一部分,但对检索组的内容不感兴趣。你可以通过使用一个非捕获组来明确这一点:(?:...),其中你可以用任何其他正则表达式替换 ...

>>> 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().']

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

>>> 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 作为第一个参数添加,但其他方面是相同的。

>>> 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])

返回通过用替换值 replacement 替换 string 中最左边不重叠的 RE 出现项而获得的字符串。如果未找到模式,则返回 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 循环,而不是大型、更通用的正则表达式引擎。

一个例子可能是用另一个字符串替换一个固定的字符串;例如,您可能将 word 替换为 deedre.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 撰写的《精通正则表达式》,由 O'Reilly 出版。不幸的是,它专门关注 Perl 和 Java 风格的正则表达式,完全不包含任何 Python 材料,因此它对在 Python 中编程没有参考价值。(第一版涵盖了 Python 现在已删除的 regex 模块,这对您帮助不大。)请考虑从您的图书馆借阅它。