正则表达式Howto

作者

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

摘要

本文档是关于在Python中使用正则表达式 re 模块。它提供了比库参考中相应部分更温和的介绍。

介绍

正则表达式(称为res,或regex,或regex模式)本质上是一种嵌入在Python中的高度专门化的小型编程语言,通过 re 模块。使用这种小语言,您可以为要匹配的一组可能的字符串指定规则;该组可能包含英语句子、电子邮件地址、tex命令或您类似于的任何内容。然后您可以问一些问题,例如“这个字符串是否符合模式?”,或“此字符串中是否有匹配的模式?”。还可以使用res修改字符串或以各种方式将其拆分。

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

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

简单模式

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

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

匹配字符

大多数字母和字符将简单地匹配自己。例如,正则表达式 test 将匹配字符串 test 确切地。(您可以启用不区分大小写的模式,以便重新匹配 TestTEST 还有,稍后再详细介绍。)

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

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

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

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

元字符在类内不活动。例如, [akm$] 将匹配任何字符 'a''k''m''$''$' 通常是元字符,但在字符类中,它被剥夺了其特殊性质。

您可以根据匹配未在类中列出的字符 complementing 集合。这通过包括 '^' 作为类的第一个角色。例如, [^5] 将匹配除 '5' . 如果插入符号出现在字符类的其他地方,则它没有特殊含义。例如: [5^] 将匹配 '5' 或A '^' .

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

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

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

以下特殊序列列表不完整。有关Unicode字符串模式的序列和扩展类定义的完整列表,请参见 Regular Expression Syntax 在标准库参考中。通常,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,.] 是将匹配任何空白字符的字符类,或者 ',''.' .

本节中的最后一个元字符是 .. It matches anything except a newline character, and there's an alternate mode (re.DOTALL) where it will match even a newline. . 通常用于要匹配“任何字符”的地方。

重复的事情

使用字符串上可用的方法,正则表达式能够匹配不同的字符集是第一件不可能做到的事情。然而,如果这是regex唯一的附加功能,那么它们就不会有太大的进步。另一种功能是,您可以指定RE的某些部分必须重复一定次数。

重复我们将要看到的事情的第一个元字符是 * . * 与文字字符不匹配 '*' ;相反,它指定前一个字符可以匹配零次或多次,而不是完全匹配一次。

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

重复,例如 *greedy ;重复RE时,匹配引擎将尝试尽可能多次重复。如果模式的后期部分不匹配,则匹配引擎将备份并以较少的重复次数重试。

一个循序渐进的例子将使这一点更加明显。让我们考虑一下这个表达 a[bcd]*b . 这和信相符 'a' ,0个或更多的类字母 [bcd] 最后以 'b' . 现在想象一下将这个re与字符串匹配 'abcbd' .

步骤

匹配的

解释

1

a

这个 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完全不匹配的结论。

另一个重复的元字符是 + ,匹配一次或多次。注意两者的区别 *+* 比赛 zero 或者更多次,所以任何重复的东西可能根本不存在,而 + 至少需要 one 发生。要使用类似的示例, 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 导致无穷大的上界。

读过还原论者本特的人可能会注意到,其他三个限定符都可以用这个符号来表示。 {{0,}} 是一样的 *{{1,}} 等于 +{{0,1}} 是一样的 ? .最好用一下 *+? 当你能做到的时候,仅仅是因为它们较短而且更容易阅读。

使用正则表达式

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

编译正则表达式

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

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

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

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

再转让给 re.compile() 作为一根绳子。Res被当作字符串处理,因为正则表达式不属于核心的Python语言,并且没有为表达它们创建特殊的语法。(有些应用程序根本不需要res,因此不需要通过包含它们来膨胀语言规范。)相反, re 模块只是包含在Python中的C扩展模块,就像 socketzlib 模块。

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

反斜杠的麻烦

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

假设你想写一个与字符串匹配的re \section 可能在 Latex 文件中找到。要想知道在程序代码中写什么,从需要匹配的字符串开始。接下来,您必须在任何反斜杠和其他元字符前面加上一个反斜杠,从而使字符串 \\section . 必须传递给的结果字符串 re.compile() 必须是 \\section . 但是,要将其表示为python字符串文字,必须对这两个反斜杠进行转义。 再一次 .

文字

舞台

\section

要匹配的文本字符串

\\section

的转义反斜杠 re.compile()

"\\\\section"

字符串文本的转义反斜杠

简而言之,要匹配一个字反斜杠,必须写 '\\\\' 作为re字符串,因为正则表达式必须 \\ ,并且每个反斜杠必须表示为 \\ 在常规的python字符串文本中。在重复使用反斜杠的res中,这会导致大量重复的反斜杠,并使结果字符串难以理解。

解决方案是对正则表达式使用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()

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

search()

扫描一个字符串,查找与此重新匹配的任何位置。

findall()

查找重新匹配的所有子字符串,并将其作为列表返回。

finditer()

查找重新匹配的所有子字符串,并将其作为 iterator .

match()search() 返回 None 如果找不到匹配项。如果他们成功了, match object 返回实例,其中包含有关匹配的信息:匹配的起始和结束位置、匹配的子字符串等等。

您可以通过交互式的 re 模块。如果你有 tkinter 可用,您也可以查看 Tools/demo/redemo.py ,一个包含在python发行版中的演示程序。它允许您输入res和字符串,并显示重新匹配或失败。 redemo.py 在调试复杂的RE时非常有用。

本文将使用标准的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() 将返回 match object ,因此您应该将结果存储在一个变量中,以供以后使用。::

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

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

方法/属性

目的

group()

返回与re匹配的字符串

start()

返回比赛的开始位置

end()

返回比赛的结束位置

span()

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

尝试这些方法将很快阐明其含义:

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

group() 返回与re匹配的子字符串。 start()end() 返回匹配项的开始索引和结束索引。 span() 返回单个元组中的开始索引和结束索引。自从 match() 方法只检查是否在字符串开头重新匹配, 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)

在实际程序中,最常见的样式是存储 match object 在变量中,然后检查 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() 方法返回 match object 实例作为 iterator ::

>>> 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-string作为第一个参数添加,但仍然返回 None 或A match object 实例。::

>>> 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 '>

在hood下,这些函数只是为您创建一个模式对象,并对其调用适当的方法。它们还将编译后的对象存储在缓存中,因此将来使用相同re的调用不需要反复解析模式。

您应该使用这些模块级函数,还是应该自己获取模式并调用其方法?如果要在循环中访问regex,则预编译它将节省一些函数调用。在循环之外,由于内部缓存,没有太大的差异。

编译标志

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

下面是一个可用标记的表,后面是对每个标记的更详细的解释。

意义

ASCII, A

几次逃走就像 \w\b\s\d 仅在具有相应属性的ASCII字符上匹配。

DOTALL, S

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

IGNORECASE, I

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

LOCALE, L

进行区域设置感知匹配。

MULTILINE, M

多行匹配,影响 ^$ .

VERBOSEX (用于“扩展”)。

启用详细的资源,可以更清晰和可理解地组织这些资源。

I
IGNORECASE

执行不区分大小写的匹配;字符类和文本字符串将通过忽略大小写来匹配字母。例如, [A-Z] 也将匹配小写字母。完全Unicode匹配也可以工作,除非 ASCII 标志用于禁用非ASCII匹配。当Unicode模式 [a-z][A-Z]IGNORECASE 标记,它们将匹配52个ASCII字母和4个附加的非ASCII字母:“_”(U+0130,上面带点的拉丁文大写字母I)、“_”(U+0131,拉丁文小写字母无点I)、“_”(U+017F,拉丁文小写字母长S)和“_”(U+212A,开尔文符号)。 Spam 将匹配 'Spam''spam''spAM''ſpam' (后者仅在Unicode模式下匹配)。此低换行不考虑当前区域设置;如果同时设置 LOCALE flag。

L
LOCALE

制作 \w\W\b\B 不区分大小写的匹配取决于当前的区域设置,而不是Unicode数据库。

语言环境是C库的一个特性,旨在帮助编写考虑语言差异的程序。例如,如果您正在处理编码的法语文本,您希望能够 \w+ 匹配单词,但是 \w 只匹配字符类 [A-Za-z] 以字节模式;它将不匹配与 éç . 如果系统配置正确并且选择了法语区域设置,某些C函数将告诉程序与 é 也应视为信函。设置 LOCALE 编译正则表达式时的标志将导致生成的已编译对象使用这些C函数 \w ;这比较慢,但也可以 \w+ 如你所料,匹配法语单词。在python 3中不鼓励使用这个标志,因为区域设置机制非常不可靠,它一次只处理一个“文化”,并且只处理8位区域设置。在python 3中,默认情况下已经为unicode(str)模式启用了unicode匹配,并且它能够处理不同的区域设置/语言。

M
MULTILINE

(^$ 还没有解释,将在第节中介绍 更多元字符

通常 ^ 仅在字符串的开头匹配,并且 $ 只匹配字符串结尾处和字符串结尾处的换行符(如果有)前面。当指定此标志时, ^ 在字符串的开头和字符串中每行的开头匹配,紧跟在每行换行符之后。同样, $ 元字符在字符串的末尾和每行的末尾(紧接着每行换行符的前面)匹配。

S
DOTALL

使 '.' 特殊字符完全匹配任何字符,包括换行符;如果没有此标志, '.' 会匹配任何东西 除了 一条新线。

A
ASCII

制作 \w\W\b\B\s\S 只执行ASCII匹配,而不是完全Unicode匹配。这只对Unicode模式有意义,而对于字节模式则被忽略。

X
VERBOSE

此标志允许您编写更具可读性的正则表达式,方法是为您提供更灵活的格式设置。当指定了此标志时,re字符串中的空白将被忽略,除非空白位于字符类中或前面有一个无范围的反斜杠;这样可以更清晰地组织和缩进re。此标志还允许您将注释放在引擎将忽略的RE中;注释由 '#' 它既不在字符类中,也不前面有一个无转义的反斜杠。

例如,这里有一个使用 re.VERBOSE 看看它读起来容易多少?::

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 .

更多模式功率

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

更多元字符

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

剩下的一些元字符将被讨论 zero-width assertions . 它们不会导致引擎在字符串中前进;相反,它们根本不消耗任何字符,只会成功或失败。例如, \b 是当前位置位于单词边界的断言;该位置不会被 \b 完全。这意味着零宽度断言不应该重复,因为如果它们在给定位置匹配一次,它们显然可以被匹配无数次。

|

或“或”运算符。如果 AB 是正则表达式, A|B 将匹配任何匹配的字符串 AB . | 具有非常低的优先级,以便在交替使用多字符字符串时使其合理工作。 Crow|Servo 两者都匹配 'Crow''Servo' 不是 'Cro' ,A '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 退一步,你的RE将不会像你期望的那样匹配。下面的示例看起来与前面的示例相同,但省略了 'r' 在re字符串前面。::

>>> 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 ,仅当当前位置不在单词边界时匹配。

分组

通常,您需要获得更多的信息,而不仅仅是重新匹配与否。正则表达式通常用于通过将重新划分为多个子组来解析字符串,这些子组与感兴趣的不同组件相匹配。例如,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,所以 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')

模式中的backreferences允许您指定还必须在字符串中的当前位置找到早期捕获组的内容。例如, \1 如果在当前位置可以找到组1的准确内容,则会成功,否则会失败。记住,python的字符串文本还使用反斜杠和数字,以允许在字符串中包含任意字符,因此在re中合并backreference时,一定要使用原始字符串。

例如,下面重新检测字符串中的双字。::

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

像这样的backreferences通常不适用于只搜索一个字符串——很少有文本格式以这种方式重复数据——但是您很快就会发现它们是 very 在执行字符串替换时很有用。

无捕获组和命名组

精心设计的res可以使用许多组,既可以捕获感兴趣的子字符串,也可以对re本身进行分组和结构。在复杂的研究中,很难跟踪组号。有两个功能可以帮助解决这个问题。它们都为正则表达式扩展使用了一个公共语法,所以我们首先来看一下。

Perl5以其对标准正则表达式的强大添加而闻名。对于这些新特性,Perl开发人员无法选择新的单个击键元字符或以 \ 如果Perl的正则表达式选择了 & 例如,作为一个新的元字符,旧表达式将假定 & 是一个普通的角色,不可能通过写作逃脱它 \&[&] .

Perl开发人员选择的解决方案是 (?...) 作为扩展语法。 ? 括号后面紧接着是一个语法错误,因为 ? 不会有任何重复,所以这不会引入任何兼容性问题。后面的字符 ? 指出正在使用的扩展名,因此 (?=foo) 是一件事(积极的前瞻性断言)和 (?:foo) 是另一个(包含子表达式的非捕获组) foo

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

既然我们已经研究了一般的扩展语法,那么我们可以返回到简化处理复杂res中的组的功能。

有时,您可能希望使用一个组来表示正则表达式的一部分,但不希望检索该组的内容。您可以通过使用非捕获组来明确这一事实: (?:...), where you can replace the ... 任何其他正则表达式。::

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

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

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

命名组的语法是特定于python的扩展之一: (?P<name>...) . name 很明显,是该组的名称。命名组的行为与捕获组完全相同,并且还将名称与组关联。这个 match object 处理捕获组的方法都接受按数字引用组的整数或包含所需组名称的字符串。命名组仍然具有编号,因此可以通过以下两种方式检索有关组的信息:

>>> 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 模块:

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。

表达式中backreference的语法,如 (...)\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'

前瞻性断言

另一个零宽度断言是lookahead断言。lookahead断言有正负两种形式,如下所示:

(?=...)

积极的前瞻性断言。如果包含的正则表达式(此处由表示)为 ... ,在当前位置成功匹配,否则失败。但是,一旦尝试了所包含的表达式,匹配引擎就根本不会前进;模式的其余部分将在断言开始的地方进行尝试。

(?!...)

否定的先行断言。这与肯定断言相反;如果包含表达式 在字符串中的当前位置匹配。

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

匹配的模式非常简单:

.*[.].*$

. 需要特别处理,因为它是元字符,所以它在字符类中只匹配该特定字符。还要注意尾随 $ ;这是为了确保字符串的其余部分必须包含在扩展中。此正则表达式与 foo.barautoexec.batsendmail.cfprinters.conf .

现在,考虑把问题复杂化一点;如果您想匹配扩展名不在的文件名,该怎么办? bat ?一些错误的尝试:

.*[.][^b].*$ 上面的第一次尝试试图排除 bat 要求扩展名的第一个字符不是 b . 这是错误的,因为模式也不匹配 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()

将字符串拆分为一个列表,并将其拆分到重新匹配的位置

sub()

查找重新匹配的所有子字符串,并用其他字符串替换它们

subn()

做同样的事 sub() ,但返回新字符串和替换的数目

拆分字符串

这个 split() 模式的方法在重新匹配的地方拆分字符串,返回片段列表。它类似于 split() 字符串的方法,但在可以拆分的分隔符中提供了更广泛的通用性;字符串 split() 只支持按空白或固定字符串拆分。正如您所期望的,有一个模块级别 re.split() 也起作用。

.split(string[, maxsplit=0])

分裂 string 通过正则表达式的匹配。如果在re中使用捕获括号,那么它们的内容也将作为结果列表的一部分返回。如果 最大分割 最多为非零 最大分割 执行分割。

通过为传递一个值,可以限制所做的拆分数。 最大分割 . 什么时候? 最大分割 最多为非零 最大分割 将进行拆分,字符串的其余部分将作为列表的最后一个元素返回。在下面的示例中,分隔符是任何非字母数字字符序列。::

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

返回通过替换re-in最左边的不重叠出现次数获得的字符串。 string 通过替换 替换 . 如果找不到图案, string 返回时不变。

可选参数 计数 是要替换的模式出现的最大数量; 计数 必须是非负整数。默认值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-'

如果 替换 是一个字符串,将处理其中的反斜杠转义。也就是说, \n 转换为单个换行符, \r 转换为回车,依此类推。未知逃逸,如 \& 一个人呆着。回溯引用,例如 \6 ,替换为re中相应组匹配的子字符串。这样可以将原始文本的部分合并到生成的替换字符串中。

这个例子与单词匹配 section 后跟一个字符串 {{}} ,和更改 sectionsubsection ::

>>> 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}'

替换 也可以是一个函数,它可以给你更多的控制。如果 替换 是一个函数,函数被调用为 模式 . 在每次调用时,函数都会 match object 匹配的参数,可以使用此信息计算所需的替换字符串并返回它。

在下面的示例中,替换函数将小数转换为十六进制:

>>> 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 具有 deed . re.sub() 似乎是用于此的函数,但请考虑 replace() 方法。注意 replace() 也将替换 word 字里行间,转身 swordfish 进入之内 sdeedfish 但是天真的 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有特殊的情况会破坏明显的正则表达式;当您编写了处理所有可能情况的正则表达式时,模式将 very 复杂的。对此类任务使用HTML或XML分析程序模块。)

使用re.verbose

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

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

这个 re.VERBOSE 标志有几种效果。正则表达式中的空白 不是 在字符类内被忽略。这意味着 dog | cat 相当于可读性较差的 dog|cat ,但是 [a b] 仍将匹配字符 'a''b' 或者是一个空间。此外,还可以将注释放在re;注释扩展自 # 下一行的字符。当与三重引号字符串一起使用时,这使res的格式更加整齐:

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*$")

反馈

正则表达式是一个复杂的主题。这份文件有助于你理解它们吗?是否有不清楚的地方,或者你遇到的问题没有在这里讨论?如果是,请将改进建议发送给作者。

关于正则表达式的最完整的书几乎可以肯定是杰弗里·弗里德尔的《掌握正则表达式》,由O'Reilly出版。不幸的是,它只专注于Perl和Java的正则表达式的味道,并且根本不包含任何Python材料,因此它不会作为Python编程的参考。(第一版介绍了python的 regex 模块,这对您没有太大帮助。)请考虑从您的库中查看它。