Python中的正则表达式(来自Dive into Python3)
利用python提供的最简单的字符串函数index(), find(), split(), count(), replace()等函数来实现字符串的剪切工作将会十分困难,因为这些函数几乎都是对大小写敏感的。如果你的任务刚好可以用这些函数完成,那就用——毕竟函数简单,代码可读性高。但是如果你的字符串处理程序中不得不出现大量的条件判断或者不断地使用split()和join()进行剪切连接操作,那么你就得考虑正则表达式了。
案例1:街道地址
>>> s = '100 NORTH MAIN ROAD'
>>> s.replace('ROAD', 'RD.')
'100 NORTH MAIN RD.'
>>> s = '100 NORTH BROAD ROAD'
>>> s.replace('ROAD', 'RD.')
'100 NORTH BRD. RD.'
>>> s[:-4] + s[-4:].replace('ROAD', 'RD.')
'100 NORTH BROAD RD.'
>>> import re
>>> re.sub('ROAD$', 'RD.', s)
'100 NORTH BROAD RD.'
作者面临的任务是将字符串中最后一个'ROAD'替换为'RD.'。最初使用字符串特有的replace方法即可完成任务。但是很快遇到了不便,出现了'BROAD ROAD',而replace方法却不能分辨二者的区别,于是改用索引方法实现。但这个方法存在的问题是:整个过程是依赖被搜索字符串长度的,比如若是想搜索'STREET',那就必须搜索最后6个字符——是一种不易扩展的方法。
作者于是改用正则表达式中的re.sub方法,并将正则表达式设置为'ROAD$',其中的$代表匹配字符串末尾。(对应的'^'放在正则表达式之前可以用来匹配字符串首。)
但是作者很快遇到了第二个问题,如果某个地址恰恰是以'BROAD'作为结尾怎么办?’100 BROAD‘会直接被转化成’100 BRD.‘。于是作者进行了进一步的改进:
>>> s = '100 BROAD'
>>> re.sub('ROAD$', 'RD.', s)
'100 BRD.'
>>> re.sub('\\bROAD$', 'RD.', s)
'100 BROAD'
>>> re.sub(r'\bROAD$', 'RD.', s)
'100 BROAD'
>>> s = '100 BROAD ROAD APT. 3'
>>> re.sub(r'\bROAD$', 'RD.', s)
'100 BROAD ROAD APT. 3'
>>> re.sub(r'\bROAD\b', 'RD.', s)
'100 BROAD RD. APT 3'
作者在原有的正则表达式之前加入了'\b',旨在作为'ROAD'的边界。这样,就能用以匹配位于末端的独立'ROAD'了。但是在Python字符串中若想输入单独的''就涉及到字符转义的问题——这里不得不对''进行转义,否则解释器会认为’\b‘是一个ascii字符,而不是''+'b'。
所以作者建议书写正则表达式时,一律使用原始字符r'',这样的字符串中每个字符都具有唯一的含义,完全不需要进行字符的转义处理。所以此时的正则表达式为r'\bROAD$'。
很快作者有遇到了更多的问题,有的地址含有独立的'ROAD',但是并不以这个词结尾。于是将正则表达式改为r'\bROAD\b'。
案例2:罗马数字
罗马数字体系中一共有7个字母会被反复、组合使用,它们分别代表:
I = 1
V = 5
X = 10
L = 50
C = 100
D = 500
M = 1000
另外,罗马数字的构成还有一些额外的规则:
- 字符有时是累加的例如,
I
是1,VI
是5+1=6,VIII
是5+1+1+1=8; I,X,C,M
四个字符每种最多连续重复三次,一旦需要表达四个连续的含义,应使用对应位置上的5倍字符减去它们的形式来进行表达:例如,罗马数字中将44表达为XLVI
,意味着X-L=40,V-I=4
;- 利用下一位减去当前位的方式来表达9的概念:90被写成
CX,C-X=90
,900被写成MC,M-C=900
; - 表达5的概念的字符永远不重复,因为
LL
代表100,可以直接使用C
表达; - 罗马数字要从左向右读。字符的顺序对含义的影响十分大,例如
DC
代表600,而CD
代表400;同时,CI
代表101,而IC
则是一种非法表示,因为罗马数字中的减法不能隔位进行,所以要想表达99的概念必须使用XCIX
;
匹配合法的千位表示
这里暂时只匹配1000,2000,3000的合法表示:
>>> import re
>>> pattern = '^M?M?M?$'
>>> re.search(pattern, 'M')
<_sre.SRE_Match object at 0106FB58>
>>> re.search(pattern, 'MM')
<_sre.SRE_Match object at 0106C290>
>>> re.search(pattern, 'MMM')
<_sre.SRE_Match object at 0106AA38>
>>> re.search(pattern, 'MMMM')
>>> re.search(pattern, '')
<_sre.SRE_Match object at 0106F4A8>
这里唯一需要解释的就是'?'的使用,用以说明'?'前的字符是可选的。这里使用了re.search方法进行匹配,匹配只需要检查返回值是否为None即可,None说明匹配失败。
匹配合法的百位表示
合法的百位表示只有9种:
100 = C
200 = CC
300 = CCC
400 = CD
500 = D
600 = DC
700 = DCC
800 = DCCC
900 = CM
所以可以总结成以下四种形式:
CM
CD
- 从字符串首开始0到3个
C
D
, 然后接着0到3个C
最后两种模式可以结合成:字符串首为可选的'D',紧接着0到3个'C'
>>> import re
>>> pattern = r'^M?M?M?(CM|CD|D?C?C?C?)$'
>>> re.search(pattern, 'MCM')
<_sre.SRE_Match object at 01070390>
>>> re.search(pattern, 'MD')
<_sre.SRE_Match object at 01073A50>
>>> re.search(pattern, 'MMMCCC')
<_sre.SRE_Match object at 010748A8>
>>> re.search(pattern, 'MCMC')
>>> re.search(pattern, '')
<_sre.SRE_Match object at 01071D98>
这里的正则表达式是从上面的任务中改进得到的,最前面和上面的表达式完全相同;第二部分由一个括号组成,括号内代表互斥的三种情况,最后的’$’符号代表了字符串尾。
另一种处理罗马数字的手段——使用{n,m}语法
>>> pattern = '^M{0,3}$'
>>> re.search(pattern, 'M')
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MM')
<_sre.SRE_Match object at 0x008EE090>
>>> re.search(pattern, 'MMM')
<_sre.SRE_Match object at 0x008EEDA8>
>>> re.search(pattern, 'MMMM')
>>>
这里使用了'^M{0,3}'的表达式,代表了从字符串首开始匹配0到3个M,可以匹配空串。
匹配罗马数字中的十位和个位数
>>> pattern = '^M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)$'
>>> re.search(pattern, 'MCMXL')#MCMXL=1940#
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MCML')#MCML=1950#
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MCMLX')#MCMLX=1960#
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MCMLXXX')#MCMLXXX=1980#
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MCMLXXXX')#正确的1990为,MCMXC#
>>>
若想要匹配个位数,则使用相同的法则即可写出正则表达式:
>>> pattern = '^M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)(IX|IV|V?I?I?I?)$'
那么如何使用{n,m}语法来书写这个正则表达式呢?
>>> pattern = '^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$'
>>> re.search(pattern, 'MDLV')
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MMDCLXVI')
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MMMDCCCLXXXVIII')
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'I')
<_sre.SRE_Match object at 0x008EEB48>
冗长的正则表达式
我们上面讨论的都是紧密型正则表达式,但是这种正则表达式的可读性极差。甚至就算你现在明白正则表达式的含义,也不见得你半年之后还能理解。
python允许你书写冗长正则表达式来解决可读性的问题:
- 冗长正则表达式中的所有空格,tab字符,换行符都不具有匹配功能,如果你真的希望它们具有匹配功能,你需要对其进行转义;
- 冗长正则表达式中的所有注释部分不具有匹配功能;
>>> pattern = '''
^ # beginning of string
M{0,3} # thousands - 0 to 3 Ms
(CM|CD|D?C{0,3}) # hundreds - 900 (CM), 400 (CD), 0-300 (0 to 3 Cs),
# or 500-800 (D, followed by 0 to 3 Cs)
(XC|XL|L?X{0,3}) # tens - 90 (XC), 40 (XL), 0-30 (0 to 3 Xs),
# or 50-80 (L, followed by 0 to 3 Xs)
(IX|IV|V?I{0,3}) # ones - 9 (IX), 4 (IV), 0-3 (0 to 3 Is),
# or 5-8 (V, followed by 0 to 3 Is)
$ # end of string
'''
>>> re.search(pattern, 'M', re.VERBOSE)
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MCMLXXXIX', re.VERBOSE)
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MMMDCCCLXXXVIII', re.VERBOSE)
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'M')
注意最后一个示例,‘M’居然无法匹配,为什么?因为方法调用时没有传入re.VERBOSE标记,这样解释器就会把一个冗长正则表达式解读为紧密正则表达式,当然无法生效。
案例3:解析电话号码
作者的一个新任务是将下列形式的电话号码统一化表示:
800-555-1212
800 555 1212
800.555.1212
(800) 555-1212
1-800-555-1212
800-555-1212-1234
800-555-1212x1234
800-555-1212 ext. 1234
work 1-(800) 555.1212 #1234
我们从一个正则表达式开始完成这项任务:
>>> phonePattern = re.compile(r'^(\d{3})-(\d{3})-(\d{4})$')
>>> phonePattern.search('800-555-1212').groups()
('800', '555', '1212')
>>> phonePattern.search('800-555-1212-1234')
>>> phonePattern.search('800-555-1212-1234').groups()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute 'groups'
这里的正则表达式以'(\d{3})'开头,代表恰好匹配三个任意数字‘\d’代表数字字符,{3}则代表恰好三个,括号将上述两种概念合并成一个组。之后要匹配一个短横线,之后再匹配一个三数字组和一个短横线。最后于结尾匹配一个四数字组。
接下来对正则表达式的search方法的调用结果使用groups方法,可以得到groups方法匹配到的所有的结果。但是四组数组成的电话号码就匹配失败了。同时也说明永远不要search().groups(),因为一旦search返回一个None对象,那么groups方法会报错。
>>> phonePattern = re.compile(r'^(\d{3})-(\d{3})-(\d{4})-(\d+)$')
>>> phonePattern.search('800-555-1212-1234').groups()
('800', '555', '1212', '1234')
>>> phonePattern.search('800 555 1212 1234')
>>>
>>> phonePattern.search('800-555-1212')
>>>
紧接着对正则表达式进行进一步的修改,我们在最后要求匹配一个短横线和一个大于等于一个数字的数字组。但是效果任然不够理想:不能匹配不带短横线的电话号码,而且反而不能匹配不带拓展部分的电话号码了。
>>> phonePattern = re.compile(r'^(\d{3})\D+(\d{3})\D+(\d{4})\D+(\d+)$')
>>> phonePattern.search('800 555 1212 1234').groups()
('800', '555', '1212', '1234')
>>> phonePattern.search('800-555-1212-1234').groups()
('800', '555', '1212', '1234')
>>> phonePattern.search('80055512121234')
>>>
>>> phonePattern.search('800-555-1212')
>>>
这里我们把所有的短横线匹配改造成了一个或多个任意非数字字符的的匹配,这样面对不适用短横线进行分割的电话号码也能完成匹配。但是从最后两个测试来看,仍然不好用,三组数字的电话号码仍然无法匹配。而且对于不加任何分隔符号的电话号码似乎我们也束手无策。
>>> phonePattern = re.compile(r'^(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$')
>>> phonePattern.search('80055512121234').groups()
('800', '555', '1212', '1234')
>>> phonePattern.search('800.555.1212 x1234').groups()
('800', '555', '1212', '1234')
>>> phonePattern.search('800-555-1212').groups()
('800', '555', '1212', '')
>>> phonePattern.search('(800)5551212 x1234')
>>>
这里我们仅做出一项改动,把所有的+替换为*,意思是匹配0个或多个非数字字符。但是最后一个测试又无法通过。这次的问题比较简单,只不过是8前面多了一个括号。
>>> phonePattern = re.compile(r'^\D*(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$')
>>> phonePattern.search('(800)5551212 ext. 1234').groups()
('800', '555', '1212', '1234')
>>> phonePattern.search('800-555-1212').groups()
('800', '555', '1212', '')
>>> phonePattern.search('work 1-(800) 555.1212 #1234')
>>>
最后一个测试又失败了,因为我们之前认为800之前出现的字符只能是非数字字符,但是现在出现了一个1。
>>> phonePattern = re.compile(r'(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$')
>>> phonePattern.search('work 1-(800) 555.1212 #1234').groups()
('800', '555', '1212', '1234')
>>> phonePattern.search('800-555-1212').groups()
('800', '555', '1212', '')
>>> phonePattern.search('80055512121234').groups()
('800', '555', '1212', '1234')
这里我们采取了一种新的手段,不去匹配数字前出现的非数字字符。因为我们根本不需要要求最前面有非数字字符出现。所以去掉^即可实现所有的匹配。
小结
下面我们可以就常见的正则表达式组件进行一个总结和解释了:
^
匹配字符串的首端;$
匹配字符串的末端;\b
匹配一个字符串的边界;\d
匹配任何数字字符;\D
匹配任何非数字字符;x?
可选地匹配一个字符,或者说匹配它0次或者多次;x*
匹配一个字符0次或者多次;x+
匹配一个字符一次或者多次;x{n,m}
匹配一个字符不少于n次同时也不多于m次;(a|b|c)
仅匹配a, b, c三种模式中的一种;(x)
一般来说记忆匹配到的字符串,通过对re.search返回的对象使用groups方法,可以得到括号内匹配成功的字符串元组;