《Python核心编程(第3版)》——1.3 正则表达式和Python语言
本節書摘來自異步社區《Python核心編程(第3版)》一書中的第1章,第1.3節,作者[美] Wesley Chun(衛斯理 春),孫波翔 李斌 李晗 譯,更多章節內容可以訪問云棲社區“異步社區”公眾號查看。
1.3 正則表達式和Python語言
在了解了關于正則表達式的全部知識后,開始查看Python當前如何通過使用re模塊來支持正則表達式,re模塊在古老的Python 1.5版中引入,用于替換那些已過時的regex模塊和regsub模塊——這兩個模塊在Python 2.5版中移除,而且此后導入這兩個模塊中的任意一個都會觸發ImportError異常。
re模塊支持更強大而且更通用的Perl風格(Perl 5風格)的正則表達式,該模塊允許多個線程共享同一個已編譯的正則表達式對象,也支持命名子組。
1.3.1 re模塊:核心函數和方法
表1-2列出了來自re模塊的更多常見函數和方法。它們中的大多數函數也與已經編譯的正則表達式對象(regex object)和正則匹配對象(regex match object)的方法同名并且具有相同的功能。本節將介紹兩個主要的函數/方法——match()和search(),以及compile()函數。下一節將介紹更多的函數,但如果想進一步了解將要介紹或者沒有介紹的更多相關信息,請查閱Python的相關文檔。
① Python 1.5.2版中新增;2.4版中增加flags參數。
② Python 2.2版中新增;2.4版中增加flags參數。
③ Python 2.7和3.1版中增加flags參數。
核心提示:編譯正則表達式(編譯還是不編譯?)在Core Python Programming或者即將出版的Core Python Language Fundamentals的執行環境章節中,介紹了Python代碼最終如何被編譯成字節碼,然后在解釋器上執行。特別是,我們指定eval()或者exec(在2.x版本中或者在3.x版本的exec()中)調用一個代碼對象而不是一個字符串,性能上會有明顯提升。這是由于對于前者而言,編譯過程不會重復執行。換句話說,使用預編譯的代碼對象比直接使用字符串要快,因為解釋器在執行字符串形式的代碼前都必須把字符串編譯成代碼對象。同樣的概念也適用于正則表達式——在模式匹配發生之前,正則表達式模式必須編譯成正則表達式對象。由于正則表達式在執行過程中將進行多次比較操作,因此強烈建議使用預編譯。而且,既然正則表達式的編譯是必需的,那么使用預編譯來提升執行性能無疑是明智之舉。re.compile()能夠提供此功能。其實模塊函數會對已編譯的對象進行緩存,所以不是所有使用相同正則表達式模式的search()和match()都需要編譯。即使這樣,你也節省了緩存查詢時間,并且不必對于相同的字符串反復進行函數調用。在不同的Python版本中,緩存中已編譯過的 正則表達式對象的數目可能不同,而且沒有文檔記錄。purge()函數能夠用于清除這些緩存。1.3.2 使用compile()函數編譯正則表達式
后續將扼要介紹的幾乎所有的re模塊函數都可以作為regex對象的方法。注意,盡管推薦預編譯,但它并不是必需的。如果需要編譯,就使用編譯過的方法;如果不需要編譯,就使用函數。幸運的是,不管使用函數還是方法,它們的名字都是相同的(也許你曾對此感到好奇,這就是模塊函數和方法的名字相同的原因,例如,search()、match()等)。因為這在大多數示例中省去一個小步驟,所以我們將使用字符串替代。我們仍將會遇到幾個預編譯代碼的對象,這樣就可以知道它的過程是怎么回事。
對于一些特別的正則表達式編譯,可選的標記可能以參數的形式給出,這些標記允許不區分大小寫的匹配,使用系統的本地化設置來匹配字母數字,等等。請參考表1-2中的條目以及在正式的官方文檔中查詢關于這些標記(re.IGNORECASE、re.MULTILINE、re.DOTALL、re.VERBOSE等)的更多信息。它們可以通過按位或操作符(|)合并。
這些標記也可以作為參數適用于大多數re模塊函數。如果想要在方法中使用這些標記,它們必須已經集成到已編譯的正則表達式對象之中,或者需要使用直接嵌入到正則表達式本身的(?F)標記,其中F是一個或者多個i(用于re.I/IGNORECASE)、m(用于re.M/MULTILINE)、s(用于re.S/DOTALL)等。如果想要同時使用多個,就把它們放在一起而不是使用按位或操作,例如,(?im)可以用于同時表示re.IGNORECASE和re.MULTILINE。
1.3.3 匹配對象以及group()和groups()方法
當處理正則表達式時,除了正則表達式對象之外,還有另一個對象類型:匹配對象。這些是成功調用match()或者search()返回的對象。匹配對象有兩個主要的方法:group()和groups()。
group()要么返回整個匹配對象,要么根據要求返回特定子組。groups()則僅返回一個包含唯一或者全部子組的元組。如果沒有子組的要求,那么當group()仍然返回整個匹配時,groups()返回一個空元組。
Python正則表達式也允許命名匹配,這部分內容超出了本節的范圍。建議讀者查閱完整的re模塊文檔,里面有這里省略掉的關于這些高級主題的詳細內容。
1.3.4 使用match()方法匹配字符串
match()是將要介紹的第一個re模塊函數和正則表達式對象(regex object)方法。match()函數試圖從字符串的起始部分對模式進行匹配。如果匹配成功,就返回一個匹配對象;如果匹配失敗,就返回None,匹配對象的group()方法能夠用于顯示那個成功的匹配。下面是如何運用match()(以及group())的一個示例:
>>> m = re.match('foo', 'foo') # 模式匹配字符串 >>> if m is not None: # 如果匹配成功,就輸出匹配內容 ... m.group() ... 'foo'模式“foo”完全匹配字符串“foo”,我們也能夠確認m是交互式解釋器中匹配對象的示例。
>>> m # 確認返回的匹配對象 <re.MatchObject instance at 80ebf48>如下為一個失敗的匹配示例,它返回None。
>>> m = re.match('foo', 'bar')# 模式并不能匹配字符串 >>> if m is not None: m.group() # (單行版本的if語句) ... >>>因為上面的匹配失敗,所以m被賦值為None,而且以此方法構建的if語句沒有指明任何操作。對于剩余的示例,如果可以,為了簡潔起見,將省去if語句塊,但在實際操作中,最好不要省去以避免 AttributeError異常(None是返回的錯誤值,該值并沒有group()屬性[方法])。
只要模式從字符串的起始部分開始匹配,即使字符串比模式長,匹配也仍然能夠成功。例如,模式“foo”將在字符串“food on the table”中找到一個匹配,因為它是從字符串的起始部分進行匹配的。
>>> m = re.match('foo', 'food on the table') # 匹配成功 >>> m.group() 'foo'可以看到,盡管字符串比模式要長,但從字符串的起始部分開始匹配就會成功。子串“foo”是從那個比較長的字符串中抽取出來的匹配部分。
甚至可以充分利用Python原生的面向對象特性,忽略保存中間過程產生的結果。
>>> re.match('foo', 'food on the table').group() 'foo'注意,在上面的一些示例中,如果匹配失敗,將會拋出AttributeError異常。
1.3.5 使用search()在一個字符串中查找模式(搜索與匹配的對比)
其實,想要搜索的模式出現在一個字符串中間部分的概率,遠大于出現在字符串起始部分的概率。這也就是search()派上用場的時候了。search()的工作方式與match()完全一致,不同之處在于search()會用它的字符串參數,在任意位置對給定正則表達式模式搜索第一次出現的匹配情況。如果搜索到成功的匹配,就會返回一個匹配對象;否則,返回None。
我們將再次舉例說明match()和search()之間的差別。以匹配一個更長的字符串為例,這次使用字符串“foo”去匹配“seafood”:
>>> m = re.match('foo', 'seafood') # 匹配失敗 >>> if m is not None: m.group() ... >>>可以看到,此處匹配失敗。match()試圖從字符串的起始部分開始匹配模式;也就是說,模式中的“f”將匹配到字符串的首字母“s”上,這樣的匹配肯定是失敗的。然而,字符串“foo”確實出現在“seafood”之中(某個位置),所以,我們該如何讓Python得出肯定的結果呢?答案是使用search()函數,而不是嘗試匹配。search()函數不但會搜索模式在字符串中第一次出現的位置,而且嚴格地對字符串從左到右搜索。
>>> m = re.search('foo', 'seafood') # 使用 search() 代替 >>> if m is not None: m.group() ... 'foo' # 搜索成功,但是匹配失敗 >>>此外,match()和search()都使用在1.3.2節中介紹的可選的標記參數。最后,需要注意的是,等價的正則表達式對象方法使用可選的pos和endpos參數來指定目標字符串的搜索范圍。
本節后面將使用match()和search()正則表達式對象方法以及group()和groups()匹配對象方法,通過展示大量的實例來說明Python中正則表達式的使用方法。我們將使用正則表達式語法中幾乎全部的特殊字符和符號。
1.3.6 匹配多個字符串
在1.2節中,我們在正則表達式bat|bet|bit中使用了擇一匹配(|)符號。如下為在Python中使用正則表達式的方法。
>>> bt = 'bat|bet|bit' # 正則表達式模式: bat、bet、bit >>> m = re.match(bt, 'bat') # 'bat' 是一個匹配 >>> if m is not None: m.group() ... 'bat' >>> m = re.match(bt, 'blt') # 對于 'blt' 沒有匹配 >>> if m is not None: m.group() ... >>> m = re.match(bt, 'He bit me!') # 不能匹配字符串 >>> if m is not None: m.group() ... >>> m = re.search(bt, 'He bit me!') # 通過搜索查找 'bit' >>> if m is not None: m.group() ... 'bit'1.3.7 匹配任何單個字符
在后續的示例中,我們展示了點號(.)不能匹配一個換行符n或者非字符,也就是說,一個空字符串。
>>> anyend = '.end' >>> m = re.match(anyend, 'bend') # 點號匹配 'b' >>> if m is not None: m.group() ... 'bend' >>> m = re.match(anyend, 'end') # 不匹配任何字符 >>> if m is not None: m.group() ... >>> m = re.match(anyend, '\nend') # 除了 \n之外的任何字符 >>> if m is not None: m.group() ... >>> m = re.search('.end', 'The end.')# 在搜索中匹配 ' ' >>> if m is not None: m.group() ... ' end'下面的示例在正則表達式中搜索一個真正的句點(小數點),而我們通過使用一個反斜線對句點的功能進行轉義:
>>> patt314 = '3.14' # 表示正則表達式的點號>>> pi_patt = '3\.14' # 表示字面量的點號 (dec. point) >>> m = re.match(pi_patt, '3.14') # 精確匹配 >>> if m is not None: m.group() ... '3.14' >>> m = re.match(patt314, '3014') # 點號匹配'0' >>> if m is not None: m.group() ... '3014' >>> m = re.match(patt314, '3.14') # 點號匹配 '.' >>> if m is not None: m.group() ... '3.14'1.3.8 創建字符集([ ])
前面詳細討論了crdp,以及它們與r2d2|c3po之間的差別。下面的示例將說明對于r2d2|c3po的限制將比crdp更為嚴格。
>>> m = re.match('[cr][23][dp][o2]', 'c3po')# 匹配 'c3po' >>> if m is not None: m.group() ... 'c3po' >>> m = re.match('[cr][23][dp][o2]', 'c2do')# 匹配 'c2do' >>> if m is not None: m.group() ... 'c2do' >>> m = re.match('r2d2|c3po', 'c2do')# 不匹配 'c2do' >>> if m is not None: m.group() ... >>> m = re.match('r2d2|c3po', 'r2d2')# 匹配 'r2d2' >>> if m is not None: m.group() ... 'r2d2'1.3.9 重復、特殊字符以及分組
正則表達式中最常見的情況包括特殊字符的使用、正則表達式模式的重復出現,以及使用圓括號對匹配模式的各部分進行分組和提取操作。我們曾看到過一個關于簡單電子郵件地址的正則表達式(w+@w+.com)?;蛟S我們想要匹配比這個正則表達式所允許的更多郵件地址。為了在域名前添加主機名稱支持,例如www.xxx.com,僅僅允許xxx.com作為整個域名,必須修改現有的正則表達式。為了表示主機名是可選的,需要創建一個模式來匹配主機名(后面跟著一個句點),使用“?”操作符來表示該模式出現零次或者一次,然后按照如下所示的方式,插入可選的正則表達式到之前的正則表達式中:w+@(w+.)?w+.com。從下面的示例中可見,該表達式允許.com前面有一個或者兩個名稱:
>>> patt = '\w+@(\w+\.)?\w+\.com' >>> re.match(patt, 'nobody@xxx.com').group() 'nobody@xxx.com' >>> re.match(patt, 'nobody@www.xxx.com').group() 'nobody@www.xxx.com'接下來,用以下模式來進一步擴展該示例,允許任意數量的中間子域名存在。請特別注意細節的變化,將“?”改為“. : w+@(w+.)w+.com”。
>>> patt = '\w+@(\w+\.)*\w+\.com' >>> re.match(patt, 'nobody@www.xxx.yyy.zzz.com').group() 'nobody@www.xxx.yyy.zzz.com'但是,我們必須要添加一個“免責聲明”,即僅僅使用字母數字字符并不能匹配組成電子郵件地址的全部可能字符。上述正則表達式不能匹配諸如xxx-yyy.com的域名或者使用非單詞W字符組成的域名。
之前討論過使用圓括號來匹配和保存子組,以便于后續處理,而不是確定一個正則表達式匹配之后,在一個單獨的子程序里面手動編碼來解析字符串。此前還特別討論過一個簡單的正則表達式模式w+-d+,它由連字符號分隔的字母數字字符串和數字組成,還討論了如何添加一個子組來構造一個新的正則表達式 (w+)-(d+)來完成這項工作。下面是初始版本的正則表達式的執行情況。
>>> m = re.match('\w\w\w-\d\d\d', 'abc-123') >>> if m is not None: m.group() ... 'abc-123'>>> m = re.match('\w\w\w-\d\d\d', 'abc-xyz') >>> if m is not None: m.group() ... >>>在上面的代碼中,創建了一個正則表達式來識別包含3個字母數字字符且后面跟著3個數字的字符串。使用abc-123測試該正則表達式,將得到正確的結果,但是使用abc-xyz則不能。現在,將修改之前討論過的正則表達式,使該正則表達式能夠提取字母數字字符串和數字。如下所示,請注意如何使用group()方法訪問每個獨立的子組以及groups()方法以獲取一個包含所有匹配子組的元組。
>>> m = re.match('(\w\w\w)-(\d\d\d)', 'abc-123') >>> m.group() # 完整匹配 'abc-123' >>> m.group(1) # 子組 1 'abc' >>> m.group(2) # 子組 2 '123' >>> m.groups() # 全部子組 ('abc', '123')由以上腳本內容可見,group()通常用于以普通方式顯示所有的匹配部分,但也能用于獲取各個匹配的子組??梢允褂胓roups()方法來獲取一個包含所有匹配子字符串的元組。
如下為一個簡單的示例,該示例展示了不同的分組排列,這將使整個事情變得更加清晰。
>>> m = re.match('ab', 'ab') # 沒有子組 >>> m.group() # 完整匹配 'ab' >>> m.groups() # 所有子組 () >>> >>> m = re.match('(ab)', 'ab') # 一個子組 >>> m.group() # 完整匹配 'ab' >>> m.group(1) # 子組 1 'ab' >>> m.groups() # 全部子組 ('ab',) >>> >>> m = re.match('(a)(b)', 'ab') # 兩個子組 >>> m.group() # 完整匹配 'ab' >>> m.group(1) # 子組 1 'a' >>> m.group(2) # 子組 2 'b' >>> m.groups() # 所有子組 ('a', 'b') >>> >>> m = re.match('(a(b))', 'ab') # 兩個子組 >>> m.group() # 完整匹配 'ab' >>> m.group(1) # 子組 1 'ab' >>> m.group(2) # 子組 2 'b' >>> m.groups() # 所有子組 ('ab', 'b')1.3.10 匹配字符串的起始和結尾以及單詞邊界
如下示例突出顯示表示位置的正則表達式操作符。該操作符更多用于表示搜索而不是匹配,因為match()總是從字符串開始位置進行匹配。
>>> m = re.search('^The', 'The end.') # 匹配 >>> if m is not None: m.group() ... 'The' >>> m = re.search('^The', 'end. The') # 不作為起始 >>> if m is not None: m.group() ... >>> m = re.search(r'\bthe', 'bite the dog') # 在邊界 >>> if m is not None: m.group() ... 'the' >>> m = re.search(r'\bthe', 'bitethe dog') # 有邊界 >>> if m is not None: m.group() ... >>> m = re.search(r'\Bthe', 'bitethe dog') # 沒有邊界 >>> if m is not None: m.group() ... 'the'讀者將注意到此處出現的原始字符串。你可能想要查看本章末尾部分的核心提示“Python中原始字符串的用法”(Using Python raw strings),里面提到了在此處使用它們的原因。通常情況下,在正則表達式中使用原始字符串是個好主意。
讀者還應當注意其他4個re模塊函數和正則表達式對象方法:findall()、sub()、subn()和split()。
1.3.11 使用findall()和finditer()查找每一次出現的位置
findall()查詢字符串中某個正則表達式模式全部的非重復出現情況。這與search()在執行字符串搜索時類似,但與match()和search()的不同之處在于,findall()總是返回一個列表。如果findall()沒有找到匹配的部分,就返回一個空列表,但如果匹配成功,列表將包含所有成功的匹配部分(從左向右按出現順序排列)。
>>> re.findall('car', 'car') ['car'] >>> re.findall('car', 'scary') ['car'] >>> re.findall('car', 'carry the barcardi to the car') ['car', 'car', 'car']子組在一個更復雜的返回列表中搜索結果,而且這樣做是有意義的,因為子組是允許從單個正則表達式中抽取特定模式的一種機制,例如匹配一個完整電話號碼中的一部分(例如區號),或者完整電子郵件地址的一部分(例如登錄名稱)。
對于一個成功的匹配,每個子組匹配是由findall()返回的結果列表中的單一元素;對于多個成功的匹配,每個子組匹配是返回的一個元組中的單一元素,而且每個元組(每個元組都對應一個成功的匹配)是結果列表中的元素。這部分內容可能第一次聽起來令人迷惑,但是如果你嘗試練習過一些不同的示例,就將澄清很多知識點。
finditer()函數是在Python 2.2版本中添加回來的,這是一個與findall()函數類似但是更節省內存的變體。兩者之間以及和其他變體函數之間的差異(很明顯不同于返回的是一個迭代器還是列表)在于,和返回的匹配字符串相比,finditer()在匹配對象中迭代。如下是在單個字符串中兩個不同分組之間的差別。
>>> s = 'This and that.' >>> re.findall(r'(th\w+) and (th\w+)', s, re.I) [('This', 'that')] >>> re.finditer(r'(th\w+) and (th\w+)', s, ... re.I).next().groups() ('This', 'that') >>> re.finditer(r'(th\w+) and (th\w+)', s, ... re.I).next().group(1) 'This' >>> re.finditer(r'(th\w+) and (th\w+)', s, ... re.I).next().group(2) 'that' >>> [g.groups() for g in re.finditer(r'(th\w+) and (th\w+)', ... s, re.I)] [('This', 'that')]在下面的示例中,我們將在單個字符串中執行單個分組的多重匹配。
>>> re.findall(r'(th\w+)', s, re.I) ['This', 'that'] >>> it = re.finditer(r'(th\w+)', s, re.I) >>> g = it.next() >>> g.groups() ('This',) >>> g.group(1) 'This' >>> g = it.next() >>> g.groups() ('that',) >>> g.group(1) 'that' >>> [g.group(1) for g in re.finditer(r'(th\w+)', s, re.I)] ['This', 'that']注意,使用finditer()函數完成的所有額外工作都旨在獲取它的輸出來匹配findall()的輸出。
最后,與match()和search()類似,findall()和finditer()方法的版本支持可選的pos和endpos參數,這兩個參數用于控制目標字符串的搜索邊界,這與本章之前的部分所描述的類似。
1.3.12 使用sub()和subn()搜索與替換
有兩個函數/方法用于實現搜索和替換功能:sub()和subn()。兩者幾乎一樣,都是將某字符串中所有匹配正則表達式的部分進行某種形式的替換。用來替換的部分通常是一個字符串,但它也可能是一個函數,該函數返回一個用來替換的字符串。subn()和sub()一樣,但subn()還返回一個表示替換的總數,替換后的字符串和表示替換總數的數字一起作為一個擁有兩個元素的元組返回。
>>> re.sub('X', 'Mr. Smith', 'attn: X\n\nDear X,\n') 'attn: Mr. Smith\012\012Dear Mr. Smith,\012' >>> >>> re.subn('X', 'Mr. Smith', 'attn: X\n\nDear X,\n') ('attn: Mr. Smith\012\012Dear Mr. Smith,\012', 2) >>> >>> print re.sub('X', 'Mr. Smith', 'attn: X\n\nDear X,\n') attn: Mr. SmithDear Mr. Smith,>>> re.sub('[ae]', 'X', 'abcdef') 'XbcdXf' >>> re.subn('[ae]', 'X', 'abcdef') ('XbcdXf', 2)前面講到,使用匹配對象的group()方法除了能夠取出匹配分組編號外,還可以使用N,其中N是在替換字符串中使用的分組編號。下面的代碼僅僅只是將美式的日期表示法MM/DD/YY{,YY}格式轉換為其他國家常用的格式DD/MM/YY{,YY}。
>>> re.sub(r'(\d{1,2})/(\d{1,2})/(\d{2}|\d{4})', ... r'\2/\1/\3', '2/20/91') # Yes, Python is... '20/2/91' >>> re.sub(r'(\d{1,2})/(\d{1,2})/(\d{2}|\d{4})', ... r'\2/\1/\3', '2/20/1991') # ... 20+ years old! '20/2/1991'1.3.13 在限定模式上使用split()分隔字符串
re模塊和正則表達式的對象方法split()對于相對應字符串的工作方式是類似的,但是與分割一個固定字符串相比,它們基于正則表達式的模式分隔字符串,為字符串分隔功能添加一些額外的威力。如果你不想為每次模式的出現都分割字符串,就可以通過為max參數設定一個值(非零)來指定最大分割數。
如果給定分隔符不是使用特殊符號來匹配多重模式的正則表達式,那么re.split()與str.split()的工作方式相同,如下所示(基于單引號分割)。
>>> re.split(':', 'str1:str2:str3') ['str1', 'str2', 'str3']這是一個簡單的示例。如果有一個更復雜的示例,例如,一個用于Web站點(類似于Google或者Yahoo! Maps)的簡單解析器,該如何實現?用戶需要輸入城市和州名,或者城市名加上ZIP編碼,還是三者同時輸入?這就需要比僅僅是普通字符串分割更強大的處理方式,具體如下。
>>> import re >>> DATA = ( ... 'Mountain View, CA 94040', ... 'Sunnyvale, CA', ... 'Los Altos, 94023', ... 'Cupertino 95014', ... 'Palo Alto CA', ... ) >>> for datum in DATA: ... print re.split(', |(?= (?:\d{5}|[A-Z]{2})) ', datum) ... ['Mountain View', 'CA', '94040'] ['Sunnyvale', 'CA'] ['Los Altos', '94023'] ['Cupertino', '95014'] ['Palo Alto', 'CA']上述正則表達式擁有一個簡單的組件:使用split語句基于逗號分割字符串。更難的部分是最后的正則表達式,可以通過該正則表達式預覽一些將在下一小節中介紹的擴展符號。在普通的英文中,通常這樣說:如果空格緊跟在五個數字(ZIP編碼)或者兩個大寫字母(美國聯邦州縮寫)之后,就用split語句分割該空格。這就允許我們在城市名中放置空格。
通常情況下,這僅僅只是一個簡單的正則表達式,可以在用來解析位置信息的應用中作為起點。該正則表達式并不能處理小寫的州名或者州名的全拼、街道地址、州編碼、ZIP+4(9位ZIP編碼)、經緯度、多個空格等內容(或者在處理時會失敗)。這僅僅意味著使用re.split()能夠實現str.split()不能實現的一個簡單的演示實例。
我們剛剛已經證實,讀者將從正則表達式split語句的強大能力中獲益;然而,記得一定在編碼過程中選擇更合適的工具。如果對字符串使用split方法已經足夠好,就不需要引入額外復雜并且影響性能的正則表達式。
1.3.14 擴展符號
Python的正則表達式支持大量的擴展符號。讓我們一起查看它們中的一些內容,然后展示一些有用的示例。
通過使用 (?iLmsux) 系列選項,用戶可以直接在正則表達式里面指定一個或者多個標記,而不是通過compile()或者其他re模塊函數。下面為一些使用re.I/IGNORECASE的示例,最后一個示例在re.M/MULTILINE實現多行混合:
>>> re.findall(r'(?i)yes', 'yes? Yes. YES!!') ['yes', 'Yes', 'YES'] >>> re.findall(r'(?i)th\w+', 'The quickest way is through this tunnel.') ['The', 'through', 'this'] >>> re.findall(r'(?im)(^th[\w ]+)', """ ... This line is the first, ... another line, ... that line, it's the best ... """) ['This line is the first', 'that line']在前兩個示例中,顯然是不區分大小寫的。在最后一個示例中,通過使用“多行”,能夠在目標字符串中實現跨行搜索,而不必將整個字符串視為單個實體。注意,此時忽略了實例“the”,因為它們并不出現在各自的行首。
下一組演示使用re.S/DOTALL。該標記表明點號(.)能夠用來表示n符號(反之其通常用于表示除了n之外的全部字符):
>>> re.findall(r'th.+', ''' ... The first line ... the second line ... the third line ... ''') ['the second line', 'the third line'] >>> re.findall(r'(?s)th.+', ''' ... The first line ... the second line ... the third line ... ''') ['the second line\nthe third line\n']re.X/VERBOSE標記非常有趣;該標記允許用戶通過抑制在正則表達式中使用空白符(除了在字符類中或者在反斜線轉義中)來創建更易讀的正則表達式。此外,散列、注釋和井號也可以用于一個注釋的起始,只要它們不在一個用反斜線轉義的字符類中。
>>> re.search(r'''(?x) ... \((\d{3})\) # 區號 ... [ ] # 空白符 ... (\d{3}) # 前綴 ... - # 橫線 ... (\d{4}) # 終點數字 ... ''', '(800) 555-1212').groups() ('800', '555', '1212')(?:…)符號將更流行;通過使用該符號,可以對部分正則表達式進行分組,但是并不會保存該分組用于后續的檢索或者應用。當不想保存今后永遠不會使用的多余匹配時,這個符號就非常有用。
>>> re.findall(r'http://(?:\w+\.)*(\w+\.com)',... 'http://google.com http://www.google.com http://code.google.com')['google.com', 'google.com', 'google.com']>>> re.search(r'\((?P<areacode>\d{3})\) (?P<prefix>\d{3})-(?:\d{4})',... '(800) 555-1212').groupdict() {'areacode': '800', 'prefix': '555'}讀者可以同時一起使用 (?P) 和 (?P=name)符號。前者通過使用一個名稱標識符而不是使用從1開始增加到N的增量數字來保存匹配,如果使用數字來保存匹配結果,我們就可以通過使用1,2 ...,N 來檢索。如下所示,可以使用一個類似風格的g來檢索它們。
>>> re.sub(r'\((?P<areacode>\d{3})\) (?P<prefix>\d{3})-(?:\d{4})',... '(\g<areacode>) \g<prefix>-xxxx', '(800) 555-1212') '(800) 555-xxxx'使用后者,可以在一個相同的正則表達式中重用模式,而不必稍后再次在(相同)正則表達式中指定相同的模式。例如,在本示例中,假定讓讀者驗證一些電話號碼的規范化。如下所示為一個丑陋并且壓縮的版本,后面跟著一個正確使用的 (?x),使代碼變得稍許易讀。
>>> bool(re.match(r'\((?P<areacode>\d{3})\) (?P<prefix>\d{3})- (?P<number>\d{4}) (?P=areacode)-(?P=prefix)-(?P=number) 1(?P=areacode)(?P=prefix)(?P=number)', ... '(800) 555-1212 800-555-1212 18005551212')) True >>> bool(re.match(r'''(?x) ... ... # match (800) 555-1212, save areacode, prefix, no. ... \((?P<areacode>\d{3})\)[ ](?P<prefix>\d{3})-(?P<number>\d{4}) ... ... # space ... [ ] ... ... # match 800-555-1212 ... (?P=areacode)-(?P=prefix)-(?P=number) ... ... # space ... [ ] ... ... # match 18005551212 ... 1(?P=areacode)(?P=prefix)(?P=number) ... ... ''', '(800) 555-1212 800-555-1212 18005551212')) True讀者可以使用 (?=...) 和 (?!…)符號在目標字符串中實現一個前視匹配,而不必實際上使用這些字符串。前者是正向前視斷言,后者是負向前視斷言。在后面的示例中,我們僅僅對姓氏為“van Rossum”的人的名字感興趣,下一個示例中,讓我們忽略以“noreply”或者“postmaster”開頭的e-mail地址。
第三個代碼片段用于演示findall()和finditer()的區別;我們使用后者來構建一個使用相同登錄名但不同域名的e-mail地址列表(在一個更易于記憶的方法中,通過忽略創建用完即丟棄的中間列表)。
>>> re.findall(r'\w+(?= van Rossum)', ... ''' ... Guido van Rossum ... Tim Peters ... Alex Martelli ... Just van Rossum ... Raymond Hettinger ... ''') ['Guido', 'Just'] >>> re.findall(r'(?m)^\s+(?!noreply|postmaster)(\w+)', ... ''' ... sales@phptr.com ... postmaster@phptr.com ... eng@phptr.com ... noreply@phptr.com ... admin@phptr.com ... ''') ['sales', 'eng', 'admin'] >>> ['%s@aw.com' % e.group(1) for e in \ re.finditer(r'(?m)^\s+(?!noreply|postmaster)(\w+)', ... ''' ... sales@phptr.com ... postmaster@phptr.com ... eng@phptr.com ... noreply@phptr.com ... admin@phptr.com ... ''')] ['sales@aw.com', 'eng@aw.com', 'admin@aw.com']最后一個示例展示了使用條件正則表達式匹配。假定我們擁有另一個特殊字符,它僅僅包含字母“x”和“y”,我們此時僅僅想要這樣限定字符串:兩字母的字符串必須由一個字母跟著另一個字母。換句話說,你不能同時擁有兩個相同的字母;要么由“x”跟著“y”,要么相反。
>>> bool(re.search(r'(?:(x)|y)(?(1)y|x)', 'xy')) True >>> bool(re.search(r'(?:(x)|y)(?(1)y|x)', 'xx')) False1.3.15 雜項
可能讀者會對于正則表達式的特殊字符和特殊ASCII符號之間的差異感到迷惑。我們可以使用n表示一個換行符,但是我們可以使用d在正則表達式中表示匹配單個數字。
如果有符號同時用于ASCII和正則表達式,就會發生問題,因此在下面的核心提示中,建議使用Python的原始字符串來避免產生問題。另一個警告是:w和W字母數字字符集同時受re.L/LOCALE和Unicode(re.U/UNICODE)標記所影響。
核心提示:使用Python原始字符串讀者可能在之前的一些示例中見過原始字符串的使用。正則表達式對于探索原始字符串有著強大的動力,原因就在于ASCII字符和正則表達式的特殊字符之間存在沖突。作為一個特殊符號,\b表示ASCII字符的退格符,但是\b同時也是一個正則表達式的特殊符號,表示匹配一個單詞的邊界。對于正則表達式編譯器而言,若它把兩個\b視為字符串內容而不是單個退格符,就需要在字符串中再使用一個反斜線轉義反斜線,就像這樣:\\b。這樣顯得略微雜亂,特別是如果在字符串中擁有很多特殊字符,就會讓人感到更加困惑。我們在Core Python Programming或者Core Python Language Fundamentals的Sequence章節中介紹了原始字符串,而且該原始字符串可以用于(且經常用于)幫助保持正則表達式查找某些可托管的東西。事實上,很多Python程序員總是抱怨這個方法,僅僅用原始字符串來定義正則表達式。如下所示的一些示例用于說明退格符\b和正則表達式\b之間的差異,它們有的使用、有的不使用原始字符串。>>> m = re.match('\bblow', 'blow') # backspace、no match >> if m: m.group() ... >> m = re.match('\\bblow', 'blow') # escaped\,now it works >> if m: m.group() ... 'blow' >> m = re.match(r'\bblow', 'blow') # use raw string instead >> if m: m.group() ... 'blow'讀者可能回想起來我們在正則表達式中使用\d而沒有使用原始字符串時并未遇到問題,這是因為ASCII中沒有相應的特殊字符,所以正則表達式的編譯器知道你想要表示十進制數字。 創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的《Python核心编程(第3版)》——1.3 正则表达式和Python语言的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 我往手里面植入了一枚芯片,但并没有获得超
- 下一篇: ubuntu 安装deb程序文件失败的解