3. [翻译] Python正则表达式

Note

这是自己翻译的一篇Python官网的Howto教程,写得非常通俗易懂,掌握该教程,能解决日常工作中绝大部分的正则模式问题。

Author: A.M. Kuchling amk@amk.ca

Website: https://docs.python.org/2/howto/regex.html#


3.1. 摘要

这份教程是Python中使用re模块操作正则表达式的入门教程,相对于库参考手册,它提供了一份更优雅的介绍。

3.2. 简介

re模块在1.5版本加入Python,并提供Perl风格的正则模式。之前的Python版本提供了regex模块,它提供Emacs风格的正则模式,regex模块在Python2.5被完全移除。

正则表达式是嵌入Python的微小的、高度专门化的语言,可以通过re模块访问。用这门微小语言,你可以为你想要匹配的字符串指定一个规则;这可能包括英文句子,电子邮件地址,Tex命令或其他任何你喜欢的。然后你可以问:这个字符串匹配该模式吗?或,这个字符串是否存在一个匹配模式?使用re你还可以修改、切割字符串。

正则模式编译成字节码,然后被用C语言编写的正则引擎执行。对于高级用法(advanced use),我们需要关注引擎如何执行一个给定的RE,并以合适的方式编写RE使得字节码可以更快执行。该文档不会包含正则表达式优化,因为你需要你对匹配引擎内部有一个清晰的理解。

正则模式语言相对简单,并很严格,所以并不是所有的字符串处理任务都可以用正则表达式完成。有些任务也可以用RE完成,但是表达式会很复杂。这种情况下,你最好编写Python代码完成;虽然Python代码比精心构造的RE慢,但却更好理解。

3.3. 简单模式

我们从最简单的模式开始学习。由于RE被用来操作字符串,所有我们从最常见任务开始:字符匹配。

关于正则表达式所隐匿的计算机科学知识(确定和非确定有限自动机),你可以参考任一本有关编译器构造的书。

3.3.1. 匹配字符

大部分字符都只会匹配它本身。例如:正则模式test会准确匹配字符串test。(当然,你可以使用大小写不敏感模式,让该模式匹配TestTEST,后面会详细介绍。)

该规则存在例外;有些字符是特殊元字符,它不会匹配它自身。这些元字符通常表示一些不寻常的匹配操作,或者通过重复、修改匹配意义来影响正则模式的其他部门。该文档的大部分都是有关不同元字符的讨论。

这里有元字符的完整列表,接下来会详细讨论他们的含义。

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

我们讨论的第一组元字符是[]。他们用来指定字符类,表示你想要匹配的字符集合。字符集可以单独列出,也可以通过两个字符外加一个-分隔符表明一个范围。例如,[abc]会匹配字符a,b,c中的任一个;它和[a-c]相同,或者是用范围表示字符集。如果你只想匹配小写字符,你的正则模式可以是[a-z]

元字符在字符类中没有特殊含义。比如,[akm$]只能匹配a, k, m, 通常是元字符,但是在字符类中,它就没有特殊含义了。

你还可以通过集合的补集来匹配字符类中没有列出的字符。可以通过在字符类前加上^^在字符类之外只会匹配^自身。如:[^5]可以匹配除5之外的任何字符(需要消耗一个字符,不能匹配空)。

可能最重要的元字符就是反斜杠\了。在Python字符串字面量中,\之后可以跟不同的字符,表示特殊的序列。你也可以对元字符进行转义,这样在模式中也可以匹配他们。比如,你想匹配]\,那你可以在他们之前加上\\]\\

一些以\开头的特殊序列代表我们经常使用的字符集,如数字集合,字母集合,或除空白之外的其他字符。下面我会讨论这些可用的特殊序列子集。他们等价的字符类是字节串模式的。他们的完整列表和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,它甚至也可以匹配空行。通常假如你想匹配任意字符时,就用.

3.3.2. 重复匹配

可以匹配不同的字符集时正则的第一大任务。当然,如果RE仅有这点能力,它也确实没有什么高级之处。另一个能力是指定正则模式的某部分的重复匹配次数。

我们先来看*。它不匹配字符’*’,它指定它之前的字符可以匹配0次或多次,而不是只能一次。

例如,ca*t可以匹配’ct’,’cat’,’caaat’等等。在正则引擎内部有一个限制,由于C语言的int类型,所以最多可以匹配一个字符20亿次。你几乎不可能有足够的内存构造这么大的字符串,所以你很少会碰到这个限制。

重复匹配默认是贪婪的,匹配引擎会匹配尽可能多的字符。如果模式的后部分不匹配,匹配引擎会回退,尝试少一些的匹配次数。

通过详细匹配步骤解释该问题。让我们考虑模式a[bcd]*b,它会匹配a,0个或多个字符类[bcd]中的字符,最后以b结尾。现在想象用这个模式匹配字符串’abcbd’。

  1. 匹配a;
  2. 引擎匹配[bcd]*,匹配尽可能多的字符,然后到达字符串末尾;
  3. 尝试匹配b,但是现在已经到达字符串末尾,所有失败;
  4. 回退一个位置,[bcd]*少匹配一个字符;
  5. 尝试匹配b,但是最后一个字符是d,匹配失败;
  6. 再次回退,[bcd]*只匹配bc;
  7. 再次尝试,当前位置是b,匹配成功。

至此已经到达模式末尾,它已经成功匹配了abcb。这证明,匹配引擎会匹配尽可能多的字符,如果找不到匹配,那么他会回退,并尝试匹配模式的剩余部分。它会回退直到它匹配[bcd]*0次,如果还失败,那引擎可以断定字符串和模式不匹配。

另一个有关重复的元字符是+,注意它和*的不同,+至少需要匹配一次;

还有一些重复量词?,它匹配0次或1次,我们可以认为他是标记某些字符是可选的。

最复杂的重复量词是{m,n},其中m和n是十进制整数。该量词表示最少重复m次,最多n次。你还可以忽略m或者n;忽略m表示最少匹配0次,忽略n代表无穷大,实际上,上限是之前提到的20亿。

精简主义者可能发现其他的三种量词可以用这种记法表示:{0,}和*等价,{1,}和+等价,{0,1}和?等价。但是你最好使用*,+和?,因为他们更简洁易读。

3.4. 使用正则

我们已经学了一点正则模式了,那么在Python中如何使用他们呢?re模块提供一个正则引擎接口,允许你编译re对象,然后执行匹配操作。

3.4.1. 编译正则表达式

正则表达式编译成模式对象,它拥有不同的方法,用来执行模式搜索和替换操作。

>>> import re
>>> p = re.compile('ab*')
>>> p
<_sre.SRE_Pattern object at 0x...>

re.compile参数还有一个可选的flag参数,从而支持特殊的语法特性。比如:

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

正则模式以字符串形式传递给re.compile。之所以这样做,因为正则表达式不是Python语音核心的一部分,也没有什么特殊的语法表示他们。(并不是所有的应用程序都需要正则表达式,所以也没有必要包含他们,使得Python语言规范更臃肿)。相反,re模块只是一个简单的C扩展模块,和socket,zlib模块一样。

正则模式放进字符串使得使得Python核心比较简单,但是它也有一个很头疼的问题,这就是下一节的主题。

3.4.2. 麻烦的反斜杠\

这前面的描述中,我们知道正则表达式使用\来表示特殊的字符序列(如\d)和进行元字符转义(如\[)。这和Python字符串字面量的某些字符用法冲突。

比如你想写一个正则模式匹配字符串”\section”(不包括引号),这在LaTeX文件中很常见。从想要匹配的字符串开始,你需要在每一个反斜杠和元字符前插入反斜杠进行转义,所以正则模式是\\section,这也是需要传递给re.compile()的字符串。但是,为了用字符串字面量表示这个模式,每一个反斜杠需要再一次转义。

字符串 阶段
\section 需要匹配的字符串
\section 为re.compile()对反斜杠转义
“\\section” 为字符串字面量对反斜杠转义
注:在md源文件中,需要对,所有一个想要显示一个反斜杠就要在md源文件中输入两个反斜杠。

简而言之,为了匹配一个反斜杠’‘,正则模式字符串需要写成”\\”(四个反斜杠),因为正则表达式是’\’,然后每一个包含在字符串字面量中的反斜杠需要表示为’\’。这种大量的重复反斜杠,使得模式字符串很难理解。

译注:据此,在Python中分析一个正则模式字符串时,先看模式字符串字面量,然后看传递给re.compile的模式,再在正则引擎中分析最终的匹配模式。

解决方法是使用Python的原始字符串标记法,在r前缀开头的字符串字面量中,反斜杠不进行任何特殊处理。所以r"\n"是一个包含两个字符’‘,’n’的字符串,而”“是包含一个换行符的字符串。正则表达式通常在Python代码中写成原始字符串形式。

正则模式 原始字符串
“ab*” r”ab*”
“\\section” r”\section”
“\w+\s+\1” r”\w+\s+”

3.4.3. 执行匹配

假如你有一个编译过的正则表达式对象,你会怎么做?模式对象具有很多的属性和方法,这里只会列出最重要的。完整的参考手册请看re库手册。

方法/属性 目的
match() 模式是否匹配字符串开头
search() 扫描字符串,检查和模式相匹配的位置
findall() 查找所有和模式相匹配的子串,并以列表形式返回
finditer() 查找所有和模式相匹配的子串,并以迭代器方式返回

如果不匹配,search()和match()返回None。如果匹配,则会返回一个match对象,该对象包含匹配信息:起始和终止信息,匹配的子串等。

你可以在交互环境学习re模块,如果你可以访问Thinter,那么你可以看看redemo.py,这是一个Python示范工程,允许你输入一个模式和字符串,然后输出匹配结果,它在调试复杂的正则表达式时很有用。Kodos也是开发和测试正则模式的有力交互工具。

我们的教程使用标准Python解释器测试我们的例子:

Python 2.2.2 (#1, Feb 10 2003, 12:57:01)
>>> import re
>>> p = re.compile('[a-z]+')
>>> p  #doctest: +ELLIPSIS
<_sre.SRE_Pattern object at 0x...>

现在你可以使用不同的字符串测试模式[a-z]+,该模式不匹配空串,并返回None。

现在我们用”tempo”进行测试,这时,match()会返回一个match对象。

>>> m = p.match('tempo')
>>> m
<_sre.SRE_Match object at 0x...>

match对象具有很多方法和属性,最重要的包括如下:

方法/属性 目的
group() 返回和模式相匹配的字符串
start() 匹配的起始位置
end() 匹配的截止位置
span() 返回匹配的位置元组:(start, end)
>>> m.group()
'tempo'
>>> m.start(), m.end()
(0, 5)
>>> m.span()
(0, 5)

group()返回和模式相匹配的子串,由于match()只检查字符串开始位置,因此start()函数总是返回0。而search()函数会扫描整个字符串,所以匹配开始位置可能不是0.

>>> print p.match('::: message')
None
>>> m = p.search('::: message'); print m
<_sre.SRE_Match object at 0x...>
>>> m.group()
'message'
>>> m.span()
(4, 11)

而在实用程序中,经常是在一个变量中保存match对象,然后检查它是否为空。如:

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

还有另外两个方法返回模式的所有匹配,findall()方法返回匹配字符串的列表。

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

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)

3.4.4. 模块级函数

你没有必要创建一个模式对象,然后调用它的方法。re模块也定义了一些顶级函数如match(), search(), findall(), sub()等。这些方法以模式字符串作为第一个参数,其他的对应参数保持不变,并依然返回None和match对象。

>>> print re.match(r'From\s+', 'Fromage amk')
None
>>> re.match(r'From\s+', 'From amk Thu May 14 19:12:10 1998')
<_sre.SRE_Match object at 0x...>

在这种情况下,这些方法也创建一个模式对象,然后调用合适的方法,他们也会在缓存里保存编译过的模式对象, 所以使用相同的正则模式会更快。

一般来说,更推荐使用编译过的模式对象然后调用它的方法,而不是模块级函数。

ref = re.compile( ... )
entityref = re.compile( ... )
charref = re.compile( ... )
starttagopen = re.compile( ... )

3.4.5. 编译标志

编译标志允许你修改正则表达式的某些工作方式,re模块为编译标志提供两个名字,如re.IGNORECASE和re.I。可以使用位或运算指定多个编译标志,如re.I | re.M表示同时设定I和M标志。

可用标志列表:

标志 含义
DOTALL, S 使.可以匹配换行符
IGNORECASE, I 忽略大小写
LOCALE, L 本地化标志
MULTILINE, M 多行匹配,影响^$的含义
VERBOSE, X 宽松排列,使得正则模式更易读
UNICODE, U 使得一些一些转义如,,
I,IGNORECASE
忽略大小写,并且不考虑本地化。除非设置了LOCALE标志;
L,LOCALE
使得,,,;

Locales是一个C库特性,主要用来帮助处理程序中不同语言的差异,如果你在处理法文文本,你想用+来匹配单词,但是;它不匹配’é’或’ç’。如果你的系统经过合适的配置并且选择了法文本地化,那么C函数会告诉程序’é’应该被当成一个字母。在编译模式对象时使用LOCALE对象会使编译对象对;这会更慢,但是却可以让+匹配法文单词。

M,MULTILINE
通常,^只会匹配字符串开头,$只会匹配字符串结尾和或者字符串结尾处的换行符。如果设立了这个标志,^会匹配字符串开头和每行字符串开头(每个换行符之后),$会匹配字符串结尾和每一行结尾(每个换行符之前)。
S,DOTALL
使得.可以匹配换行符;
U,UNICODE
使得, , , , , , 和 ;
X,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)

如果不使用宽松排列,模式是这样的:

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

在这个例子里,使用Python字符串字面量自动串接来分割模式字符串,但是这相对于使用re.VERBOSE更难读。

3.5. 更复杂的模式

这一节,我们会介绍更多的元字符,以及使用分组捕获之前匹配的文本。

3.5.1. 更多的元字符

有些元字符称为零宽断言,他们不会使引擎向后处理字符串,他们不会消耗字符,只会指示成功或者失败。例如:,当前位置是单词边界;这个位置不会被。这意味着零宽断言不能重复,因为如果在一个给定位置匹配一次,那么也可以匹配无数次。

\
选择结构,或者可以认为是or操作符。它具有非常低的优先级,使得你处理多个字符串时可以合理工作,如crow|servo可以匹配crow或者servo,而不是cro,w或者s,然后ervo。为了匹配|,使用\\|,或者放进字符集里如[|]
^
匹配字符串开头,除非设置了MULTILINE标志。在MULTILINE标志里,它也可以匹配换行符之后的位置;
$
匹配字符串结尾,也可以匹配每一个换行符之前的位置。使用\$或者放进字符集里如[|]匹配它自身。
>>> print re.search('}$', '{block}')
<_sre.SRE_Match object at 0x...>
>>> print re.search('}$', '{block} ')
None
>>> print re.search('}$', '{block}\n')
<_sre.SRE_Match object at 0x...>
\A
只匹配字符串开头。在非MULTILINE模式中,\A^是等价的。在MULTILINE模式中,\A还是只匹配字符串开头,开始^可以匹配任意换行符之前的位置。
\Z
\A类似,只匹配字符串结尾。
\b
单词边界,这是一个零宽断言,只匹配单词的开始或者结尾处。单词b被定义为字母和数字序列,所以单词结尾由空白字符或者非字母数字字符标记。比如:
>>> p = re.compile(r'\bclass\b')
>>> print p.search('no class at all')
<_sre.SRE_Match object at 0x...>
>>> print p.search('the declassified algorithm')
None
>>> print p.search('one subclass is')
None

使用\b有两点微妙之处需要特别注意。它和Python的字符串字面量发生了冲突,在字符串字面量中,,ascii值为8。如果你不使用原始字符串,Python会把它转换为回退字符,那么你的正则模式不会按你期望的进行匹配。比如上面的例子,我们忽略r试一试:

>>> p = re.compile('\bclass\b')
>>> print p.search('no class at all')
None
>>> print p.search('\b' + 'class' + '\b')
<_sre.SRE_Match object at 0x...>

第二,在字符类中,该断言不会使用,,这和Python字符串字面量兼容。

\B
零宽断言,和。匹配非单词边界位置。

3.5.2. 分组

通常需要使用正则表达式捕获更多的信息。通常,我们把正则表达式进行分组来匹配不同部分,这样来字符串进行分析。如,RFC-822 header line被分成名字和值,他们之间用:分开,像这样:

From: author@example.com
User-Agent: Thunderbird 1.5.0.9 (X11/20061227)
MIME-Version: 1.0
To: editor@example.com

我们可以编写一个模式来处理,匹配一个完整header line时,一个分组处理名字,另外一个分组处理值。

分组由元字符()标记,和数学表达式括号类似,他们把括号内的表达式分组,你可以使用重复量词重复分组内容,比如*+, ?, {m,n}。如,(ab)*可以匹配0次或多次ab。

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

用括号标记的分组同时会捕获他们匹配的文本的start和end索引;我们可以通过给group(),start(),end()和span()传递参数检索到他们。分组编号从0开始,0分组总是存在,它代表整个正则表达式,所以match对象方法总是以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')
>>> p.search('Paris in the the spring').group()
'the the'

后面你会发现,反向引用在正则替换中非常有用。

3.5.3. 非捕获分组和命名分组

精巧的正则表达式可能谁使用很多分组,即用分组捕获关心的子串,也用来分组正则模式自身。在复杂的模式中,跟踪分组号往往很困难,我们使用两特性来帮助处理该问题。

Perl 5为标准正则表达式增加了很多额外的特性,Python re模块支持其中的大部分。想要在保持和标准正则表达式没有明显不同的情况下,选择新的单击元字符和以反斜杠开始的特殊序列很困难。例如,如果你使用&作为新的元字符,老的表达式认为它是个普通字符,因此没有通过\&和[&]进行转义。

perl开发者的解决方案是:使用(?...)作为扩展语法。?立即跟在括号后面是一个语法错误,因为?它没有重复任何东西,所以这不会引入兼容性问题。跟在?之后的字符表示扩展方式,所以(?=foo)(?:foo)是不同的。(前者是前向断言,后者是非捕获分组)

Python又对perl扩展语法进行了扩展。如果?之后是P,表示这是Python特定扩展,当前存在两种这类扩展:(?P<name>...)定义了命名分组,(?P=name)定义了命名分组的反向引用。如果将来perl 5的将来版本使用不同的语法增加了类似的特性,re模块也会改变来支持新语法,同时为了保持兼容也会保留Python特定语法。

我们已经看到了一般的扩展语法,现在可以返回到使用该特性简化复杂正则模式中的分组了。由于分组从左到右编号,在复杂正则表达式中很难正确跟踪编号。更改这样复杂的正则表达式更犯人,只要插入了一个括号,在此之后所有的括号编号全部改变。

有时你想使用分组收集正则表达式的一部分,但是你不想取回分组中的内容,你可以使用非捕获分组(?:...), …可以用任意其他的正则表达式代替。

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

译注:这里有一个疑问,为什么m.groups()返回的是('c', ),而不是('a', )

除了不能取回分组匹配的内容外,非捕获分组和捕获分组的行为完全相同,你可以在里面放所有的东西,可以使用重复量词,你可以使用分组进行嵌套(捕获和非捕获)。(?:...)在修改一个已经存在的模式时很有用,因为你可以在不修改其他分组编号情况下插入新的分组。需要指出的是,在搜索时捕获和非捕获分组的性能并没有差异,不存在一个比另外一个更快。

更有意义的一个特性是命名分组:分组可以使用名字引用。

命名分组语法是特定Python扩展:(?P<name>...)。命名分组和捕获分组行为完全相同,同时给分组加了一个额外的名字。match对象对捕获分组的所有操作同样对命名分组有用,既可以使用编号也可以使用名字引用分组。命名分组也有编号,所以你可以用两种方式抽取数据:

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

命名分组很方便,因为你可以很容易记住名字,而不是记住分组编号。这里有一个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'"')

相对于使用第9分组,显然使用m.group(‘zonem’)抽取数据更方便。

反向引用语法引用分组编号,如(...)\1。这里有一个变种:使用组名而不是编号引用分组。这又是另外一个Python扩展:(?P=name)表示命名分组内容在当前位置需要再次匹配。查找重复单词的正则模式(\b\w+)\s+\1也可以写成(?P<word>\b\w+)\s+(?P=word):

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

3.5.4. 前向断言

另外一种零宽断言是前向断言,同时存在正向和负向前向断言两种形式。

(?=...)
肯定前向断言。如果由 …表示的正则表达式成功匹配当前位置,则成功,否则失败。但是,一旦包含的表达式已经被尝试,匹配引擎不会再次向前处理;从断言开始的位置开始向右尝试剩下的模式。
(?!...)
否定前向断言。和正向断言相反,如果包含的表达式不匹配字符串当前位置,则匹配成功。

让我们用一个例子来说明前向断言的用处。比如,你想匹配包含扩展名的所有文件,模式相当简单:

.*[.].*$

现在考虑对问题做微小的修改,如果你想匹配扩展名不是bat的文件,你会怎么解决?下面是一些错误的方法。

.*[.][^b].*$

该模式要求文件扩展名不以b开头,但是这是错误的。因为它也不匹配其他的文件扩展名如:foo.bar

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

这种模式更混乱:它要求文件扩展名第一个字母不是b,第二个不是a,第三个不是t。该模式匹配foo.bar,也不匹配auto.bat。但是该模式要求文件扩展名是三个字母,所以不会匹配两个字母的扩展名如mail.cf。然后对模式进行如下修改:

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

在该模式中,第二个和第三个字母是可选的,所以可以匹配少于三个字母的文件扩展名如mail.cf。

模式现在已经很复杂了,很难读也难以理解。更糟糕的是,如果问题发生改变,不匹配bat和exe结尾的文件扩展名,模式会变得更加复杂。

否定前向断言解决了所有这些困惑:

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

该否定前向断言意思是:如果表达式bat不匹配该位置,那么尝试匹配剩余的模式;如果bat是必须的,因为这样可以确保匹配类似sample.batch。[^.]*确保在文件名中有很多点号时也可以正确工作。

现在排除其他的文件扩展名也变得简单:使它变成断言的选择结构即可。例如,下面的模式排除bat和exe结尾的扩展名:

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

3.6. 修改字符串

目前为止,我们都是对固定字符串执行简单的匹配操作。实际上,RE也可以用来修改字符串,使用以下的模式方法:

方法 目标
split() 切割字符串,返回list;只要匹配RE就切割。
sub() 查找所有和RE匹配的子串,然后用一个不同的子串代替。
subn() 和sub()一样,但是返回新的字符串和替换的次数。

3.6.1. 切割字符串

只要和RE匹配,模式的split()方法就可以切割字符串,返回一个list。和字符串的split()方法类似,但是分隔符可以更通用;字符串的split()方法只支持空白符或者固定的字符串。如你所愿,也存在一个模块级别re.split()函数。

split(string[, maxsplit=0])
通过正则匹配切割字符串。如果模式中包含捕获分组括号,那么捕获分组的内容也会成为返回列表的一部分。如果maxsplit参数不为0,那么最多执行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中使用捕获分组括号,那么他们的值也会成为list的一部分。试比较:

>>> 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('[\W]+', 'Words, words, words.')
['Words', 'words', 'words', '']
>>> re.split('([\W]+)', 'Words, words, words.')
['Words', ', ', 'words', ', ', 'words', '.', '']
>>> re.split('[\W]+', 'Words, words, words.', 1)
['Words', 'words, words.']

3.6.2. 查找和替换

另一个常用操作是查找模式匹配,然后进行替换。sub()函数带有一个replacement参数,它可以是一个字符串,也可以是函数,然后进行处理。

.sub(replacement, string[, count=0])
string参数中,对最左边的非重叠的模式匹配部分替换成replacement,并返回新字符串。如果找不到匹配的模式,返回的字符串不便。可选的count参数代表替换的次数,默认为0,代表替换所有的匹配。
>>> 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()功能相同,但是它返回一个包含替换字符串和替换次数的二元组:

>>> 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参数是字符串,会执行字符转义。比如’‘会转化为换行符,未知的转义会直接保留。而反向引用,如’‘,会被模式中匹配的相应分组进行替换,这样允许你把原始字符串的部分包含在返回的新串中:

例如,下面的模式把后面跟随{}的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参数也可以是函数,如果是函数,那么对于模式的每一次不重叠匹配,都会调用该函数一次。在每一次调用中,函数接收match对象,并执行操作。

例如下面的例子,replacement函数把十进制数转化为16进制:

>>> 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()函数,那么模式作为第一个参数,模式可能通过对象或者字符串的形式提供。如果你需要指定RE标志,你可以使用模式对象作为第一个第一个参数,也可以在模式字符串中使用嵌入标志,如:sub("(?i)b+", "x", "bbbb BBBB")返回’x x’。

3.7. 常见问题

正则表达式是很强大的工具,不过由于它不太直观,下面讨论RE中的一些常见问题。

3.7.1. 使用字符串方法

首先,在你使用RE之前,优先考虑使用字符串方法是否可以解决。对于一些固定的字符串,并不需要使用RE特性的操作,使用字符串方法可以更快。

3.7.3. 贪婪和非贪婪匹配

贪婪匹配尝试匹配尽可能多的字符,而非贪婪匹配尝试匹配尽可能少的字符。非贪婪匹配对于类似html标记的工作有用。但是需要指出的是,使用RE解析xml和html非常繁琐,你最好使用专用的解析模块。

3.7.4. 使用re.VERBOSE

你已经注意到,RE记法非常紧凑,但是非常难读。re的复杂性来源于反斜杠,括号,和元字符。对于这种RE,建议指定re.VERBOSE标志,这样你可以以更清晰的方式组织正则模式。首先,模式中空白符会被忽略(字符类中的空白符除外,不能忽略),然后你还可以给模式添加注释;

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*$")更易读。