ABSP第七章:[lesson25 - ?, +, *, 和 {} ,正則表達式句法以及貪婪/非貪婪匹配]

說明1

原文來自Automate the Boring Stuff with Python,原書為Creative Common License。本翻譯亦為CCL,但不得用於商業用途以及,如需轉載請註明出處。

這是本章最後一課,相當長

前一課:用python把重複性勞動自動化吧!第七章:正則表達式[lesson 24 組]

有時候你會遇到一些可選的(optional)正則條件,也就是說這一小段文字是否存在都不影響正則表達的匹配。?這個符號表示它前面的一組規則是可選的。比如想匹配policeman和policewoman,像上一次課說的,你可以用police(man|woman),或者你也可以參考下面的代碼用?

>>> batRegex = re.compile(rBat(wo)?man)>>> mo1 = batRegex.search(The Adventures of Batman)>>> mo1.group()Batman>>> mo2 = batRegex.search(The Adventures of Batwoman)>>> mo2.group()Batwoman

注意到上面的代碼中一個batRegex可以匹配Batman和Batwoman。正則表達式里的(wo)?意味著括弧里的wo是可選組。batRegex這個正則可以匹配Bat和man之間(1)不包含(0個)或(2)僅包含1個wo的文本。

回到早前電話號碼的例子。你可以用正則表達式匹配包含或不包含區號的電話號碼了。代碼如下:

>>> phoneRegex = re.compile(r(ddd-)?ddd-dddd)>>> mo1 = phoneRegex.search(My number is 415-555-4242)>>> mo1.group()415-555-4242>>> mo2 = phoneRegex.search(My number is 555-4242)>>> mo2.group()555-4242

你可以把?當做聲明:"問號前面的這個組出現1次或0次都可以匹配"

同前,如果要匹配的是?本身,請使用進行escape:?

符號*:0個或者更多,也就是任意次

*(稱作星號)在正則裡面的意思是0個或者更多個。*前面的組不出現(0次)或出現1次以及更多的次數都是可以匹配的。還是用Batman的例子來看:

>>> batRegex = re.compile(rBat(wo)*man)>>> mo1 = batRegex.search(The Adventures of Batman)>>> mo1.group()Batman>>> mo2 = batRegex.search(The Adventures of Batwoman)>>> mo2.group()Batwoman>>> mo3 = batRegex.search(The Adventures of Batwowowowoman)>>> mo3.group()Batwowowowoman

Batman被匹配是(wo)*匹配出現0次的情況,Batwoman被匹配對應的是(wo)*出現1次,而Batwowowowoman則對應的是(wo)*被匹配4次的情況。

同前,*是*號的escape,用來匹配真的*

+:一個或者更多個

*代表的是0個或者更多個,而+意味著1個或者更多個。所以如果+前面的組沒有出現在文本裡面,那這段文本就沒法被匹配。?和*都可以表示前面的組是可選的,但+不行。看一下下面的代碼:

>>> batRegex = re.compile(rBat(wo)+man)>>> mo1 = batRegex.search(The Adventures of Batwoman)>>> mo1.group()Batwoman>>> mo2 = batRegex.search(The Adventures of Batwowowowoman)>>> mo2.group()Batwowowowoman>>> mo3 = batRegex.search(The Adventures of Batman)>>> mo3 == NoneTrue

Batwoman和Batwowowowoman被匹配的原因和之前*里提到的一樣,Batman在這個代碼里無法被匹配,因為wo不是可選,而是必須的。

同前,+表示+的escape

{}表示特定次數的重複

如果你希望一組內容重複特定的次數,用+和*都沒法滿足這個要求,正確的標記是{次數}。比如,(Ha){3}表示匹配HaHaHa,HaHa不會被匹配,因為Ha的次數只有兩次。

除了在{}裡面設置單個數字,你還可以設定一個範圍,中間用逗號隔開。比如(Ha){3,5}可以匹配HaHaHa, HaHaHaHa和HaHaHaHaHa。

{3,5}裡面第一個或者第二個數字可以省略從而不限制最小或最大值。比如(Ha){3,}表示3個Ha或以上都可以,而(Ha){,5}表示5個或5個以下Ha都可以。用{}可以讓正則表達式更加簡短,比如之前的d{3}和ddd就是一個例子。

而像下面的兩個正則意義一樣,但長短明顯不同:

(Ha){3,5}

((Ha)(Ha)(Ha))|((Ha)(Ha)(Ha)(Ha))|((Ha)(Ha)(Ha)(Ha)(Ha))

試著輸入下面的代碼:

>>> haRegex = re.compile(r(Ha){3})>>> mo1 = haRegex.search(HaHaHa)>>> mo1.group()HaHaHa>>> mo2 = haRegex.search(Ha)>>> mo2 == NoneTrue

(Ha){3}匹配HaHaHa但不會匹配Ha,所以mo2是None

貪婪與非貪婪匹配

嘗試輸入下面的代碼

>>> haRegex = re.compile(r(Ha){3,5})>>> mo = haRegex.search(HaHaHaHaHa)>>> mo.group()HaHaHaHaHa

既然(Ha){3,5}可以匹配三個,四個或者五個Ha,那為什麼mo.group()返回的是5個Ha的HaHaHaHaHa而不是四個或者三個Ha呢?畢竟這兩個也是符合的啊?

Python的正則表達式默認是作為貪婪匹配的,也就是說在沒有明確指定的情況下,它會儘可能匹配最長的字元串。如果要以非貪婪模式匹配(匹配最短的情況)的話在:{}後面加一個括弧。

>>> greedyHaRegex = re.compile(r(Ha){3,5})>>> mo1 = greedyHaRegex.search(HaHaHaHaHa)>>> mo1.group()HaHaHaHaHa>>> nongreedyHaRegex = re.compile(r(Ha){3,5}?)>>> mo2 = nongreedyHaRegex.search(HaHaHaHaHa)>>> mo2.group()HaHaHa

?在前面是0個或1個的意思,但是在{}後面表示的是非貪婪匹配。這兩個是完全不同的,別弄混了。

findall()方法

除了search()方法以外,regex對象還有一個findall()方法。search()方法返回的是包含了第一個匹配內容的match對象,而findall()返回的是匹配到的所有字元串的集合。下面的代碼是search()返回match對象的例子:

>>> phoneNumRegex = re.compile(rddd-ddd-dddd)>>> mo = phoneNumRegex.search(Cell: 415-555-9999 Work: 212-555-0000)>>> mo.group()415-555-9999

而如果用findall()的話,返回的結果不是match對象,而是字元串的列表(list)--當正則表達式裡面不包含組的時候。列表裡的每一個字元串都符合正則要求的文本。試一下下面的代碼:

>>> phoneNumRegex = re.compile(rddd-ddd-dddd) # has no groups>>> phoneNumRegex.findall(Cell: 415-555-9999 Work: 212-555-0000)[415-555-9999, 212-555-0000]

如果正則裡面包含了組的話呢?findall()這時會返回由tuple組成的列表。每一個tuple代表著一個匹配所得的結果,每一組單獨成為一個元素。試一下下面的代碼,注意一下正則表達式和上面這個代碼有什麼不同:(另外也建議回頭看一看groups()的返回值)

>>> phoneNumRegex = re.compile(r(ddd)-(ddd)-(dddd)) # has groups>>> phoneNumRegex.findall(Cell: 415-555-9999 Work: 212-555-0000)[(415, 555, 9999), (212, 555, 0000)]

總結一下findall()的返回值,請記住下面的內容:

  1. 如果正則不包含組,比如d{3}-d{3}-d{4}(沒錯,{}並不是必須要跟在()後面),findall()方法返回的是匹配得來的字元串組成的列表,比如 [415-555-9999, 212-555-0000]
  2. 如果正則包含了組,比如(ddd)-(ddd)-(d ddd),那麼findall()方法返回的是由tuple組成的列表,每個tuple又是由正則的各個組匹配出的文本組成的,最後結果像下面這樣:[(415, 555, 9999), (212, 555, 0000)]

元字元

在之前關於電話號碼的例子裡面我們遇到了d這個符號,表示的是數字。也就是說d和(0|1|2|3|4|5|6|7|8|9)是等價的,當然沒人會去寫後面這種。除了d還有很多其他的元字元幫我們寫正則,如下所示:

元字元 意義d 0到9的任意數字D 不是數字的任意字元w 任何字母、數字或者下劃線W 除了字母、數字或者下劃線以外的其他字元s 空白符號,包括空格、tab和換行符號S 空白符號以外的任何字元

註:表示除……以外有特定的符號表示

除了上面的常規元字元,還有一個[ ]可以自定義元字元,具體放在後面講。

現在試一下輸入下面的代碼:

>>> xmasRegex = re.compile(rd+sw+)>>> xmasRegex.findall(12 drummers, 11 pipers, 10 lords, 9 ladies, 8 maids, 7swans, 6 geese, 5 rings, 4 birds, 3 hens, 2 doves, 1 partridge)[12 drummers, 11 pipers, 10 lords, 9 ladies, 8 maids, 7 swans, 6geese, 5 rings, 4 birds, 3 hens, 2 doves, 1 partridge]

d+sw+這個正則表達式表示1個以上的數字,1個空格,以及一串(字母,數字或者下劃線)。用findall()可以把所有匹配到的字元串放在一個列表裡返回。

用[ ]自建元字元

用默認的元字元有時候很難滿足要求(比如匹配所有的中文字元)。這個時侯可以用[ ]自建元字元。比如[aeiouAEIOU]就是所有母音,包括大小寫的集合。試一下下面的代碼:

>>> vowelRegex = re.compile(r[aeiouAEIOU])>>> vowelRegex.findall(Robocop eats baby food. BABY FOOD.)[o, o, o, e, a, a, o, o, A, O, O]

除了一個一個列舉,你還可以用-來表示範圍。比如[a-zA-Z0-9]可以匹配大小寫字母以及0到9的數字。

注意一點,在[ ]裡面你是不需要escape的。也就是說像.*?()這些符號都不用在前面加。[0-5.]匹配的就是0,1,2,3,4,5或者.,沒有必要寫成[0-5.]

前面提到表示除……以外有特定的符號,這個符號就是^。^必須緊跟著[放才表示除……以外的意思。比如我想匹配除了母音以外的所有的字元,可以用下面的代碼:

>>> consonantRegex = re.compile(r[^aeiouAEIOU])>>> consonantRegex.findall(Robocop eats baby food. BABY FOOD.)[R, b, c, p, , t, s, , b, b, y, , f, d, ., , B, B, Y, , F, D, .]

可以看到匹配的結果是除了母音以外的所有字元。

順帶一提,匹配所有中文字元可以試試這個:r[u4e00-u9fa5],其中u4e00表示一,u9fa5表示龥,在字元表裡面,所有中文字元都在這兩個字之間。

^和$符號

^符號如果不放在[之後而是放在一個正則表達式的開頭,表示的是這一個正則規則匹配的是位於字元串開頭的文本。與之相對的,把$放在正則結尾表示的是匹配的文本位於字元串結尾。這麼說有點繞,或者你可以把^和$理解成一個特殊的字元,他倆分別代表字元串的開頭和結尾。比如一個字元串是:

homeostasis at home

那麼你可以認為這個字元串實際上是:"^homeostasis at home$",所以當你的正則是^home的時候它不會匹配到結尾的home,因為這個home的前面是空格,而不是^。同理,如果你的正則式是home$,那也只會匹配到結尾的home,因為開頭的home後面緊跟著的是o,不是$。

當然,^和$可以合用,這樣的話就代表兩個符號之間的內容就是字元串的全部內容了。接下來試幾段程序來幫助理解:

r^Hello:在字元串開頭的Hello

>>> beginsWithHello = re.compile(r^Hello)>>> beginsWithHello.search(Hello world!)<_sre.SRE_Match object; span=(0, 5), match=Hello>>>> beginsWithHello.search(He said Hello.) == NoneTrue

rd$:在結尾的數字

>>> endsWithNumber = re.compile(rd$)>>> endsWithNumber.search(Your number is 42)<_sre.SRE_Match object; span=(16, 17), match=2>>>> endsWithNumber.search(42 is forty two.) == NoneTrue

r^d+$匹配全部都是數字的情況

>>> wholeStringIsNum = re.compile(r^d+$)>>> wholeStringIsNum.search(1234567890)<_sre.SRE_Match object; span=(0, 10), match=1234567890>>>> wholeStringIsNum.search(12345xyz67890) == NoneTrue>>> wholeStringIsNum.search(12 34567890) == NoneTrue

後面兩串字元串並不是只有數字(包含了xyz或者空格)所以無法被匹配。

嗯,^和$兩個符號容易弄混,所以可以試試口訣:蘿蔔(carrot, caret, ^,嗯反正尖尖的是有點像蘿蔔)要用美元($)買,看,^在$前面吧。

Wildcard元字元:.

.號在正則表達式裡面表示除了換行以外的任意字元(注意是.,*表示任意數量)。比如下面的代碼:

>>> atRegex = re.compile(r.at)>>> atRegex.findall(The cat in the hat sat on the flat mat.)[cat, hat, sat, lat, mat]

.仍舊是元字元,所以不表示數量。一個.表示一個字元,這也是為什麼flat沒有被匹配。

如果要匹配.本身,如前所述加。

[ ]裡面可以用wildcard么?用來賣萌么……

點號(.)加*匹配任何文本

有時候你會想把所有文本都匹配下來。比如你想匹配First Name:後面的所有文本直到Last Name:,然後再匹配後面的所有文本。這個時侯.*就很好用。.代表除了換行以外的任何字元,*表示任意次。

試一下下面的代碼:

>>> nameRegex = re.compile(rFirst Name: (.*) Last Name: (.*))>>> mo = nameRegex.search(First Name: Al Last Name: Sweigart)>>> mo.group(1)Al>>> mo.group(2)Sweigart

點號星號組合默認使用的是貪婪匹配,會儘可能多的匹配文本。如果要使用非貪婪模式可以在後面加上?(.*?)。和前面在{}的情況一樣,加上?可以轉化成非貪婪模式。試下下面的程序:

>>> nongreedyRegex = re.compile(r<.*?>)>>> mo = nongreedyRegex.search(<To serve man> for dinner.>)>>> mo.group()<To serve man>>>> greedyRegex = re.compile(r<.*>)>>> mo = greedyRegex.search(<To serve man> for dinner.>)>>> mo.group()<To serve man> for dinner.>

greedyRegex和nongreedyRegex都代表<>以及其中的所有文本。但是當被匹配的字元串有多個<或>的時候,非貪婪會盡量少匹配文字,而貪婪會盡量多匹配文字,所以nongreedyRegex匹配的結果是<To serve man>而greedyRegex會儘可能往後走直到最後一個>。

用.匹配換行符

前面提到.匹配的是換行符以外的所有字元。不過這也是可以調整的:在re.compile的時候,加上第二個參數:re.DOTALL。這樣一來.就可以匹配任何字元,包括換行符。試試下面的程序:

>>> noNewlineRegex = re.compile(.*)>>> noNewlineRegex.search(Serve the public trust.
Protect the innocent.nUphold the law.).group()Serve the public trust.>>> newlineRegex = re.compile(.*, re.DOTALL)>>> newlineRegex.search(Serve the public trust.
Protect the innocent.nUphold the law.).group()Serve the public trust.
Protect the innocent.
Uphold the law.

noNewLineRegex和我們之前的正則差不多,所以不會匹配
。而newlineRegex裡面除了正則本身還加上了第二個參數re.DOTALL,注意都是大寫,現在就可以匹配所有字元,包括換行。

正則符號的總結

正則符號整體來說可以分成兩類,一類表示數量,另外一類表示字元:

數量類

數量符 意義? 0次或者1次* 任意次數+ 1次或者更多次{n} n次{n,} n次及以上{,m} m次及以下{n,m} n次到m次數量符? 非貪婪

字元類

元字元 意義d 0到9的任意數字D 不是數字的任意字元w 任何字母、數字或者下劃線W 除了字母、數字或者下劃線以外的其他字元s 空白符號,包括空格、tab和換行符號S 空白符號以外的任何字元^ 字元串開頭$ 字元串結尾. 任意字元(換行以外,re.DOTALL情況下包括換行)[] 自建元字元,無需escape,-表示範圍,[abc]表示a,b或者c[^ ] 自建元字元,除……以外的所有字元,如[^abc]表示出了a,b或者c以外的所有字元

大小寫不敏感的匹配

python默認對大小寫敏感,所以下面的四個regex是不一樣的:

>>> regex1 = re.compile(Robocop)>>> regex2 = re.compile(ROBOCOP)>>> regex3 = re.compile(robOcop)>>> regex4 = re.compile(RobocOp)

但是如果你確實不想把大小寫分那麼清也可以,用re.I作為第二個參數。試一下下面的這些代碼:

>>> robocop = re.compile(rrobocop, re.I)>>> robocop.search(Robocop is part man, part machine, all cop.).group()Robocop>>> robocop.search(ROBOCOP protects the innocent.).group()ROBOCOP>>> robocop.search(Al, why does your programming book talk about robocop so much?).group()robocop

well, 如果我又想用DOTALL又想I怎麼辦?你這個磨人的小妖精……可以參考這裡:Passing in flags as an argument to re.compile

bottomline,DOTALL也好(16),I也好(2)都是數字,可以自己print(re.DOTALL)試一下。如果兩個都要怎麼辦?直接相加,用re.DOTALL+re.I作為第二個參數

推薦閱讀:

ABSP第8章: 練手項目2則

TAG:Python | 正則表達式 | 辦公自動化 |