本节书摘来自异步社区《Python核心编程(第3版)》一书中的第1章,第1.3节,作者[美] Wesley Chun(卫斯理 春),孙波翔 李斌 李晗 译,更多章节内容可以访问云栖社区“异步社区”公众号查看。

1.3 正则表达式和Python语言

在了解了关于正则表达式的全部知识后,开始查看Python当前如何通过使用re模块来支持正则表达式,re模块在古老的Python 1.5版中引入,用于替换那些已过时的regex模块和regsub模块——这两个模块在Python 2.5版中移除,而且此后导入这两个模块中的任意一个都会触发ImportError异常。


re模块支持更强大而且更通用的Perl风格(Perl 5风格)的正则表达式,该模块允许多个线程共享同一个已编译的正则表达式对象,也支持命名子组。

1.3.1 re模块:核心函数和方法

表1-2列出了来自re模块的更多常见函数和方法。它们中的大多数函数也与已经编译的正则表达式对象(regex object)和正则匹配对象(regex match object)的方法同名并且具有相同的功能。本节将介绍两个主要的函数/方法——match()和search(),以及compile()函数。下一节将介绍更多的函数,但如果想进一步了解将要介绍或者没有介绍的更多相关信息,请查阅Python的相关文档。



① Python 1.5.2版中新增;2.4版中增加flags参数。

② Python 2.2版中新增;2.4版中增加flags参数。

③ Python 2.7和3.1版中增加flags参数。

核心提示:编译正则表达式(编译还是不编译?)在Core Python Programming或者即将出版的Core Python Language Fundamentals的执行环境章节中,介绍了Python代码最终如何被编译成字节码,然后在解释器上执行。特别是,我们指定eval()或者exec(在2.x版本中或者在3.x版本的exec()中)调用一个代码对象而不是一个字符串,性能上会有明显提升。这是由于对于前者而言,编译过程不会重复执行。换句话说,使用预编译的代码对象比直接使用字符串要快,因为解释器在执行字符串形式的代码前都必须把字符串编译成代码对象。同样的概念也适用于正则表达式——在模式匹配发生之前,正则表达式模式必须编译成正则表达式对象。由于正则表达式在执行过程中将进行多次比较操作,因此强烈建议使用预编译。而且,既然正则表达式的编译是必需的,那么使用预编译来提升执行性能无疑是明智之举。re.compile()能够提供此功能。其实模块函数会对已编译的对象进行缓存,所以不是所有使用相同正则表达式模式的search()和match()都需要编译。即使这样,你也节省了缓存查询时间,并且不必对于相同的字符串反复进行函数调用。在不同的Python版本中,缓存中已编译过的 正则表达式对象的数目可能不同,而且没有文档记录。purge()函数能够用于清除这些缓存。

1.3.2 使用compile()函数编译正则表达式

后续将扼要介绍的几乎所有的re模块函数都可以作为regex对象的方法。注意,尽管推荐预编译,但它并不是必需的。如果需要编译,就使用编译过的方法;如果不需要编译,就使用函数。幸运的是,不管使用函数还是方法,它们的名字都是相同的(也许你曾对此感到好奇,这就是模块函数和方法的名字相同的原因,例如,search()、match()等)。因为这在大多数示例中省去一个小步骤,所以我们将使用字符串替代。我们仍将会遇到几个预编译代码的对象,这样就可以知道它的过程是怎么回事。

对于一些特别的正则表达式编译,可选的标记可能以参数的形式给出,这些标记允许不区分大小写的匹配,使用系统的本地化设置来匹配字母数字,等等。请参考表1-2中的条目以及在正式的官方文档中查询关于这些标记(re.IGNORECASE、re.MULTILINE、re.DOTALL、re.VERBOSE等)的更多信息。它们可以通过按位或操作符(|)合并。

这些标记也可以作为参数适用于大多数re模块函数。如果想要在方法中使用这些标记,它们必须已经集成到已编译的正则表达式对象之中,或者需要使用直接嵌入到正则表达式本身的(?F)标记,其中F是一个或者多个i(用于re.I/IGNORECASE)、m(用于re.M/MULTILINE)、s(用于re.S/DOTALL)等。如果想要同时使用多个,就把它们放在一起而不是使用按位或操作,例如,(?im)可以用于同时表示re.IGNORECASE和re.MULTILINE。

1.3.3 匹配对象以及group()和groups()方法

当处理正则表达式时,除了正则表达式对象之外,还有另一个对象类型:匹配对象。这些是成功调用match()或者search()返回的对象。匹配对象有两个主要的方法:group()和groups()。

group()要么返回整个匹配对象,要么根据要求返回特定子组。groups()则仅返回一个包含唯一或者全部子组的元组。如果没有子组的要求,那么当group()仍然返回整个匹配时,groups()返回一个空元组。

Python正则表达式也允许命名匹配,这部分内容超出了本节的范围。建议读者查阅完整的re模块文档,里面有这里省略掉的关于这些高级主题的详细内容。

1.3.4 使用match()方法匹配字符串

match()是将要介绍的第一个re模块函数和正则表达式对象(regex object)方法。match()函数试图从字符串的起始部分对模式进行匹配。如果匹配成功,就返回一个匹配对象;如果匹配失败,就返回None,匹配对象的group()方法能够用于显示那个成功的匹配。下面是如何运用match()(以及group())的一个示例:

>>> m = re.match('foo', 'foo')  # 模式匹配字符串
>>> if m is not None:     # 如果匹配成功,就输出匹配内容
...   m.group()
...
'foo'

模式“foo”完全匹配字符串“foo”,我们也能够确认m是交互式解释器中匹配对象的示例。

>>> m           # 确认返回的匹配对象
<re.MatchObject instance at 80ebf48>

如下为一个失败的匹配示例,它返回None。

>>> m = re.match('foo', 'bar')# 模式并不能匹配字符串
>>> if m is not None: m.group() # (单行版本的if语句)
...
>>>

因为上面的匹配失败,所以m被赋值为None,而且以此方法构建的if语句没有指明任何操作。对于剩余的示例,如果可以,为了简洁起见,将省去if语句块,但在实际操作中,最好不要省去以避免 AttributeError异常(None是返回的错误值,该值并没有group()属性[方法])。

只要模式从字符串的起始部分开始匹配,即使字符串比模式长,匹配也仍然能够成功。例如,模式“foo”将在字符串“food on the table”中找到一个匹配,因为它是从字符串的起始部分进行匹配的。

>>> m = re.match('foo', 'food on the table') # 匹配成功
>>> m.group()
'foo'

可以看到,尽管字符串比模式要长,但从字符串的起始部分开始匹配就会成功。子串“foo”是从那个比较长的字符串中抽取出来的匹配部分。

甚至可以充分利用Python原生的面向对象特性,忽略保存中间过程产生的结果。

>>> re.match('foo', 'food on the table').group()
'foo'

注意,在上面的一些示例中,如果匹配失败,将会抛出AttributeError异常。

1.3.5 使用search()在一个字符串中查找模式(搜索与匹配的对比)

其实,想要搜索的模式出现在一个字符串中间部分的概率,远大于出现在字符串起始部分的概率。这也就是search()派上用场的时候了。search()的工作方式与match()完全一致,不同之处在于search()会用它的字符串参数,在任意位置对给定正则表达式模式搜索第一次出现的匹配情况。如果搜索到成功的匹配,就会返回一个匹配对象;否则,返回None。

我们将再次举例说明match()和search()之间的差别。以匹配一个更长的字符串为例,这次使用字符串“foo”去匹配“seafood”:

>>> m = re.match('foo', 'seafood')   # 匹配失败
>>> if m is not None: m.group()
...
>>>

可以看到,此处匹配失败。match()试图从字符串的起始部分开始匹配模式;也就是说,模式中的“f”将匹配到字符串的首字母“s”上,这样的匹配肯定是失败的。然而,字符串“foo”确实出现在“seafood”之中(某个位置),所以,我们该如何让Python得出肯定的结果呢?答案是使用search()函数,而不是尝试匹配。search()函数不但会搜索模式在字符串中第一次出现的位置,而且严格地对字符串从左到右搜索。

>>> m = re.search('foo', 'seafood')  # 使用 search() 代替
>>> if m is not None: m.group()
...
'foo'          # 搜索成功,但是匹配失败
>>>

此外,match()和search()都使用在1.3.2节中介绍的可选的标记参数。最后,需要注意的是,等价的正则表达式对象方法使用可选的pos和endpos参数来指定目标字符串的搜索范围。

本节后面将使用match()和search()正则表达式对象方法以及group()和groups()匹配对象方法,通过展示大量的实例来说明Python中正则表达式的使用方法。我们将使用正则表达式语法中几乎全部的特殊字符和符号。

1.3.6 匹配多个字符串

在1.2节中,我们在正则表达式bat|bet|bit中使用了择一匹配(|)符号。如下为在Python中使用正则表达式的方法。

>>> bt = 'bat|bet|bit'       # 正则表达式模式: bat、bet、bit
>>> m = re.match(bt, 'bat')    # 'bat' 是一个匹配
>>> if m is not None: m.group()
...
'bat'
>>> m = re.match(bt, 'blt')    # 对于 'blt' 没有匹配
>>> if m is not None: m.group()
...
>>> m = re.match(bt, 'He bit me!') # 不能匹配字符串
>>> if m is not None: m.group()
...
>>> m = re.search(bt, 'He bit me!') # 通过搜索查找 'bit'
>>> if m is not None: m.group()
...
'bit'

1.3.7 匹配任何单个字符

在后续的示例中,我们展示了点号(.)不能匹配一个换行符n或者非字符,也就是说,一个空字符串。

>>> anyend = '.end'
>>> m = re.match(anyend, 'bend')  # 点号匹配 'b'
>>> if m is not None: m.group()
...
'bend'
>>> m = re.match(anyend, 'end')   # 不匹配任何字符
>>> if m is not None: m.group()
...
>>> m = re.match(anyend, '\nend')  # 除了 \n之外的任何字符
>>> if m is not None: m.group()
...
>>> m = re.search('.end', 'The end.')# 在搜索中匹配 ' '
>>> if m is not None: m.group()
...
' end'

下面的示例在正则表达式中搜索一个真正的句点(小数点),而我们通过使用一个反斜线对句点的功能进行转义:

 >>> patt314 = '3.14'       # 表示正则表达式的点号>>> pi_patt = '3\.14'       # 表示字面量的点号 (dec. point)
>>> m = re.match(pi_patt, '3.14') # 精确匹配
>>> if m is not None: m.group()
...
'3.14'
>>> m = re.match(patt314, '3014') # 点号匹配'0'
>>> if m is not None: m.group()
...
'3014'
>>> m = re.match(patt314, '3.14') # 点号匹配 '.'
>>> if m is not None: m.group()
...
'3.14'

1.3.8 创建字符集([ ])

前面详细讨论了crdp,以及它们与r2d2|c3po之间的差别。下面的示例将说明对于r2d2|c3po的限制将比crdp更为严格。

>>> m = re.match('[cr][23][dp][o2]', 'c3po')# 匹配 'c3po'
>>> if m is not None: m.group()
...
'c3po'
>>> m = re.match('[cr][23][dp][o2]', 'c2do')# 匹配 'c2do'
>>> if m is not None: m.group()
...
'c2do'
>>> m = re.match('r2d2|c3po', 'c2do')# 不匹配 'c2do'
>>> if m is not None: m.group()
...
>>> m = re.match('r2d2|c3po', 'r2d2')# 匹配 'r2d2'
>>> if m is not None: m.group()
...
'r2d2'

1.3.9 重复、特殊字符以及分组

正则表达式中最常见的情况包括特殊字符的使用、正则表达式模式的重复出现,以及使用圆括号对匹配模式的各部分进行分组和提取操作。我们曾看到过一个关于简单电子邮件地址的正则表达式(w+@w+.com)。或许我们想要匹配比这个正则表达式所允许的更多邮件地址。为了在域名前添加主机名称支持,例如www.xxx.com,仅仅允许xxx.com作为整个域名,必须修改现有的正则表达式。为了表示主机名是可选的,需要创建一个模式来匹配主机名(后面跟着一个句点),使用“?”操作符来表示该模式出现零次或者一次,然后按照如下所示的方式,插入可选的正则表达式到之前的正则表达式中:w+@(w+.)?w+.com。从下面的示例中可见,该表达式允许.com前面有一个或者两个名称:

>>> patt = '\w+@(\w+\.)?\w+\.com'
>>> re.match(patt, 'nobody@xxx.com').group()
'nobody@xxx.com'
>>> re.match(patt, 'nobody@www.xxx.com').group()
'nobody@www.xxx.com'

接下来,用以下模式来进一步扩展该示例,允许任意数量的中间子域名存在。请特别注意细节的变化,将“?”改为“. : w+@(w+.)w+.com”。

>>> patt = '\w+@(\w+\.)*\w+\.com'
>>> re.match(patt, 'nobody@www.xxx.yyy.zzz.com').group()
'nobody@www.xxx.yyy.zzz.com'

但是,我们必须要添加一个“免责声明”,即仅仅使用字母数字字符并不能匹配组成电子邮件地址的全部可能字符。上述正则表达式不能匹配诸如xxx-yyy.com的域名或者使用非单词W字符组成的域名。

之前讨论过使用圆括号来匹配和保存子组,以便于后续处理,而不是确定一个正则表达式匹配之后,在一个单独的子程序里面手动编码来解析字符串。此前还特别讨论过一个简单的正则表达式模式w+-d+,它由连字符号分隔的字母数字字符串和数字组成,还讨论了如何添加一个子组来构造一个新的正则表达式 (w+)-(d+)来完成这项工作。下面是初始版本的正则表达式的执行情况。

>>> m = re.match('\w\w\w-\d\d\d', 'abc-123')
>>> if m is not None: m.group()
...
'abc-123'>>> m = re.match('\w\w\w-\d\d\d', 'abc-xyz')
>>> if m is not None: m.group()
...
>>>

在上面的代码中,创建了一个正则表达式来识别包含3个字母数字字符且后面跟着3个数字的字符串。使用abc-123测试该正则表达式,将得到正确的结果,但是使用abc-xyz则不能。现在,将修改之前讨论过的正则表达式,使该正则表达式能够提取字母数字字符串和数字。如下所示,请注意如何使用group()方法访问每个独立的子组以及groups()方法以获取一个包含所有匹配子组的元组。

>>> m = re.match('(\w\w\w)-(\d\d\d)', 'abc-123')
>>> m.group()            # 完整匹配
'abc-123'
>>> m.group(1)           # 子组 1
'abc'
>>> m.group(2)           # 子组 2
'123'
>>> m.groups()           # 全部子组
('abc', '123')

由以上脚本内容可见,group()通常用于以普通方式显示所有的匹配部分,但也能用于获取各个匹配的子组。可以使用groups()方法来获取一个包含所有匹配子字符串的元组。

如下为一个简单的示例,该示例展示了不同的分组排列,这将使整个事情变得更加清晰。

>>> m = re.match('ab', 'ab')    # 没有子组
>>> m.group()            # 完整匹配
'ab'
>>> m.groups()           # 所有子组
()
>>>
>>> m = re.match('(ab)', 'ab')   # 一个子组
>>> m.group()            # 完整匹配
'ab'
>>> m.group(1)           # 子组 1
'ab'
>>> m.groups()           # 全部子组
('ab',)
>>>
>>> m = re.match('(a)(b)', 'ab')  # 两个子组
>>> m.group()            # 完整匹配
'ab'
>>> m.group(1)           # 子组 1
'a'
>>> m.group(2)           # 子组 2
'b'
>>> m.groups()           # 所有子组
('a', 'b')
>>>
>>> m = re.match('(a(b))', 'ab')  # 两个子组
>>> m.group()            # 完整匹配
'ab'
>>> m.group(1)           # 子组 1
'ab'
>>> m.group(2)           # 子组 2
'b'
>>> m.groups()           # 所有子组
('ab', 'b')

1.3.10 匹配字符串的起始和结尾以及单词边界

如下示例突出显示表示位置的正则表达式操作符。该操作符更多用于表示搜索而不是匹配,因为match()总是从字符串开始位置进行匹配。

>>> m = re.search('^The', 'The end.')    # 匹配
>>> if m is not None: m.group()
...
'The'
>>> m = re.search('^The', 'end. The')    # 不作为起始
>>> if m is not None: m.group()
...
>>> m = re.search(r'\bthe', 'bite the dog') # 在边界
>>> if m is not None: m.group()
...
'the'
>>> m = re.search(r'\bthe', 'bitethe dog') # 有边界
>>> if m is not None: m.group()
...
>>> m = re.search(r'\Bthe', 'bitethe dog') # 没有边界
>>> if m is not None: m.group()
...
'the'

读者将注意到此处出现的原始字符串。你可能想要查看本章末尾部分的核心提示“Python中原始字符串的用法”(Using Python raw strings),里面提到了在此处使用它们的原因。通常情况下,在正则表达式中使用原始字符串是个好主意。

读者还应当注意其他4个re模块函数和正则表达式对象方法:findall()、sub()、subn()和split()。

1.3.11 使用findall()和finditer()查找每一次出现的位置

findall()查询字符串中某个正则表达式模式全部的非重复出现情况。这与search()在执行字符串搜索时类似,但与match()和search()的不同之处在于,findall()总是返回一个列表。如果findall()没有找到匹配的部分,就返回一个空列表,但如果匹配成功,列表将包含所有成功的匹配部分(从左向右按出现顺序排列)。

>>> re.findall('car', 'car')
['car']
>>> re.findall('car', 'scary')
['car']
>>> re.findall('car', 'carry the barcardi to the car')
['car', 'car', 'car']

子组在一个更复杂的返回列表中搜索结果,而且这样做是有意义的,因为子组是允许从单个正则表达式中抽取特定模式的一种机制,例如匹配一个完整电话号码中的一部分(例如区号),或者完整电子邮件地址的一部分(例如登录名称)。

对于一个成功的匹配,每个子组匹配是由findall()返回的结果列表中的单一元素;对于多个成功的匹配,每个子组匹配是返回的一个元组中的单一元素,而且每个元组(每个元组都对应一个成功的匹配)是结果列表中的元素。这部分内容可能第一次听起来令人迷惑,但是如果你尝试练习过一些不同的示例,就将澄清很多知识点。


finditer()函数是在Python 2.2版本中添加回来的,这是一个与findall()函数类似但是更节省内存的变体。两者之间以及和其他变体函数之间的差异(很明显不同于返回的是一个迭代器还是列表)在于,和返回的匹配字符串相比,finditer()在匹配对象中迭代。如下是在单个字符串中两个不同分组之间的差别。

>>> s = 'This and that.'
>>> re.findall(r'(th\w+) and (th\w+)', s, re.I)
[('This', 'that')]
>>> re.finditer(r'(th\w+) and (th\w+)', s,
...  re.I).next().groups()
('This', 'that')
>>> re.finditer(r'(th\w+) and (th\w+)', s,
...  re.I).next().group(1)
'This'
>>> re.finditer(r'(th\w+) and (th\w+)', s,
...  re.I).next().group(2)
'that'
>>> [g.groups() for g in re.finditer(r'(th\w+) and (th\w+)',
...  s, re.I)]
[('This', 'that')]

在下面的示例中,我们将在单个字符串中执行单个分组的多重匹配。

>>> re.findall(r'(th\w+)', s, re.I)
['This', 'that']
>>> it = re.finditer(r'(th\w+)', s, re.I)
>>> g = it.next()
>>> g.groups()
('This',)
>>> g.group(1)
'This'
>>> g = it.next()
>>> g.groups()
('that',)
>>> g.group(1)
'that'
>>> [g.group(1) for g in re.finditer(r'(th\w+)', s, re.I)]
['This', 'that']

注意,使用finditer()函数完成的所有额外工作都旨在获取它的输出来匹配findall()的输出。

最后,与match()和search()类似,findall()和finditer()方法的版本支持可选的pos和endpos参数,这两个参数用于控制目标字符串的搜索边界,这与本章之前的部分所描述的类似。

1.3.12 使用sub()和subn()搜索与替换

有两个函数/方法用于实现搜索和替换功能:sub()和subn()。两者几乎一样,都是将某字符串中所有匹配正则表达式的部分进行某种形式的替换。用来替换的部分通常是一个字符串,但它也可能是一个函数,该函数返回一个用来替换的字符串。subn()和sub()一样,但subn()还返回一个表示替换的总数,替换后的字符串和表示替换总数的数字一起作为一个拥有两个元素的元组返回。

>>> re.sub('X', 'Mr. Smith', 'attn: X\n\nDear X,\n')
'attn: Mr. Smith\012\012Dear Mr. Smith,\012'
>>>
>>> re.subn('X', 'Mr. Smith', 'attn: X\n\nDear X,\n')
('attn: Mr. Smith\012\012Dear Mr. Smith,\012', 2)
>>>
>>> print re.sub('X', 'Mr. Smith', 'attn: X\n\nDear X,\n')
attn: Mr. SmithDear Mr. Smith,>>> re.sub('[ae]', 'X', 'abcdef')
'XbcdXf'
>>> re.subn('[ae]', 'X', 'abcdef')
('XbcdXf', 2)

前面讲到,使用匹配对象的group()方法除了能够取出匹配分组编号外,还可以使用N,其中N是在替换字符串中使用的分组编号。下面的代码仅仅只是将美式的日期表示法MM/DD/YY{,YY}格式转换为其他国家常用的格式DD/MM/YY{,YY}。

>>> re.sub(r'(\d{1,2})/(\d{1,2})/(\d{2}|\d{4})',
...   r'\2/\1/\3', '2/20/91') # Yes, Python is...
'20/2/91'
>>> re.sub(r'(\d{1,2})/(\d{1,2})/(\d{2}|\d{4})',
...   r'\2/\1/\3', '2/20/1991') # ... 20+ years old!
'20/2/1991'

1.3.13 在限定模式上使用split()分隔字符串

re模块和正则表达式的对象方法split()对于相对应字符串的工作方式是类似的,但是与分割一个固定字符串相比,它们基于正则表达式的模式分隔字符串,为字符串分隔功能添加一些额外的威力。如果你不想为每次模式的出现都分割字符串,就可以通过为max参数设定一个值(非零)来指定最大分割数。

如果给定分隔符不是使用特殊符号来匹配多重模式的正则表达式,那么re.split()与str.split()的工作方式相同,如下所示(基于单引号分割)。

>>> re.split(':', 'str1:str2:str3')
['str1', 'str2', 'str3']

这是一个简单的示例。如果有一个更复杂的示例,例如,一个用于Web站点(类似于Google或者Yahoo! Maps)的简单解析器,该如何实现?用户需要输入城市和州名,或者城市名加上ZIP编码,还是三者同时输入?这就需要比仅仅是普通字符串分割更强大的处理方式,具体如下。

>>> import re
>>> DATA = (
...   'Mountain View, CA 94040',
...   'Sunnyvale, CA',
...   'Los Altos, 94023',
...   'Cupertino 95014',
...   'Palo Alto CA',
... )
>>> for datum in DATA:
...   print re.split(', |(?= (?:\d{5}|[A-Z]{2})) ', datum)
...
['Mountain View', 'CA', '94040']
['Sunnyvale', 'CA']
['Los Altos', '94023']
['Cupertino', '95014']
['Palo Alto', 'CA']

上述正则表达式拥有一个简单的组件:使用split语句基于逗号分割字符串。更难的部分是最后的正则表达式,可以通过该正则表达式预览一些将在下一小节中介绍的扩展符号。在普通的英文中,通常这样说:如果空格紧跟在五个数字(ZIP编码)或者两个大写字母(美国联邦州缩写)之后,就用split语句分割该空格。这就允许我们在城市名中放置空格。

通常情况下,这仅仅只是一个简单的正则表达式,可以在用来解析位置信息的应用中作为起点。该正则表达式并不能处理小写的州名或者州名的全拼、街道地址、州编码、ZIP+4(9位ZIP编码)、经纬度、多个空格等内容(或者在处理时会失败)。这仅仅意味着使用re.split()能够实现str.split()不能实现的一个简单的演示实例。

我们刚刚已经证实,读者将从正则表达式split语句的强大能力中获益;然而,记得一定在编码过程中选择更合适的工具。如果对字符串使用split方法已经足够好,就不需要引入额外复杂并且影响性能的正则表达式。

1.3.14 扩展符号

Python的正则表达式支持大量的扩展符号。让我们一起查看它们中的一些内容,然后展示一些有用的示例。

通过使用 (?iLmsux) 系列选项,用户可以直接在正则表达式里面指定一个或者多个标记,而不是通过compile()或者其他re模块函数。下面为一些使用re.I/IGNORECASE的示例,最后一个示例在re.M/MULTILINE实现多行混合:

>>> re.findall(r'(?i)yes', 'yes? Yes. YES!!')
['yes', 'Yes', 'YES']
>>> re.findall(r'(?i)th\w+', 'The quickest way is through this
tunnel.')
['The', 'through', 'this']
>>> re.findall(r'(?im)(^th[\w ]+)', """
... This line is the first,
... another line,
... that line, it's the best
... """)
['This line is the first', 'that line']

在前两个示例中,显然是不区分大小写的。在最后一个示例中,通过使用“多行”,能够在目标字符串中实现跨行搜索,而不必将整个字符串视为单个实体。注意,此时忽略了实例“the”,因为它们并不出现在各自的行首。

下一组演示使用re.S/DOTALL。该标记表明点号(.)能够用来表示n符号(反之其通常用于表示除了n之外的全部字符):

>>> re.findall(r'th.+', '''
... The first line
... the second line
... the third line
... ''')
['the second line', 'the third line']
>>> re.findall(r'(?s)th.+', '''
... The first line
... the second line
... the third line
... ''')
['the second line\nthe third line\n']

re.X/VERBOSE标记非常有趣;该标记允许用户通过抑制在正则表达式中使用空白符(除了在字符类中或者在反斜线转义中)来创建更易读的正则表达式。此外,散列、注释和井号也可以用于一个注释的起始,只要它们不在一个用反斜线转义的字符类中。

>>> re.search(r'''(?x)
...   \((\d{3})\) # 区号
...   [ ]     # 空白符
...   (\d{3})   # 前缀
...   -      # 横线
...   (\d{4})   # 终点数字
... ''', '(800) 555-1212').groups()
('800', '555', '1212')

(?:…)符号将更流行;通过使用该符号,可以对部分正则表达式进行分组,但是并不会保存该分组用于后续的检索或者应用。当不想保存今后永远不会使用的多余匹配时,这个符号就非常有用。

>>> re.findall(r'http://(?:\w+\.)*(\w+\.com)',...   'http://google.com http://www.google.com http://code.google.com')['google.com', 'google.com', 'google.com']>>> re.search(r'\((?P<areacode>\d{3})\) (?P<prefix>\d{3})-(?:\d{4})',...   '(800) 555-1212').groupdict()
{'areacode': '800', 'prefix': '555'}

读者可以同时一起使用 (?P) 和 (?P=name)符号。前者通过使用一个名称标识符而不是使用从1开始增加到N的增量数字来保存匹配,如果使用数字来保存匹配结果,我们就可以通过使用1,2 ...,N 来检索。如下所示,可以使用一个类似风格的g来检索它们。

>>> re.sub(r'\((?P<areacode>\d{3})\) (?P<prefix>\d{3})-(?:\d{4})',...   '(\g<areacode>) \g<prefix>-xxxx', '(800) 555-1212')
'(800) 555-xxxx'

使用后者,可以在一个相同的正则表达式中重用模式,而不必稍后再次在(相同)正则表达式中指定相同的模式。例如,在本示例中,假定让读者验证一些电话号码的规范化。如下所示为一个丑陋并且压缩的版本,后面跟着一个正确使用的 (?x),使代码变得稍许易读。

>>> bool(re.match(r'\((?P<areacode>\d{3})\) (?P<prefix>\d{3})-
(?P<number>\d{4}) (?P=areacode)-(?P=prefix)-(?P=number)
1(?P=areacode)(?P=prefix)(?P=number)',
...   '(800) 555-1212 800-555-1212 18005551212'))
True
>>> bool(re.match(r'''(?x)
...
...   # match (800) 555-1212, save areacode, prefix, no.
...   \((?P<areacode>\d{3})\)[ ](?P<prefix>\d{3})-(?P<number>\d{4})
...
...   # space
...   [ ]
...
...   # match 800-555-1212
...   (?P=areacode)-(?P=prefix)-(?P=number)
...
...   # space
...   [ ]
...
...   # match 18005551212
...   1(?P=areacode)(?P=prefix)(?P=number)
...
... ''', '(800) 555-1212 800-555-1212 18005551212'))
True

读者可以使用 (?=...) 和 (?!…)符号在目标字符串中实现一个前视匹配,而不必实际上使用这些字符串。前者是正向前视断言,后者是负向前视断言。在后面的示例中,我们仅仅对姓氏为“van Rossum”的人的名字感兴趣,下一个示例中,让我们忽略以“noreply”或者“postmaster”开头的e-mail地址。

第三个代码片段用于演示findall()和finditer()的区别;我们使用后者来构建一个使用相同登录名但不同域名的e-mail地址列表(在一个更易于记忆的方法中,通过忽略创建用完即丢弃的中间列表)。

>>> re.findall(r'\w+(?= van Rossum)',
... '''
...   Guido van Rossum
...   Tim Peters
...   Alex Martelli
...   Just van Rossum
...   Raymond Hettinger
... ''')
['Guido', 'Just']
>>> re.findall(r'(?m)^\s+(?!noreply|postmaster)(\w+)',
... '''
...   sales@phptr.com
...   postmaster@phptr.com
...   eng@phptr.com
...   noreply@phptr.com
...   admin@phptr.com
... ''')
['sales', 'eng', 'admin']
>>> ['%s@aw.com' % e.group(1) for e in \
re.finditer(r'(?m)^\s+(?!noreply|postmaster)(\w+)',
... '''
...   sales@phptr.com
...   postmaster@phptr.com
...   eng@phptr.com
...   noreply@phptr.com
...   admin@phptr.com
... ''')]
['sales@aw.com', 'eng@aw.com', 'admin@aw.com']

最后一个示例展示了使用条件正则表达式匹配。假定我们拥有另一个特殊字符,它仅仅包含字母“x”和“y”,我们此时仅仅想要这样限定字符串:两字母的字符串必须由一个字母跟着另一个字母。换句话说,你不能同时拥有两个相同的字母;要么由“x”跟着“y”,要么相反。

>>> bool(re.search(r'(?:(x)|y)(?(1)y|x)', 'xy'))
True
>>> bool(re.search(r'(?:(x)|y)(?(1)y|x)', 'xx'))
False

1.3.15 杂项

可能读者会对于正则表达式的特殊字符和特殊ASCII符号之间的差异感到迷惑。我们可以使用n表示一个换行符,但是我们可以使用d在正则表达式中表示匹配单个数字。

如果有符号同时用于ASCII和正则表达式,就会发生问题,因此在下面的核心提示中,建议使用Python的原始字符串来避免产生问题。另一个警告是:w和W字母数字字符集同时受re.L/LOCALE和Unicode(re.U/UNICODE)标记所影响。

核心提示:使用Python原始字符串读者可能在之前的一些示例中见过原始字符串的使用。正则表达式对于探索原始字符串有着强大的动力,原因就在于ASCII字符和正则表达式的特殊字符之间存在冲突。作为一个特殊符号,\b表示ASCII字符的退格符,但是\b同时也是一个正则表达式的特殊符号,表示匹配一个单词的边界。对于正则表达式编译器而言,若它把两个\b视为字符串内容而不是单个退格符,就需要在字符串中再使用一个反斜线转义反斜线,就像这样:\\b。这样显得略微杂乱,特别是如果在字符串中拥有很多特殊字符,就会让人感到更加困惑。我们在Core Python Programming或者Core Python Language Fundamentals的Sequence章节中介绍了原始字符串,而且该原始字符串可以用于(且经常用于)帮助保持正则表达式查找某些可托管的东西。事实上,很多Python程序员总是抱怨这个方法,仅仅用原始字符串来定义正则表达式。如下所示的一些示例用于说明退格符\b和正则表达式\b之间的差异,它们有的使用、有的不使用原始字符串。>>> m = re.match('\bblow', 'blow') # backspace、no match
>> if m: m.group()
...
>> m = re.match('\\bblow', 'blow') # escaped\,now it works
>> if m: m.group()
...
'blow'
>> m = re.match(r'\bblow', 'blow') # use raw string instead
>> if m: m.group()
...
'blow'读者可能回想起来我们在正则表达式中使用\d而没有使用原始字符串时并未遇到问题,这是因为ASCII中没有相应的特殊字符,所以正则表达式的编译器知道你想要表示十进制数字。

《Python核心编程(第3版)》——1.3 正则表达式和Python语言相关推荐

  1. python核心编程第三版_Python之父:自学python,这3本书能节约你一大半时间编程...

    今天给大家推荐三本书,有两本是属于一个系列,即<Python核心编程>第二版和第三版,一本讲基础和一本讲进阶,非常适合Python的初学者和有一定基础的学习者.还有一本书适合所有想学Pyt ...

  2. 《Python核心编程》第二版第36页第二章练习 -Python核心编程答案-自己做的-

    <Python核心编程>第二版第36页第二章练习 这里列出的答案不是来自官方资源,是我自己做的练习,可能有误. 2.21 练习 2-1. 变量,print和字符串格式化操作符.启动交互式解 ...

  3. 《Python核心编程(第二版)》——1.9 练习

    本节书摘来自异步社区<Python核心编程(第二版)>一书中的第1章,第1.9节,作者[美]Wesley J. Chun,宋吉广 译,更多章节内容可以访问云栖社区"异步社区&qu ...

  4. 《Python核心编程》第二版第18页第一章练习 -Python核心编程答案-自己做的-

    <Python核心编程>第二版第18页第一章练习 这里列出的答案不是来自官方资源,是我自己做的练习,可能有误. 1.9 练习 1-1. 安装Python.请检查Python是否已经安装到你 ...

  5. python核心编程第三版_《Python核心编程(第3版)》

    <Python核心编程(第3版)>是经典畅销图书<Python核心编程(第二版)>的全新升级版本,本书适合具有一定经验的Python开发人员阅读,总共分为3部分.第1部分为讲解 ...

  6. python核心编程 第3版_Python核心编程(第3版)PDF高清晰完整中文版

    一.书籍简介 <Python核心编程(第3版)>是经典畅销图书<Python核心编程(第二版)>的全新升级版本. <Python核心编程(第3版)>总共分为3部分. ...

  7. 面试分析《疯狂Python讲义》PDF代码+《Python核心编程第3版》PDF代码问题

    python语言现在很流行了,除了用在学校,也用在很多行业.python学起来较为简单,语法容易理解,也可用于数据分析. 国内的教材推荐看<疯狂python讲义>,对比国外也有很多好的参考 ...

  8. 《Python核心编程》第二版学习总纲

    <CorePythonProgramming> <Python核心编程> 作者:Wesley J.Chun 开始学习,全书答案目录于此博文汇集总纲记录. -- NORMAL - ...

  9. python核心编程第3版第1章 正则表达式【读书笔记】

    什么是正则表达式 正则表达式为高级的本文模式匹配.抽取.与/或文本形式的搜索和替换功能提供了基础.简单地说,正则表达式(简称为regex)是一些由字符和特殊符号组成的字符串. re模块 python通 ...

  10. Python 核心编程(第二版)——网络编程

    1. 介绍 服务器是一个软件或硬件,用于提供客户需要的"服务".服务器存在的唯一目的就是等待客户的请求,给这些客户服务,然后再等待其它的请求. 2. 套接字:通讯端点 套接字是一种 ...

最新文章

  1. Spring框架你敢写精通,面试官就敢问@Autowired注解的实现原理
  2. linux如何根据端口看进程,linux 根据端口查看系统进程
  3. 构建乘积数组(剑指offer)
  4. 从零开始学PowerShell(10)PowerShell中的子表达式
  5. 树堆(Treap)图文详解与实现
  6. 引用计数和AddRef、Release
  7. codeforces776D
  8. 【定位问题】基于matlab RSSI和模拟退火优化粒子群算法求解无线传感器网络定位问题【含Matlab源码 1766期】
  9. 在lua中调用DLL
  10. c语言的退出程序代码,C语言实现关机小程序
  11. 阿里矢量图标库的使用方法
  12. 实战项目 仿写小米商城 网页框架
  13. PLC、传感器 源型漏型、NPN与PNP之间的关系
  14. java 传真x2fax_FreeSWITCH的传真发送
  15. WOL(Wake-On-LAN)网络唤醒介绍
  16. 身高预测c语言代码switch,C++ ,预测身高程序
  17. python创建类的两个对象_Python为一个类创建多个对象
  18. Camera ITS当中的gain/shutter组合测试
  19. 如何关闭微软news
  20. 宇视摄像机接入TSINGSEE青犀视频平台,如何基于SDK获取设备的组织和摄像机?

热门文章

  1. MySQL学习笔记——显示数据库信息
  2. Golang——递归的使用
  3. 十道解分式方程及答案_100道八年级分式方程及答案.
  4. 约翰诺曼超级计算机研究中心,第433章 拉泽尔松教授的决定_学霸的黑科技系统_晨星LL作品_du00...
  5. java实现手机开关机_Android 系统重启与关机:Java 代码实现
  6. mysql如何管理空间_管理空间的mysql数据库
  7. linux中按行读取文件,Linux按行读取文件内容
  8. json在线解析工具大集合
  9. 计算机二级access什么时候报名_全国计算机等级考试什么时候报名
  10. 数据转移-从MySQL到Hive