这本书虽然叫编程快速上手,但实际上讲的是如何利用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日