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

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

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

1.3 正则表达式和Python语言

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


c25fa0a80c14b3f6b7f2f9b7d02011587557ea7f

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

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

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

screenshot
screenshot
screenshot

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


79d46dc42060ab48cd79ff696ebcdc5985185cc5

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. Smith

Dear 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中没有相应的特殊字符,所以正则表达式的编译器知道你想要表示十进制数字。
相关实践学习
基于函数计算快速搭建Hexo博客系统
本场景介绍如何使用阿里云函数计算服务命令行工具快速搭建一个Hexo博客。
相关文章
|
9天前
|
安全 Java 数据处理
Python网络编程基础(Socket编程)多线程/多进程服务器编程
【4月更文挑战第11天】在网络编程中,随着客户端数量的增加,服务器的处理能力成为了一个重要的考量因素。为了处理多个客户端的并发请求,我们通常需要采用多线程或多进程的方式。在本章中,我们将探讨多线程/多进程服务器编程的概念,并通过一个多线程服务器的示例来演示其实现。
|
9天前
|
程序员 开发者 Python
Python网络编程基础(Socket编程) 错误处理和异常处理的最佳实践
【4月更文挑战第11天】在网络编程中,错误处理和异常管理不仅是为了程序的健壮性,也是为了提供清晰的用户反馈以及优雅的故障恢复。在前面的章节中,我们讨论了如何使用`try-except`语句来处理网络错误。现在,我们将深入探讨错误处理和异常处理的最佳实践。
|
12天前
|
缓存 监控 Python
解密Python中的装饰器:优雅而强大的编程利器
Python中的装饰器是一种强大而又优雅的编程工具,它能够在不改变原有代码结构的情况下,为函数或类添加新的功能和行为。本文将深入解析Python装饰器的原理、用法和实际应用,帮助读者更好地理解和利用这一技术,提升代码的可维护性和可扩展性。
|
29天前
|
编译器 测试技术 C++
【Python 基础教程 01 全面介绍】 Python编程基础全攻略:一文掌握Python语法精髓,从C/C++ 角度学习Python的差异
【Python 基础教程 01 全面介绍】 Python编程基础全攻略:一文掌握Python语法精髓,从C/C++ 角度学习Python的差异
159 0
|
1天前
|
前端开发 Java Go
开发语言详解(python、java、Go(Golong)。。。。)
开发语言详解(python、java、Go(Golong)。。。。)
|
1天前
|
安全 数据处理 开发者
《Python 简易速速上手小册》第7章:高级 Python 编程(2024 最新版)
《Python 简易速速上手小册》第7章:高级 Python 编程(2024 最新版)
13 1
|
1天前
|
人工智能 数据挖掘 程序员
《Python 简易速速上手小册》第1章:Python 编程入门(2024 最新版)
《Python 简易速速上手小册》第1章:Python 编程入门(2024 最新版)
27 0
|
2天前
|
API Python
Python模块化编程:面试题深度解析
【4月更文挑战第14天】了解Python模块化编程对于构建大型项目至关重要,它涉及代码组织、复用和维护。本文深入探讨了模块、包、导入机制、命名空间和作用域等基础概念,并列举了面试中常见的模块导入混乱、不适当星号导入等问题,强调了避免循环依赖、合理使用`__init__.py`以及理解模块作用域的重要性。掌握这些知识将有助于在面试中自信应对模块化编程的相关挑战。
17 0
|
3天前
|
Python
Python金融应用编程:衍生品定价和套期保值的随机过程
Python金融应用编程:衍生品定价和套期保值的随机过程
|
3天前
|
Python
python面型对象编程进阶(继承、多态、私有化、异常捕获、类属性和类方法)(上)
python面型对象编程进阶(继承、多态、私有化、异常捕获、类属性和类方法)(上)
39 0

热门文章

最新文章