当前位置:首页 ‣ 深入Python 3 ‣
Updated October 7, 2009 • Difficulty level: ♦♦♦♢♢
❝ Some people, when confronted with a problem, think “I know, I’ll use regular expressions.” Now they have two problems. ❞
— Jamie Zawinski
所有的现代编程语言都有内建字符串处理函数。在python里查找,替换字符串的方法是:index()、 find()、split()、 count()、 replace()等。但这些方法都只是最简单的字符串处理。比如:用index()方法查找单个子字符串,而且查找总是区分大小写的。为了使用不区分大小写的查找,可以使用s.lower()或者s.upper(),但要确认你查找的字符串的大小写是匹配的。replace() 和split() 方法有相同的限制。
如果使用string的方法就可以达到你的目的,那么你就使用它们。它们速度快又简单,并且很容易阅读。但是如果你发现自己要使用大量的if语句,以及很多字符串函数来处理一些特例,或者说你需要组合调用split() 和 join() 来切片、合并你的字符串,你就应该使用正则表达式。
正则表达式有强大并且标准化的方法来处理字符串查找、替换以及用复杂模式来解析文本。正则表达式的语法比我们的程序代码更紧凑,格式更严格,比用组合调用字符串处理函数的方法更具有可读性。甚至你可以在正则表达式中嵌入注释信息,这样就可以使它有自文档化的功能。
☞如果你在其他语言中使用过正则表达式(比如perl,javascript或者php),python的正则表达式语法和它们的很像。阅读re模块的摘要信息可以了解到一些处理函数以及它们参数的一些概况。
⁂
下面一系列的示例的灵感来自于现实生活中我几年前每天的工作。我需要把一些街道地址导入一个新的系统,在这之前我要从一个遗留的老系统中清理和标准化这些街道地址。下面这个例子展示我怎么解决这个问题。
>>> 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’不够好。因为并不是所有的地址结尾都有它。一些地址简单的用一个街道名结尾。大部分的情况下不会有问题,但如果街道的名字就叫‘BROAD’,这个时候,正则表达式会匹配到‘BROAD’的最后4个字符,这并不是我想要的。
>>> 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'⁂
你肯定见过罗马数字,即使你不认识他们。你可能在版权信息、老电影、电视、大学或者图书馆的题词墙看到(用Copyright MCMXLVI” 表示版权信息,而不是用 “Copyright 1946”),你也可能在大纲或者目录参考中看到他们。这种系统的数字表达方式可以追溯到罗马帝国(因此而得名)。
在罗马数字中,有七个不同的数字可以以不同的方式结合起来表示其他数字。
I = 1V = 5X = 10L = 50C = 100D = 500M = 1000下面是几个通常的规则来构成罗马数字:
怎么验证一个字符串是否是一个合法的罗马数字呢?我们可以每次取一个字符来处理。因为罗马数字总是从高位到低位来书写。我们从最高位的千位开始。表示1000或者更高的位数值,方法是用一系列的M来重复表示。
>>> 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>
百位的匹配比千位复杂。根据值的不同,会有不同的表达方式。
100 = C200 = CC300 = CCC400 = CD500 = D600 = DC700 = DCC800 = DCCC900 = CM因此会有四种可能的匹配模式:
CMCD这两个模式还可以组合起来表示:
下面的例子展示了怎样在罗马数字中验证百位。
>>> import re >>> pattern = '^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} 在上一节中,你处理过同样的字符可以重复0到3次的情况。实际上,还有另一种正则表达式的书写方式可以表达同样的意思,而且这种表达方式更具有可读性。首先看看我们在前面例子中使用的方法。
>>> import re >>> pattern = '^M?M?M?$' >>> re.search(pattern, 'M') ① <_sre.SRE_Match object at 0x008EE090> >>> pattern = '^M?M?M?$' >>> re.search(pattern, 'MM') ② <_sre.SRE_Match object at 0x008EEB48> >>> pattern = '^M?M?M?$' >>> re.search(pattern, 'MMM') ③ <_sre.SRE_Match object at 0x008EE090> >>> re.search(pattern, 'MMMM') ④ >>>
>>> 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') ⑤
>>> 现在,我们继续解释正则表达式匹配罗马数字中的十位和个位。下面的例子是检查十位。
>>> pattern = '^M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)$' >>> re.search(pattern, 'MCMXL') ① <_sre.SRE_Match object at 0x008EEB48> >>> re.search(pattern, 'MCML') ② <_sre.SRE_Match object at 0x008EEB48> >>> re.search(pattern, 'MCMLX') ③ <_sre.SRE_Match object at 0x008EEB48> >>> re.search(pattern, 'MCMLXXX') ④ <_sre.SRE_Match object at 0x008EEB48> >>> re.search(pattern, 'MCMLXXXX') ⑤ >>>
个位数的匹配是同样的模式,我会告诉你细节以及最终结果。
>>> 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允许你使用松散正字表达式来达到目的。松散正字表达式和普通紧凑的正则表达式有两点不同:
下面是一个更加清楚的例子。我们再来看看把上面的紧凑正则表达式改写成松散正字表达式后的样子。
>>> 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') ④到目前为止,我们主要关注于整个表达式是否能匹配到,要么整个匹配,要么整个都不匹配。但正则表达式还有更加强大的功能。如果正则表达式成功匹配,你可以找到正则表达式中某一部分匹配到什么。
这个例子来自于我在真实世界中遇到的另一个问题。这个问题是:解析一个美国电话号码。客户想用自由的格式来输入电话号码(在单个输入框),这需要存储区域码,交换码以及后四码(美国的电话分为区域码、交换码和后四码)。我在网上搜索,发现了很多解决这个问题的正则表达式,但是它们都能不完全满足我的要求。
下面是我要接受的电话号码格式:
800-555-1212800 555 1212800.555.1212(800) 555-12121-800-555-1212800-555-1212-1234800-555-1212x1234800-555-1212 ext. 1234work 1-(800) 555.1212 #1234样式够多的!在上面的例子中,我知道区域码是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'
>>> 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') ⑤
>>> 下面的例子展示怎么处理电话号码前面还有其他字符的情况。
>>> 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') ④
>>> 我们回过头看看。到目前为止,所有的正则表达式都匹配了字符串开始位置。但现在在字符串的开头可能有一些你想忽略掉的不确定的字符。为了匹配到想要的数据,你需要跳过他们。我们来看看不明确匹配字符串开始的方法。
>>> 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') ③
('800', '555', '1212', '')
>>> phonePattern.search('80055512121234') ④
('800', '555', '1212', '1234')看看正则表达式失控有多快?快速回顾一下之前的例子。你能说出他们的区别吗?
你看到了最终的答案(这就是最终答案!如果你发现还有它不能正确处理的情况,我也不想知道了 )。在你忘掉它之前,我们来把它改写成松散正则表达式吧。
>>> phonePattern = re.compile(r'''
# don't match beginning of string, number can start anywhere
(\d{3}) # area code is 3 digits (e.g. '800')
\D* # optional separator is any number of non-digits
(\d{3}) # trunk is 3 digits (e.g. '555')
\D* # optional separator
(\d{4}) # rest of number is 4 digits (e.g. '1212')
\D* # optional separator
(\d*) # extension is optional and can be any number of digits
$ # end of string
''', re.VERBOSE)
>>> phonePattern.search('work 1-(800) 555.1212 #1234').groups() ①
('800', '555', '1212', '1234')
>>> phonePattern.search('800-555-1212') ②
('800', '555', '1212', '')⁂
这只是正则表达式能完成的工作中的冰山一角。换句话说,尽管你可能很受打击,相信我,你已经不是什么都不知道了。
现在,你应该已经熟悉了下面的技巧:
^ 匹配字符串开始位置。
$ 匹配字符串结束位置。
\b 匹配一个单词边界。
\d 匹配一个数字。
\D 匹配一个任意的非数字字符。
x? 匹配可选的x字符。换句话说,就是0个或者1个x字符。
x* 匹配0个或更多的x。
x+ 匹配1个或者更多x。
x{n,m} 匹配n到m个x,至少n个,不能超过m个。
(a|b|c) 匹配单独的任意一个a或者b或者c。
(x) 这是一个组,它会记忆它匹配到的字符串。你可以用re.search返回的匹配对象的groups()函数来获取到匹配的值。
正则表达式非常强大,但它也并不是解决每一个问题的正确答案。你需要更多的了解来判断哪些情况适合使用正则表达式。某些时候它可以解决你的问题,某些时候它可能带来更多的问题。
© 2001–9 Mark Pilgrim