这本书虽然叫编程快速上手,但实际上讲的是如何利用Python对日常工作进行自动化处理。所以前面一些比较基础的东西我就先略过了,直接讲书中自动化的部分。

第一部分就是标题所说的模式匹配与正则表达式。

7.1 不用正则表达式来查找文本模式

假设你希望在字符串中查找电话号码。你知道模式:3个数字+一个短横线+3个数字+一个短横线+4个数字,如415-555-4242。

假设我们用一个名为isPhoneNumber()的函数来检查字符串是否匹配模式,它返回True或False。打开一个新的文件编辑器窗口,输入以下代码,然后保存为isPhoneNumber:

def isPhoneNumber(text):
    if len(text) !=12:
        return False
    for i in range(0,3):
        if not text[i].isdecimal():
            return False
    if text[3] != '-':
        return False
    for i in range(4,7):
        if not text[i].isdecimal():
            return False
    if text[7] != '-':
        return False
    for i in range(8,12):
        if not text[i].isdecimal():
            return False
    return True

print('415-5555-4242 is a phone number:')
print(isPhoneNumber('415-555-4242'))
print('wangdacuhi is a phone number:')
print(isPhoneNumber('wangdachui'))

=======================================
415-5555-4242 is a phone number:
True
wangdacuhi is a phone number:
False

isdecimal():检查字符串是否只包含十进制字符。

如果想在更长的字符串中寻找电话号码,就必须添加更多代码来寻找电话号码模式。用下面的代码替代isPhoneNumber.py中的最后4个print()函数调用:
message= '拜托别问我微信了!我的电话是415-555-1011,办公室电话是415-555-9999,再问我微信砍你全家!'
for i in range(len(message)):
    chunk = message[i:i+12]
    if isPhoneNumber(chunk):
        print('Phone number found:'+chunk)
print('Done')

在for循环的每一次迭代中,取自message的一段新的12个字符被赋给变量chunk。例如,在第一次迭代式,i是0,chunk被赋值message[0:12](即字符串‘拜托别问我微信了!我的电’)。在下一次迭代式,i是‘话’,chunk被赋值为message[1:13](即字符串‘话是415-555-10’)。

将chunk传递给isPhoneNumber(),看看它是否符合电话号码的模式。如果和服,就输出这段文本。

继续遍历message,最终chunk中的12个字符会是一个电话号码。该循环遍历了整个字符串,测试了每一段12个字符,输出所有满足isPhoneNumber的chunk。当我们遍历完message,就输出Done。

虽然在这个例子中的message中的字符串很短,但它也可能包含上百万个字符,程序运行仍然不需要1秒。使用正则表达式编写查找电话号码的类似程序,运行也不会超过1秒,但用正则表达式编写这类程序会快很多。
7.2 用正则表达式查找文本模式
前面的电话号码查找程序能工作,但它使用了很多代码,做的事情会很有限:用了17行代码,却无法查找类似于415.555.4242这样的电话号码。你可以增加更多的代码来处理额外的情况,但还有更简单的方法。

正则表达式简称为“regex”,是文本模式描述方式。例如\d是一个正则表达式,表示钱一个数字字符,即任何一位0-9的数字。Python使用正则表达式\d\d\d-\d\d\d-\d\d\d\d来匹配前面isPhoneNumber()函数匹配的同样文本:3个数字、一个短横线、3个数字、一个短横线、4个数字。所有其他字符串都不能匹配\d\d\d-\d\d\d-\d\d\d\d正则表达式。

但正则表达式可以复杂的多,在一个模式后加上花括号包围的3({3}),表示“匹配这个模式3次”。所以较短的正则表达式\d{3}-\d{3}-\d{4}也匹配正确的电话号码格式。

7.2.1 创建正则表达式对象
Python中所有正则表达式的函数都在re模块中。在交互环境中输入以下代码,导入该模块:
import re

向re.compile()传入一个字符串值,表示正则表达式,它将返回一个Regex模式对象(或者就简称为“Regex对象”)。

要创建一个Regex对象来匹配电话号码模式,就在交互式环境中输入以下代码。

In [4]: import re

In [5]: phone = re.compile(r'\d{3}-\d{3}-\d{4}')

现在phone变量包含了一个Regex对象。
7.2.2 匹配Regex对象

Regex对象的search()方法查找传入的字符串,以寻找该正则表达式的所有匹配。如果字符串中没有找到该正则表达式模式,那么search方法将返回None。如果找到了该模式,search()方法将返回一个Match对象。Match对象有一个group()方法,它返回被查找字符串中实际匹配的文本。例如,在交互式环境中输入以下代码:
In [5]: phone = re.compile(r'\d{3}-\d{3}-\d{4}')

In [6]: mo = phone.search('my phone is 415-555-9555.')

In [7]: print('phone numeber found:'+mo.group())
phone numeber found:415-555-9555

变量名mo是一个通用的名称,用于Match对象。这里,我们将期待的模式传递给了re.compile(),并将得到的Regex对象保存在phone中。然后我们在phone上调用search(),向它传入想查找的字符串。查找的结果保存在变量mo中。在这个例子里,我们知道模式会在这个字符串中找到,因此我们知道会返回一个Match对象。知道mo包含一个Match对象,而不是空值None,我们就可以在MO变量上调用group(),返回匹配的结果。将mo.group()写在输出语句中以显示出完整的匹配,即415-555-4242。
7.2.3 正则表达式匹配复习
在Python中使用正则表达式有以下几个步骤,每一步都相当简单。

1、用import re导入正则表达式模块。
2、用re.compile()函数创建一个Regex对象(记得使用原始字符串)。
3、向Regex 对象的search()方法传入想查找的字符串,它返回一个Match对象。
4、调用Match对象的Group()方法,返回实际匹配文本的字符串。 

7.3 用正则表达式匹配更多模式

7.3.1 利用括号分组

假定想要将区号从电话号码中分离,添加括号将在正则表达式中创建“分组”:(\d\d\d)-(\d\d\d-\d\d\d\d)。然后可以使用group()匹配对象方法,从一个分组中获取匹配的文本。

正则表达式字符串中的第一对括号是第1组,第二队括号是第2组。向group()匹配对象方法传入整数1或2,就可以取得匹配文本的不同部分。向group()方法传入0或不传入参数,将返回整个匹配的文本。

In [13]: phone = re.compile(r'(\d{3})-(\d{3}-\d{4})')

In [14]: mo = phone.search('my phone is 415-555-9555.')

In [15]: mo.group(0)
Out[15]: '415-555-9555'

In [16]: mo.group(1)
Out[16]: '415'

In [17]: mo.group(2)
Out[17]: '555-9555'

想要一次就获得所有的分组,请使用groups()方法,注意函数名的复数形式:
In [18]: mo.groups()
Out[18]: ('415', '555-9555')

In [19]: wang,dachui = mo.groups()

In [20]: wang
Out[20]: '415'

In [21]: dachui
Out[21]: '555-9555'

因为mo.groups()返回多个值的元组,所以你可以使用多重复制的技巧,每个值都赋给一个独立的变量,就像前面的代码行:wang,dachui = mo.groups()。

括号在正则表达式中有特殊的含义,如果你需要在文本中匹配括号,该怎么办呢?例如,你要匹配的电话号码可能将区号放在一对括号中。在这种情况下,就需要用倒斜杠对括号进行字符转义。在交互式环境中输入以下代码:
In [10]: phone = re.compile(r'(\(\d{3}\)) (\d{3}-\d{4})')

In [11]: import re

In [12]: phone = re.compile(r'(\(\d{3}\)) (\d{3}-\d{4})')

In [13]: mo = phone.search('my phone is (415) 555-4242')

In [14]: mo.groups()
Out[14]: ('(415)', '555-4242')

In [17]: mo.group(1)
Out[17]: '(415)'

In [18]: mo.group(2)
Out[18]: '555-4242'

这里需要注意两点:

1、(\(\d{3}\)),这部分的反斜杠转义是有两个的,一个在(\d 前,一个在3}后。因为不在一个括号里,再加上正则表达式一堆反斜杠,很容易漏,一旦漏了就会报错:

error: unbalanced parenthesis at position 9(9是指位置)

2、在输出的时候,是group(),不是groups()。需要注意复数形式,一旦输入错误,就会把所有的元组都输出出来:

In [15]: mo.groups(1)
Out[15]: (‘(415)’, ‘555-4242’)

在传递给re.compile()的原始字符串中,\(和\)转义字符将匹配实际的括号字符。在正则表达式中,以下字符具有特殊含义:

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

如果要检测包含这些字符的文本模式,那么就需要用倒斜杠对它进行转义:

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

要确保在正则表达式中,没有将转义的括号\( 和 \)误作为括号(和)。如果你收到类似"missing)"或“unbalanced parenthesis” 的错误信息,则可能是忘记了为分组添加转义的右括号。
7.3.2 用管道匹配多个分组
字符|称为“管道”,希望匹配许多表达式中的一个时,就可以使用它。例如,正则表达式r'Batman|Tina'将匹配‘Batman’或‘Tina’。

如果batman和tina都出现在被查找的字符串中,那么第一次出现的匹配文本将作为match对象返回。在交互式环境中输入以下代码:

In [19]: hero = re.compile(r'Batman|Tina')

In [20]: mo1 = hero.search('Batman or Tina')

In [23]: mo1.group()
Out[23]: 'Batman'

In [24]: mo1 = hero.search('Tina or Batman')

In [25]: mo1.group()
Out[25]: 'Tina'

也可以使用管道来匹配多个模式中的一个,这些模式可作为正则表达式的一部分。例如,假设你希望匹配‘Batman’、‘Batmobile’、‘Batcopter’、‘Batbat’中的任意一个。因为这些字符串都以Bat开始,所以如果能够只指定一次前缀就很方便。这可以通过括号实现。在交互式环境中输入以下代码:
In [27]: bat = re.compile(r'Bat(man|mobile|copter|bat)')

In [28]: mo = bat.search('Batmobile lost a wheel')

In [29]: mo.group()
Out[29]: 'Batmobile'

In [30]: mo.group(1)
Out[30]: 'mobile'

方法调用mo.group()返回了完全匹配的文本'Batmoblie',而mo.group(1)只是返回第一个括号分组内匹配的文本'moblie'。使用管道字符和分组括号可以指定集中可选的模式,以让正则表达式去匹配。

如果需要匹配真正的管道字符,就用倒斜杠转义,即\|。
7.3.3 用问号实现可选匹配
有时候,想匹配的模式是可选的。就是说,不论这段文本在不在,正则表达式都会认为匹配。字符?表明它前面的分组在这个模式中是可选的。例如,在交互式环境中输入以下代码:
In [37]: bat = re.compile(r'Bat(wo)?man')

In [38]: mo1 = bat.search('The Adventure of Batman')

In [39]: mo1.group()
Out[39]: 'Batman'

In [40]: mo2 = bat.search('The Adventures of Batwoman')

In [41]: mo2.group()
Out[41]: 'Batwoman'

正则表达式中的(wo)?部分表明模式wo是可选的分组。在该正则表达式匹配的文本中,wo将出现零次或一次。这就是为什么正则表达式既匹配‘Batman’又匹配‘Batwoman’。

利用前面电话号码的例子,你可以让正则表达式寻找包含区号或不包含区号的电话号码。

In [49]: phone = re.compile(r'(\d{3}-)?\d{3}-\d{4}')

In [50]: mo = phone.search('my phone number is 455-555-4242')

In [51]: mo.group()
Out[51]: '455-555-4242'

In [52]: mo1 = phone.search('my phone number is 555-4242')

In [53]: mo1.group()
Out[53]: '555-4242'

你可以认为?是在说“匹配这个问号之前的分组零次或一次”。

如果需要匹配真正的问号字符,就使用转义字符\?。
7.3.4 用星号匹配零次或多次

*(星号)意味着“匹配零次或多次”,即星号之前的分组可以在文本中出现任意次。它可以完全不存在,也可以一次又一次地重复。让我们来看看batman的例子:
In [54]: bat = re.compile(r'bat(wo)*man')

In [55]: mo1 = bat.search('我最喜欢的超级英雄是batman!')

In [56]: mo1.group()
Out[56]: 'batman'

In [57]: mo2 = bat.search('我最喜欢的超级英雄是batwoman!')

In [58]: mo2.group()
Out[58]: 'batwoman'

In [59]: mo3 = bat.search('我最喜欢的超级英雄是batwowowowoman!')

In [63]: mo3.group()
Out[63]: 'batwowowowoman'

对于‘batman’,正则表达式的(wo)*部分匹配wo的零个实例。对于'batman',(wo)*匹配wo的一个实例。对于‘Batwowowowoman’,(wo)*匹配wo的4个实例。

如果需要匹配真正的星号字符,就在正则表达式的星号字符前加上倒斜杠,即\*。

这里需要强调一点,目前所教授的代码里,match的必要条件是字符串里一定要包含匹配正则表达式的内容。否则在输出时就会报错:

In [60]: mo3.group()
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-60-6bef6744f9ed> in <module>
----> 1 mo3.group()

AttributeError: 'NoneType' object has no attribute 'group'

这就说明mo3的字符串里没有匹配正则表达式规则的内容。

7.3.5 用加号匹配一次或多次
*意味着“匹配零次或多次”,+(加号)则意味着“匹配一次或多次”。星号不要求分组出现在匹配的字符串中,但加号不同,加号前面的分组必须“至少出现一次”。这不是可选的。
In [64]: bat = re.compile(r'bat(wo)+man')

In [65]: mo1 = bat.search('我最喜欢的超级英雄是batwoman!')

In [66]: mo1.group()
Out[66]: 'batwoman'

In [67]: mo2 = bat.search('我最喜欢的超级英雄是batwowowowoman!')

In [68]: mo2.group()
Out[68]: 'batwowowowoman'

In [69]: mo2 = bat.search('我最喜欢的超级英雄是batman!')

In [70]: mo2.group()
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-70-fe964b823f27> in <module>
----> 1 mo2.group()

AttributeError: 'NoneType' object has no attribute 'group'

正则表达式Bat(wo)+man 不会匹配字符串'我最喜欢的超级英雄是batman!'),因为加号要求wo至少出现一次。

如果需要匹配真正的加号字符,就在正则表达式的星号字符前加上倒斜杠,即\+。
7.3.6 用花括号匹配特定次数
如果想要一个分组重复特定次数,就在正则表达式中该分组的后面跟上花括号保卫的数字。例如,正则表达式(HA){3}将匹配字符串‘HAHAHA’,而不会匹配'HAHA',因为后者只重复了(HA)分组两次。

除了一个数字,还可以指定范围,即在花括号中写下一个最小数、一个逗号、一个最大值。例如,正则表达式(HA){3,5},将匹配(HAHAHA),(HAHAHAHA),(HAHAHAHAHA)。

也可以不写花括号中的第一个或第二个数字,表示不限定最小值或最大值。例如,(HA){,3}将匹配3次或更多次实例,(HA){,5}将匹配0-5次实例。花括号让正则表达式更简短。

In [71]: ha = re.compile(r'(HA){3}')

In [72]: mo1 = ha.search('HAHAHA')

In [73]: mo1.group()
Out[73]: 'HAHAHA'

In [74]: mo2 = ha.search('HA')

In [76]: None == mo2
Out[76]: True

这里,(HA){3}匹配'HAHAHA',但不匹配'HA',因为它不匹配'HA',所以search()返回None。

7.4 贪心与非贪心匹配

在字符串’HAHAHAHAHA’中,(HA){3,5}可以匹配3个、4个、5个实例。但若用(HA){3,5}去查找’HAHAHAHAHA’时,你会发现match对象的group()调用只会返回’HAHAHAHAHA’,而不是更短的结果。毕竟’HAHAHA’和’HAHAHAHA’也能有效地匹配正则表达式(HA){3,5}。

Python的正则表达式默认是’贪心’的,这表示在有二义的情况下,它们会尽可能匹配最长的字符串。花括号的“非贪心”(也称“惰性”)版本尽可能匹配最短的字符串,即在结束的花括号后跟着一个问号。

在查找相同字符串时,注意花括号的贪心形式和非贪心形式之间的区别:

In [77]: ha = re.compile(r'(HA){3,5}')

In [78]: mo1 = ha.search('HAHAHAHAHA')

In [79]: mo1.group()
Out[79]: 'HAHAHAHAHA'

In [80]: ha = re.compile(r'(HA){3,5}?')

In [81]: mo2 = ha.search('HAHAHAHAHA')

In [82]: mo2.group()
Out[82]: 'HAHAHA'

请注意,问号在正则表达式中可能有两种含义:声明非贪心匹配或表示可选的分组。这两种含义是完全无关的。

7.5 findall()方法

除了search()方法,Regex对象还有一个findall()方法。search()方法将返回一个Match对象,包含被查找字符串中的“第一次”匹配的文本:而findall()方法将返回一组字符串,包含被查找字符串中“所有”匹配文本。为了验证search()方法返回的match对象只包含第一次出现的匹配文本,请在交互式环境中输入以下代码:

In [83]: phone = re.compile(r'\d{3}-\d{3}-\d{4}')

In [84]: mo = phone.search('cell:455-555-9999 Work:212-555-0000')

In [85]: mo.group()
Out[85]: '455-555-9999'

另一方面,findall()方法不是返回一个Match对象,而是返回一个字符串列表,条件是在正则表达式中没有分组。列表中的每个字符串都是一段被查找的文本,它匹配该正则表达式。

In [86]: phone = re.compile(r'\d{3}-\d{3}-\d{4}') # has no groups

In [87]: phone.findall('cell:455-555-9999 Work:212-555-0000')
Out[87]: ['455-555-9999', '212-555-0000']

如果在正则表达式中有分组,那么findall()方法将返回元组的列表。每个元组表示一个找到的匹配,其中的项就是正则表达式中每个分组的匹配字符串。为了查看findall()方法的效果,请在交互式环境中输入以下代码:

In [88]: phone = re.compile(r'(\d{3})-(\d{3})-(\d{4})') # has no groups

In [90]: phone.findall('cell:455-555-9999 Work:212-555-0000')
Out[90]: [('455', '555', '9999'), ('212', '555', '0000')]

作为findall()方法的返回结果的总结,请记住以下两点。

1、如果在一个没有分组的正则表达式上调用,例如\d{3}-\d{3}-\d{4}.findall()方法将返回一个匹配字符串的列表,例如 ['455-555-9999', '212-555-0000']。

2、如果在一个有分组的正则表达式上调用,例如(\d{3})-(\d{3})-(\d{4}),findall()方法将返回一个字符串的元组的列表(每个分组对应一个字符串),例如[('455', '555', '9999'), ('212', '555', '0000')]。 
7.6 字符分类
在前面电话号码正则表达式例子中,\d可以代表任何数字。也就是说,\d是正则表达式(0|1|2|3|4|5|6|7|8|9)的缩写。有很多这样的“缩写字符分类”,如下所示:

\d  0~9的任何数字
\D  除0~9的数字以外的任何字符
\w  任何字母、数字或下划线字符(可以认为是匹配“单词”字符)
\W  除字母、数字和下划线意外的任何字符
\s  空格、制表符或换行符(可以认为是匹配“空白”字样)
\S  除空格、制表符、换行符以外的任何字符。

字符分类对于缩短正则表达式很有用。请注意,虽然\d 匹配数字,而\w 匹配数字、字母和下划线,但是没有速记字符类仅匹配字母。(你可以使用[a-zA-Z]字符类匹配字母。)

 In [105]: xmas = re.compile(r'\d+\s\w+')

In [106]: xmas.findall('12 drummers, 11 pipers, 10 lords, 9 ladies, 8 maids, 7 swans')
Out[106]: ['12 drummers', '11 pipers', '10 lords', '9 ladies', '8 maids', '7 swans']

正则表达式\d+\s\w+匹配的文本有一个或多个数字(\d+),然后是一个空白字符(\S),接下来是一个或多个字母/数字/下划线字符(\w+)。findall()方法将返回所有匹配该正则表达式的字符串,并将其放在一个列表中。

7.7 建立自己的字符分类

有时候你想匹配一组字符,但缩写的字符分类太宽泛。这时候你可以用方括号定义自己的字符分类。例如,字符分类将匹配所有元音字符,且不区分大小写。在交互式环境中输入一下代码:

In [107]: vo = re.compile(r'[aeiouAEIOU]')

In [108]: vo.findall('Baby baby bay HO~ O~ baby baby baby no~ ')
Out[108]: ['a', 'a', 'a', 'O', 'O', 'a', 'a', 'a', 'o']

也可以使用短横线表示字母或数字的范围。例如,字符分类[a-zA-Z0-9]将匹配所有小写字母、大写字母和数字。

请注意,在方括号内,普通的正则表达式符号不会被解释。这意味着你不需要在前面加上倒斜杠转义。例如,字符分类将匹配数字0~5和一个据点,你不需要将它写成[0-5\.]。

在字符分类的左方括号后加上一个插入字符(^),就可以得到“非字符类”。非字符类将匹配不在这个字符类中的所有字符。

In [110]: vo.findall('Baby baby bay HO~ O~ baby baby baby no~ ')
Out[110]: 
['B',
 'b',
 'y',
 ' ',
 'b',
 'b',
 'y',
 ' ',
 'b',
 'y',
 ' ',
 'H',
 '~',
 ' ',
 '~',
 ' ',
 'b',
 'b',
 'y',
 ' ',
 'b',
 'b',
 'y',
 ' ',
 'b',
 'b',
 'y',
 ' ',
 'n',
 '~',
 ' ']

现在程序不是匹配所有元音字符,而是匹配所有非元音字符。

7.8 插入字符和美元字符

可以在正则表达式的开始处使用插入符号(^),表明匹配必须发生在被查找文本开始处。类似地,可以在正则表达式的末尾加上美元符号($),表示该字符串必须以这个正则表达式的模式结束。可以同时使用^和$,表明整个字符串必须匹配该模式,也就是说,只匹配该字符串的某个子集是不够的。

例如,正则表达式r’^Hello’匹配以‘Hello’开始的字符串。

In [111]: beg = re.compile(r'^Hello')

In [112]: beg.search('Hello world!')
Out[112]: <re.Match object; span=(0, 5), match='Hello'>

In [113]: beg.search('He said hello.') == None
Out[113]: True

正则表达式r'\d$'匹配以数字0~9结束的字符串。

In [114]: end = re.compile(r'\d$')

In [115]: end.search('You number is 42')
Out[115]: <re.Match object; span=(15, 16), match='2'>

In [116]: end.search('You number is lalala') == None
Out[116]: True

正则表达式r'^\d+$'匹配从开始到结束都是数字的字符串。
In [119]: end = re.compile(r'^\d+$')

In [120]: end.search('42')
Out[120]: <re.Match object; span=(0, 2), match='42'>

In [121]: end.search('lalla') == None
Out[121]: True

In [122]: end.search('123123lalal') == None
Out[122]: True

In [123]: end.search('123123lalal123123') == None
Out[123]: True

前面交互式脚本例子中的最后两次search()调用表明,如果使用了^和$,那么整个字符串必须匹配该正则表达式。

7.9 通配字符

在正则表达式中,.(句点)称为“通配字符”。它匹配换行符之外的所有字符。例如,在交互式环境中输入以下代码:

In [127]: at = re.compile(r'.at')

In [129]: at.findall('aat,bat,cat,dat,eat,ddat')
Out[129]: ['aat', 'bat', 'cat', 'dat', 'eat', 'dat']


要记住,句点字符只匹配一个字符,这就是为什么在上面的例子中,对于文本ddat,只匹配dat。要匹配真正的句点,就使用倒斜杠转义,\。
7.9.1 用点-星匹配所有字符
有时候想要匹配所有字符串。例如,假定想要匹配字符串‘First Name:’,接下来是任意文本,再接下来是‘Last Name:’,然后又是任意文本。可以用点-星(.*)表示“任意文本”。回忆一下,句点字符表示“换行符外的所有单个字符”,星号字符表示“前面字符出现零次或多次”。
In [130]: name = re.compile(r'First Name:(.*) Last Name:(.*)')

In [131]: mo = name.search('First Name:piao Last Name:xiaosheng')

In [132]: mo.group()
Out[132]: 'First Name:piao Last Name:xiaosheng'

In [133]: mo.group(1)
Out[133]: 'piao'

In [134]: mo.group(2)
Out[134]: 'xiaosheng'

点-星使用“贪心”模式:它总是匹配尽可能多的文本。要用“非贪心”模式匹配所有文本,就使用点-星和问号,像和大括号一起使用那样,问号告诉Python用非贪心模式匹配:
In [147]: name = re.compile(r'<.*?>')

In [148]: mo = name.search('<piao xiaosheng><yesyesyes>')

In [149]: mo.group()
Out[149]: '<piao xiaosheng>'

In [150]: name = re.compile(r'<.*>')

In [151]: mo = name.search('<piao xiaosheng><yesyesyes>')

In [152]: mo.group()
Out[152]: '<piao xiaosheng><yesyesyes>'

两个正则表达式都可以翻译成“匹配一个左尖括号,接下来是任意字符,然后是一个右尖括号”。但是字符串'<piao xiaosheng><yesyesyes>'对右尖括号有两种可能的匹配。在非贪心的正则表达式中,Python匹配最短可能的字符串:<piao xiaosheng>。在贪心模式下,则匹配最长可能的字符串:'<piao xiaosheng><yesyesyes>'。
7.9.2 用句点字符匹配换行符
点-星将匹配换行符意外的所有字符。传入re.DOTALL作为re.compile()的第二个参数,可以让句点字符匹配所有字符,也包括换行符。

In [153]: non = re.compile('.*')

In [155]: non.search('abcd\nacec').group()
Out[155]: 'abcd'

In [156]: non = re.compile('.*',re.DOTALL)

In [157]: non.search('abcd\nacec').group()
Out[157]: 'abcd\nacec'

正则表达式non在创建时没有向re.compile()传入re.DOTALL,它将匹配所有字符,直到出现第一个换行符。但是第二个non在创建时向re.compile()传入了re.DOTALL,它将匹配所有字符。这就是为什么第二个non.search()匹配完整的字符串,包括其中的换行符。

7.10 正则表达式符号复习

?匹配零次或一次前面的分组

* 匹配零次或多次前面的分组

+ 匹配一次或多次前面的分组

{n} 匹配n次前面的分组

{n,}匹配n次到更多次前面的分组

{,m}匹配零次到m次前面的分组

{n,m} 匹配至少n次,至多m次前面的分组

{n,m}?、*?或+?前面的分组进行非贪心匹配

^spam 意味着字符串必须以spam开始

spam$意味着字符串必须以spam结束

.匹配所有字符串,换行符除外

\d、\w和\s分别匹配数字、单词和空格

\D、\W和\S分别匹配数字、单词和空格外的所有字符

[abc]匹配方括号内的任意字符

[^abc]匹配不在方括号内的任意字符

7.11 不区分大小写的匹配

通常,正则表达式用你制定的大小写匹配文本。但是有时候你只关心匹配的字母,不关心它们是大写还是小写。要让正则表达式不区分大小写,可以向re.IGNORECASE或re.I作为第二个参数。

In [160]: ro = re.compile(r'abcd',re.I)

In [161]: ro.search('ABCDabcd').group()
Out[161]: 'ABCD'

In [165]: ro = re.compile(r'(abcd)*',re.IGNORECASE)

In [166]: ro.search('ABCDabcdAASDAS').group()
Out[166]: 'ABCDabcd'

7.12 用sub()方法替换字符串

正则表达式不仅能找到文本模式,而且能够用新的文本替换这些模式。Regex对象的sub()需要传入两个参数。第一个参数是一个字符串,用于替换发现的匹配。第二个参数是一个字符串,即正则表达式。sub()方法返回替换完成后的字符串。

In [173]: name = re.compile(r'piaoxiaomin \w+')

In [174]: name.sub('quanxiaosheng','piaoxiaomin is a girl')
Out[174]: 'quanxiaosheng a girl'

有时候,你可能需要使用匹配的文本本身作为替换的一部分。在sub()方法的第一个参数中,可以输入\1、\2、\3,表示“在替换中输入分组1、2、3的文本”。

假如,想要隐去某些人的姓名,只显示他们姓名的第一个字母。要做到这一点,可以使用正则表达式(\w)\w*,传入r'\1****'作为sub()方法的第一个参数。字符串中的\1将由分组1匹配的文本所替代,也就是正则表达式的(\w)分组:
In [175]: name = re.compile(r'(\w)\w*')

In [177]: name.sub(r'\1****','quana liwan demax sunli')
Out[177]: 'q**** l**** d**** s****'

这里一定要注意,在name.sub字符串里是有转义字符r在的,如果省略了r,就会出错。

In [176]: name.sub('\1****','quana liwan demax sunli')
Out[176]: '\x01**** \x01**** \x01**** \x01****'

7.13 管理复杂的正则表达式

如果要匹配的文本模式很简单,那么使用正则表达式就好。但匹配复杂的文本模式,可能需要长的、令人费解的正则表达式。你可以告诉re.compile()忽略正则表达式字符串中的空白符和注释,从而缓解这一点。要实现这种详细模式,可以向re.compile()传入变量re.VERBOSE作为第二个参数。

现在,不比使用这样难以阅读的正则表达式(原文这里的正则表达式很复杂,懒得打了,简单一点能理解意思就行):

name = re.compile(r'(\w)\w*')

你可以将正则表达式放入多行中,并加上注释:
In [183]: name = re.compile(r'''(
     ...: (\w)   # 将第一个字母分第一个组
     ...: \w*    # 将其他字母分第二个组
     ...: )''',re.VERBOSE)

请注意,前面的例子使用三重引号创建了一个多行字符串,这样就可以将正则表达式定义放在多行中,让它更具有可读性。

正则表达式字符串中的注释规则与普通的Python代码一样。
7.14 组合使用re.IGNORECASE、re.DOTALL 和 re.VERBOSE
如果你希望在正则表达式中使用re.VERBOSE来编写注释,还希望使用re.IGNORECASE来忽略大小写怎么办?遗憾的是,re.compile()函数只接收一个值作为它的第二参数。可以使用管道字符|将变量组合起来,从而绕过这个限制。管道字符在这里成为“按位或”操作符。

所以,如果希望正则表达式不区分大小写,并且句点字符匹配换行符,就可以这样构造re.compile()调用。

使用第二个参数的3个选项,看起来像这样:
In [185]: some = re.compile('foo',re.IGNORECASE | re.DOTALL | re.VERBOSE)

这个语法有点老,源自早期的Python版本。位运算符的细节超出了本书的范围。可以向第二个参数传入其他选项,它们不常用,你可以在前面的资源中找到有关它们的信息。

胭惜雨

2021年05月25日

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据