當前位置:首頁 ? 深入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