# Chapter 5 正則表達式
> " Some people, when confronted with a problem, think “I know, I’ll use regular expressions.” Now they have two problems. "
> — [Jamie Zawinski](//www.jwz.org/hacks/marginal.html)
## 深入
所有的現代編程語言都有內建字符串處理函數。在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'
'100 NORTH MAIN RD.'
>>> s = '100 NORTH BROAD ROAD'
'100 NORTH BRD. RD.'
'100 NORTH BROAD RD.'
'100 NORTH BROAD RD.'
```
1. 我的目的是要標準化街道的格式。而‘ROAD’總是在.RD的前面。剛開始我以為只需要簡單的使用string的replace()方法就可以。所有的數據都是大寫的,因此不會出現大小寫不匹配的問題。而查找的字符串‘ROAD’也是一個常量。在這個簡單的例子中s.replace()可以很好的工作。
2. 事實上,不幸的是,我很快發現一個問題,在一些地址中‘ROAD’出現了兩次,一個是前面的街道名里帶了‘ROAD’,一個是‘ROAD’本身。repalce()發現了兩個就把他們都給替換掉了。這意味著,我的地址錯了。
3. 為了解決地址中出現超過一個‘ROAD’子字符串的問題,你可能會這么考慮:只在地址的最后四個字符中查找和替換‘‘ROAD’(s[-4:])。然后把剩下的字符串獨立開來處理(s[:-4])。這個方法很笨拙。比如,這個方法會依賴于你要替換的字符串長度(如果你用‘.ST’來替換‘STREET’,就需要在s[-6:]中查找‘STREET’,然后再取s[:-6]。你難道還想半年后回來繼續修改BUG?反正我是不想。
4. 是時候轉換到正則表達式了。在python中,所有的正則表達式相關功能都包含在re模塊中。
5. 注意第一個參數‘ROAD$’,這是一個匹配‘ROAD’僅僅出現在字符串結尾的正則表達式。$ 表示“字符串結尾”。(還有一個相應的表示“字符串開頭”的字符 ^ )。正則表達式模塊的re.sub()函數可以做字符串替換,它在字符串s中用正則表達式‘ROAD$’來搜索并替換成‘RD.’。它只會匹配字符串結尾的‘ROAD’,而不會匹配到‘BROAD’中的‘ROAD’,因為這種情況它在字符串的中間。
^ 匹配字符串開始. $ 匹配字符串結尾
繼續我的處理街道地址的故事。我很快發現,在之前的例子中,匹配地址結尾的‘ROAD’不夠好。因為并不是所有的地址結尾都有它。一些地址簡單的用一個街道名結尾。大部分的情況下不會有問題,但如果街道的名字就叫‘BROAD’,這個時候,正則表達式會匹配到‘BROAD’的最后4個字符,這并不是我想要的。
```
>>> s = '100 BROAD'
>>> re.sub('ROAD$', 'RD.', s)
'100 BRD.'
'100 BROAD'
'100 BROAD'
>>> s = '100 BROAD ROAD APT. 3'
'100 BROAD ROAD APT. 3'
'100 BROAD RD. APT 3'
```
1. 我真正想要的‘ROAD’,必須是匹配到字符串結尾,并且是獨立的詞(他不能是某個比較長的詞的一部分)。為了在正則表達式中表達這個獨立的詞,你可以使用‘\b’。它的意思是“在右邊必須有一個分隔符”。在python中,比較復雜的是‘\’字符必須被轉義,這有的時候會導致‘\’字符傳染(想想可能還要對\字符做轉義的情況)。這也是為什么perl中的正則表達式比python的簡單的原因之一。另一方面,perl會在正則表達式中混合其他非正則表達式的語法,如果出現了bug,那么很難區分這個bug是在正則表達式中,還是在其他的語法部分。
2. 為了解決‘\’字符傳染的問題,可以使用原始字符串。這只需要在字符串的前面添加一個字符‘r’。它告訴python,字符串中沒有任何字符需要轉義。‘\t’是一個制表符,但r‘\t’只是一個字符‘\’緊跟著一個字符t。我建議在處理正則表達式的時候總是使用原始字符串。否則,會因為理解正則表達式而消耗大量時間(本身正則表達式就已經夠讓人困惑的了)。
3. 哎,不幸的是,我發現了更多的地方與我的邏輯背道而馳。街道地址包含了獨立的單詞‘ROAD’,但并不是在字符串尾,因為街道后面還有個單元號。因為'ROAD'并不是最靠后,就不能匹配,因此re.sub()最后沒有做任何的替換,只是返回了一個原始的字符串,這并不是你想要的。
4. 為了解決這個問題,我刪除了正則表達式尾部的$,然后添加了一個\b。現在這個正則表達式的意思是“在字符串的任意位置匹配獨立的‘ROAD’單詞”不管是在字符串的結束還是開始,或者中間的任意一個位置。
## 案例研究: 羅馬數字
你肯定見過羅馬數字,即使你不認識他們。你可能在版權信息、老電影、電視、大學或者圖書館的題詞墻看到(用Copyright MCMXLVI” 表示版權信息,而不是用 “Copyright 1946”),你也可能在大綱或者目錄參考中看到他們。這種系統的數字表達方式可以追溯到羅馬帝國(因此而得名)。
在羅馬數字中,有七個不同的數字可以以不同的方式結合起來表示其他數字。
* `I = 1`
* `V = 5`
* `X = 10`
* `L = 50`
* `C = 100`
* `D = 500`
* `M = 1000`
下面是幾個通常的規則來構成羅馬數字:
* 大部分時候用字符相疊加來表示數字。I是1, II是2, III是3。VI是6(挨個看來,是“5 和 1”的組合),VII是7,VIII是8。
* 含有10的字符(I,X,C和M)最多可以重復出現三個。為了表示4,必須用同一位數的下一個更大的數字5來減去一。不能用IIII來表示4,而應該是IV(意思是比5小1)。40寫做XL(比50小10),41寫做XLI,42寫做XLII,43寫做XLIII,44寫做XLIV(比50小10并且比5小1)。
* 有些時候表示方法恰恰相反。為了表示一個中間的數字,需要從一個最終的值來減。比如:9需要從10來減:8是VIII,但9確是IX(比10小1),并不是VIII(I字符不能重復4次)。90是XC,900是CM。
* 表示5的字符不能在一個數字中重復出現。10只能用X表示,不能用VV表示。100只能用C表示,而不是LL。
* 羅馬數字是從左到右來計算,因此字符的順序非常重要。DC表示600,而CD完全是另一個數字400(比500小100)。CI是101,IC不是一個羅馬數字(因為你不能從100減1,你只能寫成XCIX,表示比100小10,且比10小1)。
### 檢查千位數
怎么驗證一個字符串是否是一個合法的羅馬數字呢?我們可以每次取一個字符來處理。因為羅馬數字總是從高位到低位來書寫。我們從最高位的千位開始。表示1000或者更高的位數值,方法是用一系列的M來重復表示。
```
>>> import re
<_sre.SRE_Match object at 0106FB58>
<_sre.SRE_Match object at 0106C290>
<_sre.SRE_Match object at 0106AA38>
<_sre.SRE_Match object at 0106F4A8>
```
1. 這個模式有三部分。^表示必須從字符串開頭匹配。如果沒有指定^,這個模式將在任意位置匹配M,這個可能并不是你想要的。你需要確認是否要匹配字符串開始的M,還是匹配單個M字符。因為它重復了三次,你要在一行中的任意位置匹配0到3次的M字符。$匹配字符串結束。當它和匹配字符串開始的^一起使用,表示匹配整個字符串。沒有任何一個字符可在M的前面或者后面。
2. re模塊最基本的方法是search()函數。它使用正則表達式來匹配字符串(M)。如果成功匹配,search()返回一個匹配對象。匹配對象中有很多的方法來描述這個匹配結果信息。如果沒有匹配到,search()返回None。你只需要關注search()函數的返回值就可以知道是否匹配成功。‘M’被正則表達式匹配到了。原因是正則表達式中的第一個可選的M匹配成功,第二個和第三個被忽略掉了。
3. ‘MM’匹配成功。因為正則表達式中的第一個和第二個可選的M匹配到,第三個被忽略。
4. ‘MMM’匹配成功。因為正則表達式中的所有三個M都匹配到。
5. ‘MMMM’匹配失敗。正則表達式中所有三個M都匹配到,接著正則表達式試圖匹配字符串結束,這個時候失敗了。因此search()函數返回None。
6. 有趣的是,空字符串也能匹配成功,因為正則表達式中的所有M都是可選的。
### 檢查百位數
? 表示匹配是可選的
百位的匹配比千位復雜。根據值的不同,會有不同的表達方式。
* `100 = C`
* `200 = CC`
* `300 = CCC`
* `400 = CD`
* `500 = D`
* `600 = DC`
* `700 = DCC`
* `800 = DCCC`
* `900 = CM`
因此會有四種可能的匹配模式:
* `CM`
* `CD`
* 可能有0到3個字符C(0個表示千位為0)。
* D緊跟在0到3個字符C的后面。
這兩個模式還可以組合起來表示:
* 一個可選的D,后面跟著0到3個字符C。
下面的例子展示了怎樣在羅馬數字中驗證百位。
```
>>> import re
<_sre.SRE_Match object at 01070390>
<_sre.SRE_Match object at 01073A50>
<_sre.SRE_Match object at 010748A8>
<_sre.SRE_Match object at 01071D98>
```
1. 這個正則表達式的寫法從上面千位的匹配方法接著往后寫。檢查字符串開始(^),然后是千位,后面才是新的部分。這里用圓括號定義了三個不同的匹配模式,他們是用豎線分隔的:CM,CD和D?C?C?C?(這表示是一個可選的D,以及緊跟的0到3個可選的字符C)。正則表達式按從左到右的順序依次匹配,如果第一個CM匹配成功,用豎線分隔這幾個中的后面其他的都會被忽略。
2. ‘MCM’匹配成功。因為第一個M匹配到,第二個和第三個M被忽略。后面的CM匹配到(因此后面的CD和D?C?C?C?根本就不被考慮匹配了)。MCM在羅馬數字中表示1900。
3. ‘MD’匹配成功。因為第一個M匹配到,第二個和第三個M被忽略。然后D?C?C?C?匹配到D(后面的三個C都是可選匹配的,都被忽略掉)。MD在羅馬數字中表示1500。
4. ‘MMMCCC’匹配成功。因為前面三個M都匹配到。后面的D?C?C?C?匹配CCC(D是可選的,它被忽略了)。MMMCCC在羅馬數字中表示3300。
5. ‘MCMC’匹配失敗。第一個M被匹配,第二個和第三個M被忽略,然后CM匹配成功。緊接著$試圖匹配字符串結束,但后面是C,匹配失敗。C也不能被D?C?C?C?匹配到,因為CM和它只能匹配其中一個,而CM已經匹配過了。
6. 有趣的是,空字符串仍然可以匹配成功。因為所有的M都是可選的,都可以被忽略。并且后面的D?C?C?C?也是這種情況。
哈哈,看看正則表達式如此快速的處理了這些令人厭惡的東西。你已經可以找到千位數和百位數了!后面的十位和個位的處理和千位、百位的處理是一樣的。但我們可以看看怎么用另一種方式來寫這個正則表達式。
## 使用語法`{n,m}`
{1,4} 匹配1到4個前面的模式
在上一節中,你處理過同樣的字符可以重復0到3次的情況。實際上,還有另一種正則表達式的書寫方式可以表達同樣的意思,而且這種表達方式更具有可讀性。首先看看我們在前面例子中使用的方法。
```
>>> import re
>>> pattern = '^M?M?M?$'
<_sre.SRE_Match object at 0x008EE090>
>>> pattern = '^M?M?M?$'
<_sre.SRE_Match object at 0x008EEB48>
>>> pattern = '^M?M?M?$'
<_sre.SRE_Match object at 0x008EE090>
>>>
```
1. 正則表達式匹配字符串開始,然后是第一個可選的字符M,但沒有第二個和第三個M(沒問題!因為他們是可選的),接著是字符串結尾。
2. 正則表達式匹配字符串開始,然后是第一個和第二個M,第三個被忽略(因為它是可選的),最后匹配字符串結尾。
3. 正則表達式匹配字符串開始,然后是三個M,接著是字符串結尾。
4. 正則表達式匹配字符串開始,然后是三個M,但匹配字符串結尾失敗(因為后面還有個M)。因此,這次匹配返回None。
```
<_sre.SRE_Match object at 0x008EEB48>
<_sre.SRE_Match object at 0x008EE090>
<_sre.SRE_Match object at 0x008EEDA8>
>>>
```
1. 這個正則表達式的意思是“匹配字符串開始,然后是任意的0到3個M字符,再是字符串結尾”。0和3的位置可以寫任意的數字。如果你想表示可以匹配的最小次數為1次,最多為3次M字符,可以寫成M{1,3}。
2. 匹配字符串開始,然后匹配了1次M,這在0到3的范圍內,接著是字符串結尾。
3. 匹配字符串開始,然后匹配了2次M,這在0到3的范圍內,接著是字符串結尾。
4. 匹配字符串開始,然后匹配了3次M,這在0到3的范圍內,接著是字符串結尾。
5. 匹配字符串開始,然后匹配了3次M,這在0到3的范圍內,但無法匹配后面的字符串結尾。正則表達式在字符串結尾之前最多允許匹配3次M,但這里有4個。因此本次匹配返回None。
### 檢查十位和個位
現在,我們繼續解釋正則表達式匹配羅馬數字中的十位和個位。下面的例子是檢查十位。
```
>>> pattern = '^M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)$'
<_sre.SRE_Match object at 0x008EEB48>
<_sre.SRE_Match object at 0x008EEB48>
<_sre.SRE_Match object at 0x008EEB48>
<_sre.SRE_Match object at 0x008EEB48>
>>>
```
1. 匹配字符串開始,然后是第一個可選的M,接著是CM,XL,以及字符串結尾。記住:(A|B|C)的意思是“只匹配A,B或者C中的一個”。你匹配了XL,因此XC和L?X?X?X?被忽略,緊接著將檢查字符串結尾。MCMXL在羅馬數字中表示1940。
2. 匹配字符串開始,然后是第一個可選的M,接著是CM。后面的L被L?X?X?X?匹配,這里忽略掉L后面所有的X。然后檢查字符串結尾。MCML在羅馬數字中表示1950。
3. 匹配字符串開始,然后是第一個可選的M,接著是CM,還有可選的L以及第一個X,跳過后面的第二個和第三個X。然后檢查字符串結尾。MCMLX表示1960。
4. 匹配字符串開始,然后是第一個可選的M,接著是CM,還有可選的L以及所有的三個X。然后是字符串結尾。MCMLXXX表示1980。
5. 匹配字符串開始,然后是第一個可選的M,接著是CM,還有可選的L以及所有的三個X。但匹配字符串結尾失敗。因為后面還有一個X。整個匹配失敗,返回None。MCMLXXXX不是一個合法的羅馬數字。
(A|B) 匹配A模式或者B模式中的一個
個位數的匹配是同樣的模式,我會告訴你細節以及最終結果。
```
>>> 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})$'
<_sre.SRE_Match object at 0x008EEB48>
<_sre.SRE_Match object at 0x008EEB48>
<_sre.SRE_Match object at 0x008EEB48>
<_sre.SRE_Match object at 0x008EEB48>
```
1. ^匹配字符串開始,然后表達式M{0,3}可以匹配0到3個的M。這里只能匹配一個M,也是可以的。接著,D?C{0,3}可以匹配一個可選的D,以及0到3個可能的C。這里我們實際只有一個D可以匹配到,正則表達式中的C全部忽略。往后,L?X{0,3}只能匹配到一個可選的L,沒有X。接著V?I{0,3}匹配到一個可選的V,沒有字符I。最后$匹配字符串結束。MDLV表示1555。
2. ^匹配字符串開始,然后匹配到2個M,D?C{0,3}匹配到可選的D,以及1個可能的C。往后,L?X{0,3}匹配到可選的L和1個X。接著V?I{0,3}匹配可選的V以及1個可選的I字符。最后匹配字符串結束。MMDCLXVI表示2666。
3. ^匹配字符串開始,然后是3個M,D?C{0,3}匹配到可選的D,以及3個C。往后,L?X{0,3}匹配可選的L和3個X。接著V?I{0,3}匹配可選的V以及3個I。最后匹配字符串結束。MMMDCCCLXXXVIII表示3888。這是你不用擴展語法寫出來的最長羅馬數字。
4. 靠近一點,(我就像一個魔術師:“靠近一點,孩子們。我要從帽子里拿出一只兔子。”)^匹配字符串開始,然后M可以不被匹配(因為是匹配0到3次),接著匹配D?C{0,3},這里跳過了可選的D,并且也沒有匹配到C,下面L?X{0,3}也一樣,跳過了L,沒有匹配X。V?I{0,3}也跳過了V,匹配了1個I。然后匹配字符串結尾。太讓人驚奇了!
如果你一次性就理解了上面所有的例子,那你會做的比我還好!現在想象一下以前的做法,在一個大程序用條件判斷和函數來處理現在正則表達式處理的內容,或者想象一下前面寫的正則表達式。我們發現,那些做法一點也不漂亮。
現在我們來研究一下怎么讓你的正則表達式更具有維護性,但表達的意思卻是相同的。
## 松散正則表達式
到目前為止,你只是處理了一些小型的正則表達式。就像你所看到的,他們難以閱讀,甚至你不能保證半年后,你還能理解這些東西,并指出他們是干什么的。所以你需要在正則表達式內部添加一些說明信息。
python允許你使用松散正字表達式來達到目的。松散正字表達式和普通緊湊的正則表達式有兩點不同:
* 空白符被忽略。空格、制表符和回車在正則表達式中并不會匹配空格、制表符、回車。如果你想在正則表達式中匹配他們,可以在前面加一個\來轉義。
* 注釋信息被忽略。松散正字表達式中的注釋和python代碼中的一樣,都是以#開頭直到行尾。它可以在多行正則表達式中增加注釋信息,這就避免了在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
'''
<_sre.SRE_Match object at 0x008EEB48>
<_sre.SRE_Match object at 0x008EEB48>
<_sre.SRE_Match object at 0x008EEB48>
```
1. 注意,如果要使用松散正則表達式,需要傳遞一個叫re.VERBOSE的參數。就像你看到的那樣,正則表達式中有很多空白符,他們都被忽略掉了。還有一些注釋信息,當然也被正則表達式忽略掉。當空白符和注釋信息被忽略掉后,這個正則表達式和上面的是完全一樣的,但是它有更高的可讀性。
2. 匹配字符串開始,然后是1個M,接著是CM,還有一個L和三個X,后面是IX,最后匹配字符串結尾。
3. 匹配字符串開始,然后是3個M,接著是D和三個C,以及三個X,一個V,三個I,最后匹配字符串結尾。
4. 這個不能匹配成功。為什么呢?因為他沒有re.VERBOSE標記。因此search()會把他們整個當成一個緊湊的正則表達式,包括里面的空白符。python不會自動檢測一個正則表達式是否是松散正則表達式,而需要明確的指定。?
## 案例研究: 解析電話號碼
\d 匹配所有0-9的數字. \D 匹配除了數字外的所有字符.
到目前為止,我們主要關注于整個表達式是否能匹配到,要么整個匹配,要么整個都不匹配。但正則表達式還有更加強大的功能。如果正則表達式成功匹配,你可以找到正則表達式中某一部分匹配到什么。
這個例子來自于我在真實世界中遇到的另一個問題。這個問題是:解析一個美國電話號碼。客戶想用自由的格式來輸入電話號碼(在單個輸入框),這需要存儲區域碼,交換碼以及后四碼(美國的電話分為區域碼、交換碼和后四碼)。我在網上搜索,發現了很多解決這個問題的正則表達式,但是它們都能不完全滿足我的要求。
下面是我要接受的電話號碼格式:
* `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`
樣式夠多的!在上面的例子中,我知道區域碼是800,交換碼是555,以及最后的后四碼是1212。如果還有分機號,那就是1234。
我們來解決這個電話號碼解析問題。下面的例子是第一步。
```
('800', '555', '1212')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute 'groups'
```
1. 我們通常從左到右的閱讀正則表達式。首先是匹配字符串開始位置,然后是(\d{3})。\d{3}表示什么意思?\d表示任意的數字(0到9),{3}表示一定要匹配3個數字。這個是你前面看到的{n,m}表示方法。把他們放在圓括號中,表示必須匹配3個數字,并且把他們記做一個組。分組的概念我們后面會說到。然后匹配一個連字符,接著匹配另外的3個數字,他們也同樣作為一個組。然后又是一個連字符,后面還要準確匹配4個數字,他們也作為一位分組。最后匹配字符串結尾。
2. 為了使用正則表達式匹配到的這些分組,需要對search()函數的返回值調用groups()方法。它會返回一個這個正則表達式中定義的所有分組結果組成的元組。在這里,我們定義了三個分組,一個三個數字,另一個是三個數字,以及一個四個數字
3. 這個正則表達式并不是最終答案。因為它還沒有處理有分機號的情況。為了處理這種情況,必須要對這個正則表達式進行擴展。
4. 這是為什么你不能在產品代碼中鏈式調用search()和groups()的原因。如果search()方法匹配不成功,也就是返回None,這就不是返回的一個正則表達式匹配對象。它沒有groups()方法,所以調用None.groups()將會拋出一個異常。(當然,在你的代碼中,這個異常很明顯。在這里我說了我的一些經驗。)
```
('800', '555', '1212', '1234')
>>>
>>>
```
1. 這個正則表達式和前面的一樣。匹配了字符串開始位置,然后是一個三個數字的分組,接著一個連字符,又是一個三個數字的分組,又是一個連字符,然后一個四個數字的分組。這三個分組匹配的內容都會被記憶下來。和上面不同的是,這里多匹配了一個連字符以及一個分組,這個分組里的內容是匹配一個或更多個數字。最后是字符串結尾。
2. 現在groups()方法返回有四個元素的元組。因為正則表達式現在定義了四個組。
3. 不幸的是,這個正則表達式仍然不是最終答案。因為它假設這些數字是有連字符分隔的。實際上還有用空格,逗號和點分隔的情況。這就需要用更加通用的解決方案來匹配這些不同的分隔符。
4. 噢,這個正則表達式不但不能做到你想要的,而且還不如上一個了!因為我們現在不能匹配沒有分機號的電話號碼。這絕對不是你想要的。如果有分機號,你希望取到,但如果沒有,你同樣也希望匹配到電話號碼其他的部分。
下面的例子展示了正則表達式中怎么處理電話號碼中各個部分之間使用了不同分隔符的情況。
```
('800', '555', '1212', '1234')
('800', '555', '1212', '1234')
>>>
>>>
```
1. 注意了!你匹配了字符串開始,然后是3個數字的分組,接著是\D+,這是什么?好吧,\D匹配除了數字以外的任意字符,+的意思是一個或多個。因此\D+匹配一個或一個以上的非數字字符。這就是你用來替換連字符的東西,它用來匹配不同的分隔符。
2. 用\D+替換-,意味著你可以匹配分隔符為空格的情況。
3. 當然,分隔符為連字符一樣可以正確工作。
4. 不幸的是,這仍然不是最終答案。因為這里我們假設有分隔符的存在,如果是根本就沒有空格或者是連字符呢?
5. 天啊,它仍然沒有解決分機號的問題。現在你有兩個問題沒有解決,但是我們可以用相同的技術來解決他們。
下面的例子展示用正則表達式處理電話號碼沒有分隔符的情況。
```
('800', '555', '1212', '1234')
('800', '555', '1212', '1234')
('800', '555', '1212', '')
>>>
```
1. 這里和上面唯一不同的地方是,把所有的+換成了*。號碼之間的分隔符不再用\D+來匹配,而是使用\D*。還記得+表示一個或更多吧?好,現在可以解析號碼之間沒有分隔符的情況了。
2. 你看,它真的可以工作。為什么呢?首先匹配字符串開始,然后是3個數字的分組(800),分組匹配的內容會被記憶下來。然后是0個非數字分隔字符,然后又是3個數字的分組(555),同樣也會被記憶下來。后面是0個非數字字符,接著是4個數字的分組(1212),然后又是0個非數字字符,還有一個任意個數字的分機號(1234)。最后匹配字符串結尾。
3. 其他字符作為分隔符一樣可以工作。這里點替代了之前的連字符,分機號的前面還可以是空格和x。
4. 最后我們解決了這個長久以來的問題:分機號是可選的。如果分機號不存在,groups()仍然可以返回一個4元素的元組,只是第四個元素為空字符串。
5. 我討厭壞消息。這還沒有結束。還有什么問題呢?在區域碼前面還可能有其他字符。但正則表達式假設區域碼在字符串的開頭。沒關系,你還可以使用0個或更多的非數字字符串來跳過區位碼前面的字符。
下面的例子展示怎么處理電話號碼前面還有其他字符的情況。
```
('800', '555', '1212', '1234')
('800', '555', '1212', '')
>>>
```
1. 現在除了在第一個分組之前要用\d*匹配0個或更多非數字字符外,這和前面的例子是相同的。注意你不會對這些非數字字符分組,因為他們不在圓括號內,也就是說不是一個組。如果發現有這些字符,這里只是跳過他們,然后開始對后面的區域碼匹配、分組。
2. 即使區位碼之前有圓括號,你也可以成功的解析電話號碼了。(右邊的圓括號已經處理,它被\D*匹配成一個非數字字符。)
3. 這只是一個全面的檢查,來確認以前能正確工作的現在仍然可以正確工作。因為首字符是可選的,因此首先匹配字符串開始,0個非數字字符,然后是三個數字并分組,接著是一個非數字字符,后面是三個數字并且分組,然后又是一個非數字分隔符,又是一個4個數字且分組,還有0個非數字字符,以及0個數字并且分組。最后匹配字符串結尾。
4. 還有問題。為什么不能匹配這個電話號碼?因為在區域碼前面還有一個1,但你假設的是區位碼前面的第一個字符是非數字字符(\d*)
我們回過頭看看。到目前為止,所有的正則表達式都匹配了字符串開始位置。但現在在字符串的開頭可能有一些你想忽略掉的不確定的字符。為了匹配到想要的數據,你需要跳過他們。我們來看看不明確匹配字符串開始的方法。
```
('800', '555', '1212', '1234')
('800', '555', '1212', '')
('800', '555', '1212', '1234')
```
1. 注意正則表達式沒有^。不會再匹配字符串開始位置了。正則表達式不會匹配整個字符串,而是試圖找到一個字符串開始匹配的位置,然后從這個位置開始匹配。
2. 現在,你可以正確的解析出字符串開頭有不需要的字符、數字或者其他分隔符的情況了。
3. 全面性檢查,同樣正常工作了。
4. 這里也仍然可以工作。
看看正則表達式失控有多快?快速回顧一下之前的例子。你能說出他們的區別嗎?
你看到了最終的答案(這就是最終答案!如果你發現還有它不能正確處理的情況,我也不想知道了 )。在你忘掉它之前,我們來把它改寫成松散正則表達式吧。
```
>>> 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)
('800', '555', '1212', '1234')
('800', '555', '1212', '')
```
1. 除了這里是用多行表示的以外,它和上面最后的那個是完全一樣的。它一樣可以處理之前的相同的情況。
2. 最后我們的全面檢查也通過。很好,你終于完成了。
## 小結
這只是正則表達式能完成的工作中的冰山一角。換句話說,盡管你可能很受打擊,相信我,你已經不是什么都不知道了。
現在,你應該已經熟悉了下面的技巧:
* `^` 匹配字符串開始位置。
* `$` 匹配字符串結束位置。
* `\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()函數來獲取到匹配的值。
正則表達式非常強大,但它也并不是解決每一個問題的正確答案。你需要更多的了解來判斷哪些情況適合使用正則表達式。某些時候它可以解決你的問題,某些時候它可能帶來更多的問題。
- 版權信息
- Chapter -1 《深入 Python 3》中有何新內容
- Chapter 0 安裝 Python
- Chapter 1 你的第一個 Python 程序
- Chapter 2 內置數據類型
- Chapter 3 解析
- Chapter 4 字符串
- Chapter 5 正則表達式
- Chapter 6 閉合 與 生成器
- Chapter 7 類 & 迭代器
- Chapter 8 高級迭代器
- Chapter 9 單元測試
- Chapter 10 重構
- Chapter 11 文件
- Chapter 12 XML
- Chapter 13 序列化Python對象
- Chapter 14 HTTP Web 服務
- Chapter 15 案例研究:將chardet移植到Python 3
- Chapter 16 打包 Python 類庫
- Chapter A 使用2to3將代碼移植到Python 3
- Chapter B 特殊方法名稱
- Chapter C 接下來閱讀什么?