天天看點

《Python核心程式設計(第3版)》——1.3 正規表達式和Python語言

本節書摘來自異步社群《python核心程式設計(第3版)》一書中的第1章,第1.3節,作者[美] wesley chun(衛斯理 春),孫波翔 李斌 李晗 譯,更多章節内容可以通路雲栖社群“異步社群”公衆号檢視。

在了解了關于正規表達式的全部知識後,開始檢視python目前如何通過使用re子產品來支援正規表達式,re子產品在古老的python 1.5版中引入,用于替換那些已過時的regex子產品和regsub子產品——這兩個子產品在python 2.5版中移除,而且此後導入這兩個子產品中的任意一個都會觸發importerror異常。

《Python核心程式設計(第3版)》——1.3 正規表達式和Python語言

re子產品支援更強大而且更通用的perl風格(perl 5風格)的正規表達式,該子產品允許多個線程共享同一個已編譯的正規表達式對象,也支援命名子組。

表1-2列出了來自re子產品的更多常見函數和方法。它們中的大多數函數也與已經編譯的正規表達式對象(regex object)和正則比對對象(regex match object)的方法同名并且具有相同的功能。本節将介紹兩個主要的函數/方法——match()和search(),以及compile()函數。下一節将介紹更多的函數,但如果想進一步了解将要介紹或者沒有介紹的更多相關資訊,請查閱python的相關文檔。

《Python核心程式設計(第3版)》——1.3 正規表達式和Python語言
《Python核心程式設計(第3版)》——1.3 正規表達式和Python語言
《Python核心程式設計(第3版)》——1.3 正規表達式和Python語言

① python 1.5.2版中新增;2.4版中增加flags參數。

② python 2.2版中新增;2.4版中增加flags參數。

③ python 2.7和3.1版中增加flags參數。

後續将扼要介紹的幾乎所有的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。

當處理正規表達式時,除了正規表達式對象之外,還有另一個對象類型:比對對象。這些是成功調用match()或者search()傳回的對象。比對對象有兩個主要的方法:group()和groups()。

group()要麼傳回整個比對對象,要麼根據要求傳回特定子組。groups()則僅傳回一個包含唯一或者全部子組的元組。如果沒有子組的要求,那麼當group()仍然傳回整個比對時,groups()傳回一個空元組。

python正規表達式也允許命名比對,這部分内容超出了本節的範圍。建議讀者查閱完整的re子產品文檔,裡面有這裡省略掉的關于這些進階主題的詳細内容。

match()是将要介紹的第一個re子產品函數和正規表達式對象(regex object)方法。match()函數試圖從字元串的起始部分對模式進行比對。如果比對成功,就傳回一個比對對象;如果比對失敗,就傳回none,比對對象的group()方法能夠用于顯示那個成功的比對。下面是如何運用match()(以及group())的一個示例:

模式“foo”完全比對字元串“foo”,我們也能夠确認m是互動式解釋器中比對對象的示例。

如下為一個失敗的比對示例,它傳回none。

因為上面的比對失敗,是以m被指派為none,而且以此方法建構的if語句沒有指明任何操作。對于剩餘的示例,如果可以,為了簡潔起見,将省去if語句塊,但在實際操作中,最好不要省去以避免 attributeerror異常(none是傳回的錯誤值,該值并沒有group()屬性[方法])。

隻要模式從字元串的起始部分開始比對,即使字元串比模式長,比對也仍然能夠成功。例如,模式“foo”将在字元串“food on the table”中找到一個比對,因為它是從字元串的起始部分進行比對的。

可以看到,盡管字元串比模式要長,但從字元串的起始部分開始比對就會成功。子串“foo”是從那個比較長的字元串中抽取出來的比對部分。

甚至可以充分利用python原生的面向對象特性,忽略儲存中間過程産生的結果。

注意,在上面的一些示例中,如果比對失敗,将會抛出attributeerror異常。

其實,想要搜尋的模式出現在一個字元串中間部分的機率,遠大于出現在字元串起始部分的機率。這也就是search()派上用場的時候了。search()的工作方式與match()完全一緻,不同之處在于search()會用它的字元串參數,在任意位置對給定正規表達式模式搜尋第一次出現的比對情況。如果搜尋到成功的比對,就會傳回一個比對對象;否則,傳回none。

我們将再次舉例說明match()和search()之間的差别。以比對一個更長的字元串為例,這次使用字元串“foo”去比對“seafood”:

可以看到,此處比對失敗。match()試圖從字元串的起始部分開始比對模式;也就是說,模式中的“f”将比對到字元串的首字母“s”上,這樣的比對肯定是失敗的。然而,字元串“foo”确實出現在“seafood”之中(某個位置),是以,我們該如何讓python得出肯定的結果呢?答案是使用search()函數,而不是嘗試比對。search()函數不但會搜尋模式在字元串中第一次出現的位置,而且嚴格地對字元串從左到右搜尋。

此外,match()和search()都使用在1.3.2節中介紹的可選的标記參數。最後,需要注意的是,等價的正規表達式對象方法使用可選的pos和endpos參數來指定目标字元串的搜尋範圍。

本節後面将使用match()和search()正規表達式對象方法以及group()和groups()比對對象方法,通過展示大量的執行個體來說明python中正規表達式的使用方法。我們将使用正規表達式文法中幾乎全部的特殊字元和符号。

在1.2節中,我們在正規表達式bat|bet|bit中使用了擇一比對(|)符号。如下為在python中使用正規表達式的方法。

在後續的示例中,我們展示了點号(.)不能比對一個換行符n或者非字元,也就是說,一個空字元串。

下面的示例在正規表達式中搜尋一個真正的句點(小數點),而我們通過使用一個反斜線對句點的功能進行轉義:

前面詳細讨論了crdp,以及它們與r2d2|c3po之間的差别。下面的示例将說明對于r2d2|c3po的限制将比crdp更為嚴格。

正規表達式中最常見的情況包括特殊字元的使用、正規表達式模式的重複出現,以及使用圓括号對比對模式的各部分進行分組和提取操作。我們曾看到過一個關于簡單電子郵件位址的正規表達式(w+@w+.com)。或許我們想要比對比這個正規表達式所允許的更多郵件位址。為了在域名前添加主機名稱支援,例如www.xxx.com,僅僅允許xxx.com作為整個域名,必須修改現有的正規表達式。為了表示主機名是可選的,需要建立一個模式來比對主機名(後面跟着一個句點),使用“?”操作符來表示該模式出現零次或者一次,然後按照如下所示的方式,插入可選的正規表達式到之前的正規表達式中:w+@(w+.)?w+.com。從下面的示例中可見,該表達式允許.com前面有一個或者兩個名稱:

接下來,用以下模式來進一步擴充該示例,允許任意數量的中間子域名存在。請特别注意細節的變化,将“?”改為“. : w+@(w+.)w+.com”。

但是,我們必須要添加一個“免責聲明”,即僅僅使用字母數字字元并不能比對組成電子郵件位址的全部可能字元。上述正規表達式不能比對諸如xxx-yyy.com的域名或者使用非單詞w字元組成的域名。

之前讨論過使用圓括号來比對和儲存子組,以便于後續處理,而不是确定一個正規表達式比對之後,在一個單獨的子程式裡面手動編碼來解析字元串。此前還特别讨論過一個簡單的正規表達式模式w+-d+,它由連字元号分隔的字母數字字元串和數字組成,還讨論了如何添加一個子組來構造一個新的正規表達式 (w+)-(d+)來完成這項工作。下面是初始版本的正規表達式的執行情況。

在上面的代碼中,建立了一個正規表達式來識别包含3個字母數字字元且後面跟着3個數字的字元串。使用abc-123測試該正規表達式,将得到正确的結果,但是使用abc-xyz則不能。現在,将修改之前讨論過的正規表達式,使該正規表達式能夠提取字母數字字元串和數字。如下所示,請注意如何使用group()方法通路每個獨立的子組以及groups()方法以擷取一個包含所有比對子組的元組。

由以上腳本内容可見,group()通常用于以普通方式顯示所有的比對部分,但也能用于擷取各個比對的子組。可以使用groups()方法來擷取一個包含所有比對子字元串的元組。

如下為一個簡單的示例,該示例展示了不同的分組排列,這将使整個事情變得更加清晰。

如下示例突出顯示表示位置的正規表達式操作符。該操作符更多用于表示搜尋而不是比對,因為match()總是從字元串開始位置進行比對。

讀者将注意到此處出現的原始字元串。你可能想要檢視本章末尾部分的核心提示“python中原始字元串的用法”(using python raw strings),裡面提到了在此處使用它們的原因。通常情況下,在正規表達式中使用原始字元串是個好主意。

讀者還應當注意其他4個re子產品函數和正規表達式對象方法:findall()、sub()、subn()和split()。

findall()查詢字元串中某個正規表達式模式全部的非重複出現情況。這與search()在執行字元串搜尋時類似,但與match()和search()的不同之處在于,findall()總是傳回一個清單。如果findall()沒有找到比對的部分,就傳回一個空清單,但如果比對成功,清單将包含所有成功的比對部分(從左向右按出現順序排列)。

子組在一個更複雜的傳回清單中搜尋結果,而且這樣做是有意義的,因為子組是允許從單個正規表達式中抽取特定模式的一種機制,例如比對一個完整電話号碼中的一部分(例如區号),或者完整電子郵件位址的一部分(例如登入名稱)。

對于一個成功的比對,每個子組比對是由findall()傳回的結果清單中的單一進制素;對于多個成功的比對,每個子組比對是傳回的一個元組中的單一進制素,而且每個元組(每個元組都對應一個成功的比對)是結果清單中的元素。這部分内容可能第一次聽起來令人迷惑,但是如果你嘗試練習過一些不同的示例,就将澄清很多知識點。

《Python核心程式設計(第3版)》——1.3 正規表達式和Python語言

finditer()函數是在python 2.2版本中添加回來的,這是一個與findall()函數類似但是更節省記憶體的變體。兩者之間以及和其他變體函數之間的差異(很明顯不同于傳回的是一個疊代器還是清單)在于,和傳回的比對字元串相比,finditer()在比對對象中疊代。如下是在單個字元串中兩個不同分組之間的差别。

在下面的示例中,我們将在單個字元串中執行單個分組的多重比對。

注意,使用finditer()函數完成的所有額外工作都旨在擷取它的輸出來比對findall()的輸出。

最後,與match()和search()類似,findall()和finditer()方法的版本支援可選的pos和endpos參數,這兩個參數用于控制目标字元串的搜尋邊界,這與本章之前的部分所描述的類似。

有兩個函數/方法用于實作搜尋和替換功能:sub()和subn()。兩者幾乎一樣,都是将某字元串中所有比對正規表達式的部分進行某種形式的替換。用來替換的部分通常是一個字元串,但它也可能是一個函數,該函數傳回一個用來替換的字元串。subn()和sub()一樣,但subn()還傳回一個表示替換的總數,替換後的字元串和表示替換總數的數字一起作為一個擁有兩個元素的元組傳回。

前面講到,使用比對對象的group()方法除了能夠取出比對分組編号外,還可以使用n,其中n是在替換字元串中使用的分組編号。下面的代碼僅僅隻是将美式的日期表示法mm/dd/yy{,yy}格式轉換為其他國家常用的格式dd/mm/yy{,yy}。

re子產品和正規表達式的對象方法split()對于相對應字元串的工作方式是類似的,但是與分割一個固定字元串相比,它們基于正規表達式的模式分隔字元串,為字元串分隔功能添加一些額外的威力。如果你不想為每次模式的出現都分割字元串,就可以通過為max參數設定一個值(非零)來指定最大分割數。

如果給定分隔符不是使用特殊符号來比對多重模式的正規表達式,那麼re.split()與str.split()的工作方式相同,如下所示(基于單引号分割)。

這是一個簡單的示例。如果有一個更複雜的示例,例如,一個用于web站點(類似于google或者yahoo! maps)的簡單解析器,該如何實作?使用者需要輸入城市和州名,或者城市名加上zip編碼,還是三者同時輸入?這就需要比僅僅是普通字元串分割更強大的處理方式,具體如下。

上述正規表達式擁有一個簡單的元件:使用split語句基于逗号分割字元串。更難的部分是最後的正規表達式,可以通過該正規表達式預覽一些将在下一小節中介紹的擴充符号。在普通的英文中,通常這樣說:如果空格緊跟在五個數字(zip編碼)或者兩個大寫字母(美國聯邦州縮寫)之後,就用split語句分割該空格。這就允許我們在城市名中放置空格。

通常情況下,這僅僅隻是一個簡單的正規表達式,可以在用來解析位置資訊的應用中作為起點。該正規表達式并不能處理小寫的州名或者州名的全拼、街道位址、州編碼、zip+4(9位zip編碼)、經緯度、多個空格等内容(或者在處理時會失敗)。這僅僅意味着使用re.split()能夠實作str.split()不能實作的一個簡單的示範執行個體。

我們剛剛已經證明,讀者将從正規表達式split語句的強大能力中獲益;然而,記得一定在編碼過程中選擇更合适的工具。如果對字元串使用split方法已經足夠好,就不需要引入額外複雜并且影響性能的正規表達式。

python的正規表達式支援大量的擴充符号。讓我們一起檢視它們中的一些内容,然後展示一些有用的示例。

通過使用 (?ilmsux) 系列選項,使用者可以直接在正規表達式裡面指定一個或者多個标記,而不是通過compile()或者其他re子產品函數。下面為一些使用re.i/ignorecase的示例,最後一個示例在re.m/multiline實作多行混合:

在前兩個示例中,顯然是不區分大小寫的。在最後一個示例中,通過使用“多行”,能夠在目标字元串中實作跨行搜尋,而不必将整個字元串視為單個實體。注意,此時忽略了執行個體“the”,因為它們并不出現在各自的行首。

下一組示範使用re.s/dotall。該标記表明點号(.)能夠用來表示n符号(反之其通常用于表示除了n之外的全部字元):

re.x/verbose标記非常有趣;該标記允許使用者通過抑制在正規表達式中使用空白符(除了在字元類中或者在反斜線轉義中)來建立更易讀的正規表達式。此外,散列、注釋和井号也可以用于一個注釋的起始,隻要它們不在一個用反斜線轉義的字元類中。

(?:…)符号将更流行;通過使用該符号,可以對部分正規表達式進行分組,但是并不會儲存該分組用于後續的檢索或者應用。當不想儲存今後永遠不會使用的多餘比對時,這個符号就非常有用。

讀者可以同時一起使用 (?p) 和 (?p=name)符号。前者通過使用一個名稱辨別符而不是使用從1開始增加到n的增量數字來儲存比對,如果使用數字來儲存比對結果,我們就可以通過使用1,2 ...,n 來檢索。如下所示,可以使用一個類似風格的g來檢索它們。

使用後者,可以在一個相同的正規表達式中重用模式,而不必稍後再次在(相同)正規表達式中指定相同的模式。例如,在本示例中,假定讓讀者驗證一些電話号碼的規範化。如下所示為一個醜陋并且壓縮的版本,後面跟着一個正确使用的 (?x),使代碼變得稍許易讀。

讀者可以使用 (?=...) 和 (?!…)符号在目标字元串中實作一個前視比對,而不必實際上使用這些字元串。前者是正向前視斷言,後者是負向前視斷言。在後面的示例中,我們僅僅對姓氏為“van rossum”的人的名字感興趣,下一個示例中,讓我們忽略以“noreply”或者“postmaster”開頭的e-mail位址。

第三個代碼片段用于示範findall()和finditer()的差別;我們使用後者來建構一個使用相同登入名但不同域名的e-mail位址清單(在一個更易于記憶的方法中,通過忽略建立用完即丢棄的中間清單)。

最後一個示例展示了使用條件正規表達式比對。假定我們擁有另一個特殊字元,它僅僅包含字母“x”和“y”,我們此時僅僅想要這樣限定字元串:兩字母的字元串必須由一個字母跟着另一個字母。換句話說,你不能同時擁有兩個相同的字母;要麼由“x”跟着“y”,要麼相反。

可能讀者會對于正規表達式的特殊字元和特殊ascii符号之間的差異感到迷惑。我們可以使用n表示一個換行符,但是我們可以使用d在正規表達式中表示比對單個數字。

如果有符号同時用于ascii和正規表達式,就會發生問題,是以在下面的核心提示中,建議使用python的原始字元串來避免産生問題。另一個警告是:w和w字母數字字元集同時受re.l/locale和unicode(re.u/unicode)标記所影響。