正則表達式指南
摘要
本文是關于在 Python 中通過?re?模塊使用正則表達式的入門教程。它提供了比“標準庫參考”的相關章節更平易的介紹。
引言
正則表達式(Regular expressions,也叫 REs、 regexs 或 regex patterns),本質上是嵌入 Python 內部并通過?re?模塊提供的一種微小的、高度專業化的編程語言。使用這種小語言,你可以為想要匹配的可能字符串編寫規則;這些字符串可能是英文句子、郵箱地址、TeX 命令或任何你喜歡的內容。然后,你可以提出諸如“此字符串是否與表達式匹配?”、“字符串中是否存在表達式的匹配項?”之類的問題。你還可以用正則來修改字符串,或以各種方式將其拆分。
正則表達式會被編譯成一系列字節碼,然后由 C 語言編寫的匹配引擎執行。對于高級用途,可能有必要特別注意引擎將如何執行一個給定的正則,并以某種方式寫入正則,以生成運行更快的字節碼。本文不涉及優化問題,因為這要求你對正則引擎的匹配過程有很好的了解。
正則表達式語言相對較小且受限,因此并非所有可能的字符串處理任務都可以使用正則表達式完成。有些任務盡管*可以*用正則表達式來完成,但表達式會變得非常復雜。這些情況下,最好通過編寫 Python 代碼來進行處理。也許 Python 代碼會比精心設計的正則表達式慢,但它可能更容易理解。
簡單正則
讓我們從最簡單的正則表達式開始吧。由于正則表達式是用來操作字符串的,我們將從最常見的任務開始:匹配字符。
關于正則表達式背后的計算機科學的詳細解釋(確定性和非確定性有限自動機),你可以參考幾乎所有關于編寫編譯器的教科書。
匹配字符
大多數字母和符號都會簡單地匹配自身。例如,正則表達式?test
?將會精確地匹配到?test
?。(你可以啟用不區分大小寫模式,讓這個正則也匹配?Test
?或?TEST
?,稍后會詳細介紹。)
但該規則有例外。有些字符是特殊的?元字符(metacharacters),并不匹配自身。事實上,它們表示匹配一些非常規的內容,或者通過重復它們或改變它們的含義來影響正則的其他部分。本文的大部分內容都致力于討論各種元字符及其作用。
這是元字符的完整列表。它們的含義將在本 HOWTO 的其余部分進行討論。
. ^ $ * + ? { } [ ] \ | ( )
首先介紹的元字符是?[
?和?]
?。這兩個元字符用于指定一個字符類,也就是你希望匹配的字符的一個集合。這些字符可以單獨地列出,也可以用字符范圍來表示(給出兩個字符并用?'-'
?分隔)。例如,[abc]
?將匹配?a
、b
、c
?之中的任意一個字符;這與?[a-c]
?相同,后者使用一個范圍來表達相同的字符集合。如果只想匹配小寫字母,則正則表達式將是?[a-z]
?。
元字符 (除了?\
) 在字符類中是不起作用的。 例如,[akm$]
?將會匹配以下任一字符?'a'
,?'k'
,?'m'
?或?'$'
;'$'
?通常是一個元字符,但在一個字符類中它的特殊性被消除了。
你可以通過對集合?取反?來匹配字符類中未列出的字符。方法是把?'^'
?放在字符類的最開頭。 例如,[^5]
?將匹配除?'5'
?之外的任何字符。 如果插入符出現在字符類的其他位置,則它沒有特殊含義。 例如:[5^]
?將匹配?'5'
?或?'^'
。
也許最重要的元字符是反斜杠,\
?。 與 Python 字符串字面量一樣,反斜杠后面可以跟各種字符來表示各種特殊序列。它還用于轉義元字符,以便可以在表達式中匹配元字符本身。例如,如果需要匹配一個?[
?或?\
?,可以在其前面加上一個反斜杠來消除它們的特殊含義:\[
?或?\\
?。
一些以?'\'
?開頭的特殊序列表示預定義的字符集合,這些字符集通常很有用,例如數字集合、字母集合或非空白字符集合。
讓我們舉一個例子:\w
?匹配任何字母數字字符。 如果正則表達式以 bytes 類型表示,\w
?相當于字符類?[a-zA-Z0-9_]
?。如果正則表達式是 str 類型,\w
?將匹配由?unicodedata?模塊提供的 Unicode 數據庫中標記為字母的所有字符。 通過在編譯正則表達式時提供?re.ASCII?標志,可以在 str 表達式中使用較為狹窄的?\w
?定義。
以下為特殊序列的不完全列表。 有關 Unicode 字符串正則表達式的序列和擴展類定義的完整列表,參見標準庫參考中?正則表達式語法?的最后一部分 。通常,Unicode 版本的字符類會匹配 Unicode 數據庫的相應類別中的任何字符。
\d
匹配任何十進制數字,等價于字符類?[0-9]
?。
\D
匹配任何非數字字符,等價于字符類?[^0-9]
?。
\s
匹配任何空白字符,等價于字符類?[?\t\n\r\f\v]
?。
\S
匹配任何非空白字符,等價于字符類?[^?\t\n\r\f\v]
?。
\w
匹配任何字母與數字字符,等價于字符類?[a-zA-Z0-9_]
?。
\W
匹配任何非字母與數字字符,等價于字符類?[^a-zA-Z0-9_]
?。
這些序列可以包含在字符類中。 例如,[\s,.]
?是一個匹配任何空白字符、','
?或?'.'
?的字符類。
本節的最后一個元字符是?.
?。 它匹配除換行符之外的任何字符,并且有一個可選模式(?re.DOTALL?),在該模式下它甚至可以匹配換行符。?.
?通常用于你想匹配“任何字符”的場景。
重復
能夠匹配各種各樣的字符集合是正則表達式可以做到的第一件事,而這是字符串方法所不能做到的。但是,如果正則表達式就只有這么一個附加功能,它很難說的上有多大優勢。另一個功能是,你可以指定正則的某部分必須重復一定的次數。
我們先來說說重復元字符?*
?。?*
?并不是匹配一個字面字符?'*'
?。實際上,它指定前一個字符可以匹配零次或更多次,而不是只匹配一次。
例如,ca*t
?將匹配?'ct'
?( 0 個?'a'
?)、'cat'
?( 1 個?'a'
?)、?'caaat'
?( 3 個?'a'
?)等等。
類似?*
?這樣的重復是?貪婪的?。當重復正則時,匹配引擎將嘗試重復盡可能多的次數。 如果表達式的后續部分不匹配,則匹配引擎將回退并以較少的重復次數再次嘗試。
通過一個逐步示例更容易理解這一點。讓我們分析一下表達式?a[bcd]*b
?。 該表達式首先匹配一個字母?'a'
?,接著匹配字符類?[bcd]
?中的零個或更多個字母,最后以一個?'b'
?結尾。 現在想象一下用這個正則來匹配字符串?'abcbd'
?。
步驟 | 匹配 | 說明 |
---|---|---|
1 |
| 正則中的? |
2 |
| 引擎盡可能多地匹配? |
3 | 失敗 | 引擎嘗試匹配? |
4 |
| 回退,讓? |
5 | 失敗 | 再次嘗試匹配? |
6 |
| 再次回退,讓? |
6 |
| 再次嘗試匹配? |
此時正則表達式已經到達了盡頭,并且匹配到了?'abcb'
?。 這個例子演示了匹配引擎一開始會盡其所能地進行匹配,如果沒有找到匹配,它將逐步回退并重試正則的剩余部分,如此往復,直至?[bcd]*
?只匹配零次。如果隨后的匹配還是失敗了,那么引擎會宣告整個正則表達式與字符串匹配失敗。
另一個重復元字符是?+
?,表示匹配一次或更多次。請注意?*
?與?+
?之間的差別。?*
?表示匹配?零次?或更多次,也就是說它所重復的內容是可以完全不出現的。而?+
?則要求至少出現一次。舉一個類似的例子,?ca+t
?可以匹配?'cat'
?( 1 個?'a'
?)或?'caaat'
?( 3 個?'a'
),但不能匹配?'ct'
?。
此外還有兩個重復操作符或限定符。 問號??
?表示匹配一次或零次;你可以認為它把某項內容變成了可選的。 例如,home-?brew
?可以匹配?'homebrew'
?或?'home-brew'
。
最復雜的限定符是?{m,n}
,其中?m?和?n?都是十進制整數。 該限定符表示必須至少重復?m?次,至多重復?n?次。 例如,a/{1,3}b
?將匹配?'a/b'
,?'a//b'
?和?'a///b'
。 它不能匹配?'ab'
,因為其中沒有斜杠,也不能匹配?'ab'
,因為其中有四個斜杠。
m?和?n?不是必填的,缺失的情況下會設定為默認值。缺失?m?會解釋為最少重復 0 次 ,缺失?n?則解釋為最多重復無限次。
最簡單情況?{m}
?將與前一項完全匹配?m?次。 例如,a/{2}b
?將只匹配?'a//b'
。
細心的讀者可能會注意到另外三個限定符都可以使用此標記法來表示。?{0,}
?等同于?*
,?{1,}
?等同于?+
, 而?{0,1}
?等同于??
。 在可能的情況下使用?*
,?+
?或??
?會更好,因為它們更為簡短易讀。
使用正則表達式
現在我們已經了解了一些簡單的正則表達式,那么我們如何在 Python 中實際使用它們呢??re?模塊提供了正則表達式引擎的接口,可以讓你將正則編譯為對象,然后用它們來進行匹配。
編譯正則表達式
正則表達式被編譯成模式對象,模式對象具有各種操作的方法,例如搜索模式匹配或執行字符串替換。:
>>>
>>> import re >>> p = re.compile('ab*') >>> p re.compile('ab*')
re.compile()?也接受一個可選的?flags?參數,用于啟用各種特殊功能和語法變體。 我們稍后將介紹可用的設置,但現在只需一個例子
>>>
>>> p = re.compile('ab*', re.IGNORECASE)
正則作為字符串傳遞給?re.compile()?。 正則被處理為字符串,因為正則表達式不是核心Python語言的一部分,并且沒有創建用于表達它們的特殊語法。 (有些應用程序根本不需要正則,因此不需要通過包含它們來擴展語言規范。)相反,re?模塊只是Python附帶的C擴展模塊,就類似于?socket?或?zlib?模塊。
將正則放在字符串中可以使 Python 語言更簡單,但有一個缺點是下一節的主題。
反斜杠災難
如前所述,正則表達式使用反斜杠字符 ('\'
) 來表示特殊形式或允許使用特殊字符而不調用它們的特殊含義。 這與 Python 在字符串文字中用于相同目的的相同字符的使用相沖突。
假設你想要編寫一個與字符串?\section
?相匹配的正則,它可以在 LaTeX 文件中找到。 要找出在程序代碼中寫入的內容,請從要匹配的字符串開始。 接下來,您必須通過在反斜杠前面添加反斜杠和其他元字符,從而產生字符串?\\section
。 必須傳遞給?re.compile()?的結果字符串必須是?\\section
。 但是,要將其表示為 Python 字符串文字,必須?再次?轉義兩個反斜杠。
字符 | 階段 |
---|---|
| 被匹配的字符串 |
| 為?re.compile()?轉義的反斜杠 |
| 為字符串字面轉義的反斜杠 |
簡而言之,要匹配文字反斜杠,必須將?'\\\\'
?寫為正則字符串,因為正則表達式必須是?\\
,并且每個反斜杠必須表示為?\\
?在常規Python字符串字面中。 在反復使用反斜杠的正則中,這會導致大量重復的反斜杠,并使得生成的字符串難以理解。
解決方案是使用 Python 的原始字符串表示法來表示正則表達式;反斜杠不以任何特殊的方式處理前綴為?'r'
?的字符串字面,因此?r"\n"
?是一個包含?'\'
?和?'n'
?的雙字符字符串,而?"\n"
?是一個包含換行符的單字符字符串。 正則表達式通常使用這種原始字符串表示法用 Python 代碼編寫。
此外,在正則表達式中有效但在 Python 字符串文字中無效的特殊轉義序列現在導致?DeprecationWarning?并最終變為?SyntaxError。 這意味著如果未使用原始字符串表示法或轉義反斜杠,序列將無效。
常規字符串 | 原始字符串 |
---|---|
|
|
|
|
|
|
應用匹配
一旦你有一個表示編譯正則表達式的對象,你用它做什么? 模式對象有幾種方法和屬性。 這里只介紹最重要的內容;請參閱?re?文檔獲取完整列表。
方法 / 屬性 | 目的 |
---|---|
| 確定正則是否從字符串的開頭匹配。 |
| 掃描字符串,查找此正則匹配的任何位置。 |
| 找到正則匹配的所有子字符串,并將它們作為列表返回。 |
| 找到正則匹配的所有子字符串,并將它們返回為一個?iterator。 |
如果沒有找到匹配,?match()?和?search()?返回?None
?。如果它們成功, 一個?匹配對象?實例將被返回,包含匹配相關的信息:起始和終結位置、匹配的子串以及其它。
你可以通過交互式地試驗?re?模塊來學習這一點。
本 HOWTO 使用標準 Python 解釋器作為示例。 首先,運行 Python 解釋器,導入?re?模塊,然后編譯一個正則
>>>
>>> import re >>> p = re.compile('[a-z]+') >>> p re.compile('[a-z]+')
現在,你可以嘗試匹配正則?[a-z]+
?的各種字符串。 空字符串根本不匹配,因為?+
?表示“一次或多次重復”。?match()?在這種情況下應返回?None
,這將導致解釋器不打印輸出。 你可以顯式打印?match()
?的結果,使其清晰。:
>>>
>>> p.match("") >>> print(p.match("")) None
現在,讓我們嘗試一下它應該匹配的字符串,例如?tempo
。在這個例子中?match()?將返回一個?匹配對象,因此你應該將結果儲存到一個變量中以供稍后使用。
>>>
>>> m = p.match('tempo') >>> m <re.Match object; span=(0, 5), match='tempo'>
現在你可以檢查?匹配對象?以獲取有關匹配字符串的信息。 匹配對象實例也有幾個方法和屬性;最重要的是:
方法 / 屬性 | 目的 |
---|---|
| 返回正則匹配的字符串 |
| 返回匹配的開始位置 |
| 返回匹配的結束位置 |
| 返回包含匹配 (start, end) 位置的元組 |
嘗試這些方法很快就會清楚它們的含義:
>>>
>>> m.group() 'tempo' >>> m.start(), m.end() (0, 5) >>> m.span() (0, 5)
group()?返回正則匹配的子字符串。?start()?和?end()?返回匹配的起始和結束索引。?span()?在單個元組中返回開始和結束索引。 由于?match()?方法只檢查正則是否在字符串的開頭匹配,所以?start()
?將始終為零。 但是,模式的?search()?方法會掃描字符串,因此在這種情況下匹配可能不會從零開始。:
>>>
>>> print(p.match('::: message')) None >>> m = p.search('::: message'); print(m) <re.Match object; span=(4, 11), match='message'> >>> m.group() 'message' >>> m.span() (4, 11)
在實際程序中,最常見的樣式是在變量中存儲?匹配對象,然后檢查它是否為?None
。 這通常看起來像:
p = re.compile( ... ) m = p.match( 'string goes here' ) if m:print('Match found: ', m.group()) else:print('No match')
兩種模式方法返回模式的所有匹配項。?findall()?返回匹配字符串的列表:
>>>
>>> p = re.compile(r'\d+') >>> p.findall('12 drummers drumming, 11 pipers piping, 10 lords a-leaping') ['12', '11', '10']
在這個例子中需要?r
?前綴,使字面為原始字符串字面,因為普通的“加工”字符串字面中的轉義序列不能被 Python 識別為正則表達式,導致?DeprecationWarning?并最終產生?SyntaxError。 請參閱?反斜杠災難。
findall()?必須先創建整個列表才能返回結果。?finditer()?方法將一個?匹配對象?的序列返回為一個?iterator
>>>
>>> iterator = p.finditer('12 drummers drumming, 11 ... 10 ...') >>> iterator <callable_iterator object at 0x...> >>> for match in iterator: ... print(match.span()) ... (0, 2) (22, 24) (29, 31)
模塊級函數
你不必創建模式對象并調用其方法;re?模塊還提供了頂級函數?match(),search(),findall(),sub()?等等。 這些函數采用與相應模式方法相同的參數,并將正則字符串作為第一個參數添加,并仍然返回?None
?或?匹配對象?實例。:
>>>
>>> print(re.match(r'From\s+', 'Fromage amk')) None >>> re.match(r'From\s+', 'From amk Thu May 14 19:12:10 1998') <re.Match object; span=(0, 5), match='From '>
本質上,這些函數只是為你創建一個模式對象,并在其上調用適當的方法。 它們還將編譯對象存儲在緩存中,因此使用相同的未來調用將不需要一次又一次地解析該模式。
你是否應該使用這些模塊級函數,還是應該自己獲取模式并調用其方法? 如果你正在循環中訪問正則表達式,預編譯它將節省一些函數調用。 在循環之外,由于有內部緩存,沒有太大區別。
編譯標志
編譯標志允許你修改正則表達式的工作方式。 標志在?re?模塊中有兩個名稱,長名稱如?IGNORECASE?和一個簡短的單字母形式,例如?I。 (如果你熟悉 Perl 的模式修飾符,則單字母形式使用和其相同的字母;例如,?re.VERBOSE?的縮寫形式為?re.X。)多個標志可以 通過按位或運算來指定它們;例如,re.I?|?re.M
?設置?I?和?M?標志。
這是一個可用標志表,以及每個標志的更詳細說明。
旗標 | 含意 |
---|---|
ASCII,?A | 使幾個轉義如? |
DOTALL,?S | 使? |
IGNORECASE,?I | 進行大小寫不敏感匹配。 |
LOCALE,?L | 進行區域設置感知匹配。 |
MULTILINE,?M | 多行匹配,影響? |
VERBOSE,?X?(為 '擴展') | 啟用詳細的正則,可以更清晰,更容易理解。 |
re.I
re.IGNORECASE
執行不區分大小寫的匹配;字符類和字面字符串將通過忽略大小寫來匹配字母。 例如,[A-Z]
?也匹配小寫字母。 除非使用?ASCII?標志來禁用非ASCII匹配,否則完全 Unicode 匹配也有效。 當 Unicode 模式?[a-z]
?或?[A-Z]
?與?IGNORECASE?標志結合使用時,它們將匹配 52 個 ASCII 字母和 4 個額外的非 ASCII 字母:'?' (U+0130,拉丁大寫字母 I,帶上面的點),'?' (U+0131,拉丁文小寫字母無點 i),'s' (U+017F,拉丁文小寫字母長 s) 和'K' (U+212A,開爾文符號)。?Spam
?將匹配?'Spam'
,'spam'
,'spAM'
?或?'?pam'
?(后者僅在 Unicode 模式下匹配)。 此小寫不考慮當前區域設置;如果你還設置了?LOCALE?標志,則將考慮。
re.L
re.LOCALE
使?\w
、\W
、\b
、\B
?和大小寫敏感匹配依賴于當前區域而不是 Unicode 數據庫。
區域設置是 C 庫的一個功能,旨在幫助編寫考慮到語言差異的程序。例如,如果你正在處理編碼的法語文本,那么你希望能夠編寫?\w+
?來匹配單詞,但?\w
?只匹配字符類?[A-Za-z]
?字節模式;它不會匹配對應于?é
?或??
?的字節。如果你的系統配置正確并且選擇了法語區域設置,某些C函數將告訴程序對應于?é
?的字節也應該被視為字母。在編譯正則表達式時設置?LOCALE?標志將導致生成的編譯對象將這些C函數用于?\w
;這比較慢,但也可以使?\w+
?匹配你所期望的法語單詞。在 Python 3 中不鼓勵使用此標志,因為語言環境機制非常不可靠,它一次只處理一個“文化”,它只適用于 8 位語言環境。默認情況下,Python 3 中已經為 Unicode(str)模式啟用了 Unicode 匹配,并且它能夠處理不同的區域/語言。
re.M
re.MULTILINE
(^
?和?$
?還沒有解釋;它們將在以下部分介紹?更多元字符。)
通常?^
?只匹配字符串的開頭,而?$
?只匹配字符串的結尾,緊接在字符串末尾的換行符(如果有的話)之前。 當指定了這個標志時,^
?匹配字符串的開頭和字符串中每一行的開頭,緊跟在每個換行符之后。 類似地,$
?元字符匹配字符串的結尾和每行的結尾(緊接在每個換行符之前)。
re.S
re.DOTALL
使?'.'
?特殊字符匹配任何字符,包括換行符;沒有這個標志,'.'
?將匹配任何字符?除了?換行符。
re.A
re.ASCII
使?\w
、\W
、\b
、\B
、\s
?和?\S
?執行僅 ASCII 匹配而不是完整匹配 Unicode 匹配。 這僅對 Unicode 模式有意義,并且對于字節模式將被忽略。
re.X
re.VERBOSE
此標志允許你編寫更易讀的正則表達式,方法是為您提供更靈活的格式化方式。 指定此標志后,將忽略正則字符串中的空格,除非空格位于字符類中或前面帶有未轉義的反斜杠;這使你可以更清楚地組織和縮進正則。 此標志還允許你將注釋放在正則中,引擎將忽略該注釋;注釋標記為?'#'
?既不是在字符類中,也不是在未轉義的反斜杠之前。
例如,這里的正則使用?re.VERBOSE;看看閱讀有多容易?:
charref = re.compile(r"""&[#] # Start of a numeric entity reference(0[0-7]+ # Octal form| [0-9]+ # Decimal form| x[0-9a-fA-F]+ # Hexadecimal form); # Trailing semicolon """, re.VERBOSE)
如果沒有詳細設置,正則將如下所示:
charref = re.compile("&#(0[0-7]+""|[0-9]+""|x[0-9a-fA-F]+);")
在上面的例子中,Python的字符串文字的自動連接已被用于將正則分解為更小的部分,但它仍然比以下使用?re.VERBOSE?版本更難理解。
更多模式能力
到目前為止,我們只介紹了正則表達式的一部分功能。 在本節中,我們將介紹一些新的元字符,以及如何使用組來檢索匹配的文本部分。
更多元字符
我們還沒有涉及到一些元字符。 其中大部分內容將在本節中介紹。
要討論的其余一些元字符是?零寬度斷言?。 它們不會使解析引擎在字符串中前進一個字符;相反,它們根本不占用任何字符,只是成功或失敗。例如,\b
?是一個斷言,指明當前位置位于字邊界;這個位置根本不會被?\b
?改變。這意味著永遠不應重復零寬度斷言,因為如果它們在給定位置匹配一次,它們顯然可以無限次匹配。
|
或者“or”運算符。 如果?A?和?B?是正則表達式,A|B
?將匹配任何與?A?或?B?匹配的字符串。?|
?具有非常低的優先級,以便在交替使用多字符字符串時使其合理地工作。?Crow|Servo
?將匹配?'Crow'
?或?'Servo'
,而不是?'Cro'
、'w'
?或?'S'
?和?'ervo'
。
要匹配字面?'|'
,請使用?\|
,或將其括在字符類中,如?[|]
。
^
在行的開頭匹配。 除非設置了?MULTILINE?標志,否則只會在字符串的開頭匹配。 在?MULTILINE?模式下,這也在字符串中的每個換行符后立即匹配。
例如,如果你希望僅在行的開頭匹配單詞?From
,則要使用的正則?^From
。:
>>>
>>> print(re.search('^From', 'From Here to Eternity')) <re.Match object; span=(0, 4), match='From'> >>> print(re.search('^From', 'Reciting From Memory')) None
要匹配字面?'^'
,使用?\^
。
$
匹配行的末尾,定義為字符串的結尾,或者后跟換行符的任何位置。:
>>>
>>> print(re.search('}$', '{block}')) <re.Match object; span=(6, 7), match='}'> >>> print(re.search('}$', '{block} ')) None >>> print(re.search('}$', '{block}\n')) <re.Match object; span=(6, 7), match='}'>
以匹配字面?'$'
,使用?\$
?或者將其包裹在一個字符類中,例如?[$]
。
\A
僅匹配字符串的開頭。 當不在?MULTILINE?模式時,\A
?和?^
?實際上是相同的。 在?MULTILINE?模式中,它們是不同的:?\A
?仍然只在字符串的開頭匹配,但?^
?可以匹配在換行符之后的字符串內的任何位置。
\Z
只匹配字符串尾。
\b
字邊界。 這是一個零寬度斷言,僅在單詞的開頭或結尾處匹配。 單詞被定義為一個字母數字字符序列,因此單詞的結尾由空格或非字母數字字符表示。
以下示例僅當它是一個完整的單詞時匹配?class
;當它包含在另一個單詞中時將不會匹配。
>>>
>>> p = re.compile(r'\bclass\b') >>> print(p.search('no class at all')) <re.Match object; span=(3, 8), match='class'> >>> print(p.search('the declassified algorithm')) None >>> print(p.search('one subclass is')) None
使用這個特殊序列時,你應該記住兩個細微之處。 首先,這是 Python 的字符串文字和正則表達式序列之間最嚴重的沖突。 在 Python 的字符串文字中,\b
?是退格字符,ASCII 值為8。 如果你沒有使用原始字符串,那么 Python 會將?\b
?轉換為退格,你的正則不會按照你的預期匹配。 以下示例與我們之前的正則看起來相同,但省略了正則字符串前面的?'r'
。:
>>>
>>> p = re.compile('\bclass\b') >>> print(p.search('no class at all')) None >>> print(p.search('\b' + 'class' + '\b')) <re.Match object; span=(0, 7), match='\x08class\x08'>
其次,在一個字符類中,這個斷言沒有用處,\b
?表示退格字符,以便與 Python 的字符串文字兼容。
\B
另一個零寬度斷言,這與?\b
?相反,僅在當前位置不在字邊界時才匹配。
分組
通常,你需要獲取更多信息,而不僅僅是正則是否匹配。 正則表達式通常用于通過將正則分成幾個子組來解析字符串,這些子組匹配不同的感興趣組件。 例如,RFC-822 標題行分為標題名稱和值,用?':'
?分隔,如下所示:
From: author@example.com User-Agent: Thunderbird 1.5.0.9 (X11/20061227) MIME-Version: 1.0 To: editor@example.com
這可以通過編寫與整個標題行匹配的正則表達式來處理,并且具有與標題名稱匹配的一個組,以及與標題的值匹配的另一個組。
分組是用?'('
,?')'
?元字符來標記的。?'('
?和?')'
?與它們在數學表達式中的含義基本一致;它們會將所包含的表達式合為一組,并且你可以使用限定符例如?*
,?+
,??
, 或?{m,n}
?來重復一個分組的內容。 舉例來說,(ab)*
?將匹配?ab
?的零次或多次重復。
>>>
>>> p = re.compile('(ab)*') >>> print(p.match('ababababab').span()) (0, 10)
用?'('
,')'
?表示的組也捕獲它們匹配的文本的起始和結束索引;這可以通過將參數傳遞給?group()、start()、end()?以及?span()。 組從 0 開始編號。組 0 始終存在;它表示整個正則,所以?匹配對象?方法都將組 0 作為默認參數。 稍后我們將看到如何表達不捕獲它們匹配的文本范圍的組。:
>>>
>>> p = re.compile('(a)b') >>> m = p.match('ab') >>> m.group() 'ab' >>> m.group(0) 'ab'
子組從左到右編號,從 1 向上編號。 組可以嵌套;要確定編號,只需計算從左到右的左括號字符。:
>>>
>>> p = re.compile('(a(b)c)d') >>> m = p.match('abcd') >>> m.group(0) 'abcd' >>> m.group(1) 'abc' >>> m.group(2) 'b'
group()?可以一次傳遞多個組號,在這種情況下,它將返回一個包含這些組的相應值的元組。:
>>>
>>> m.group(2,1,2) ('b', 'abc', 'b')
groups()?方法返回一個元組,其中包含所有子組的字符串,從1到最后一個子組。:
>>>
>>> m.groups() ('abc', 'b')
模式中的后向引用允許你指定還必須在字符串中的當前位置找到先前捕獲組的內容。 例如,如果可以在當前位置找到組 1 的確切內容,則?\1
?將成功,否則將失敗。 請記住,Python 的字符串文字也使用反斜杠后跟數字以允許在字符串中包含任意字符,因此正則中引入反向引用時務必使用原始字符串。
例如,以下正則檢測字符串中重復的單詞。:
>>>
>>> p = re.compile(r'\b(\w+)\s+\1\b') >>> p.search('Paris in the the spring').group() 'the the'
像這樣的后向引用通常不僅僅用于搜索字符串 —— 很少有文本格式以這種方式重復數據 —— 但是你很快就會發現它們在執行字符串替換時?非常?有用。
非捕獲和命名組
精心設計的正則可以使用許多組,既可以捕獲感興趣的子串,也可以對正則本身進行分組和構建。 在復雜的正則中,很難跟蹤組號。 有兩個功能可以幫助解決這個問題。 它們都使用常用語法進行正則表達式擴展,因此我們首先看一下。
Perl 5 以其對標準正則表達式的強大補充而聞名。 對于這些新功能,Perl 開發人員無法選擇新的單鍵擊元字符或以?\
?開頭的新特殊序列,否則 Perl 的正則表達式與標準正則容易混淆。 例如,如果他們選擇?&
?作為一個新的元字符,舊的表達式將假設?&
?是一個普通字符,并且不會編寫?\&
?或?[&]
。
Perl 開發人員選擇的解決方案是使用?(?...)
?作為擴展語法。 括號后面緊跟??
?是一個語法錯誤,因為??
?沒有什么可重復的,所以這樣并不會帶來任何兼容性問題。 緊跟在??
?之后的字符表示正在使用的擴展語法,所以?(?=foo)
?是一種語法(一個前視斷言)和?(?:foo)
?是另一種語法( 包含子表達式?foo
?的非捕獲組)。
Python 支持一些 Perl 的擴展,并增加了新的擴展語法用于 Perl 的擴展語法。 如果在問號之后的第一個字符為?P
,即表明其為 Python 專屬的擴展。
現在我們已經了解了一般的擴展語法,我們可以回到簡化復雜正則中組處理的功能。
有時你會想要使用組來表示正則表達式的一部分,但是對檢索組的內容不感興趣。 你可以通過使用非捕獲組來顯式表達這個事實:?(?:...)
,你可以用任何其他正則表達式替換?...
。:
>>>
>>> m = re.match("([abc])+", "abc") >>> m.groups() ('c',) >>> m = re.match("(?:[abc])+", "abc") >>> m.groups() ()
除了你無法檢索組匹配內容的事實外,非捕獲組的行為與捕獲組完全相同;你可以在里面放任何東西,用重復元字符重復它,比如?*
,然后把它嵌入其他組(捕獲或不捕獲)。?(?:...)
?在修改現有模式時特別有用,因為你可以添加新組而不更改所有其他組的編號方式。 值得一提的是,捕獲和非捕獲組之間的搜索沒有性能差異;兩種形式沒有一種更快。
更重要的功能是命名組:不是通過數字引用它們,而是可以通過名稱引用組。
命名組的語法是Python特定的擴展之一:?(?P<name>...)
。?name?顯然是該組的名稱。 命名組的行為與捕獲組完全相同,并且還將名稱與組關聯。 處理捕獲組的?匹配對象?方法都接受按編號引用組的整數或包含所需組名的字符串。 命名組仍然是給定的數字,因此你可以通過兩種方式檢索有關組的信息:
>>>
>>> p = re.compile(r'(?P<word>\b\w+\b)') >>> m = p.search( '(((( Lots of punctuation )))' ) >>> m.group('word') 'Lots' >>> m.group(1) 'Lots'
此外,你可以通過?groupdict()?將命名分組提取為一個字典:
>>>
>>> m = re.match(r'(?P<first>\w+) (?P<last>\w+)', 'Jane Doe') >>> m.groupdict() {'first': 'Jane', 'last': 'Doe'}
命名分組很方便因為它們讓你可以使用容易記憶的名稱,而不必記憶數字。 下面是一個來自?imaplib?模塊的正則表達式示例:
InternalDate = re.compile(r'INTERNALDATE "'r'(?P<day>[ 123][0-9])-(?P<mon>[A-Z][a-z][a-z])-'r'(?P<year>[0-9][0-9][0-9][0-9])'r' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'r' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'r'"')
檢索?m.group('zonem')
?顯然要容易得多,而不必記住檢索第 9 組。
表達式中的后向引用語法,例如?(...)\1
,指的是組的編號。 當然有一種變體使用組名而不是數字。 這是另一個 Python 擴展:?(?P=name)
?表示在當前點再次匹配名為?name?的組的內容。 用于查找重復單詞的正則表達式,\b(\w+)\s+\1\b
?也可以寫為?\b(?P<word>\w+)\s+(?P=word)\b
:
>>>
>>> p = re.compile(r'\b(?P<word>\w+)\s+(?P=word)\b') >>> p.search('Paris in the the spring').group() 'the the'
前視斷言
另一個零寬斷言是前視斷言。 前視斷言有肯定型和否定型兩種形式,如下所示:
(?=…)
肯定型前視斷言。如果內部的表達式(這里用?...
?來表示)在當前位置可以匹配,則匹配成功,否則匹配失敗。 但是,內部表達式嘗試匹配之后,正則引擎并不會向前推進;正則表達式的其余部分依然會在斷言開始的地方嘗試匹配。
(?!…)
否定型前視斷言。 與肯定型斷言正好相反,如果內部表達式在字符串中的當前位置?不?匹配,則成功。
更具體一些,來看一個前視的實用案例。 考慮用一個簡單的表達式來匹配文件名并將其拆分為基本名稱和擴展名,以?.
?分隔。 例如,在?news.rc
?中,news
?是基本名稱,rc
?是文件名的擴展名。
與此匹配的模式非常簡單:
.*[.].*$
請注意,.
?需要特別處理,因為它是元字符,所以它在字符類中只能匹配特定字符。 還要注意尾隨的?$
;添加此項以確保擴展名中的所有其余字符串都必須包含在擴展名中。 這個正則表達式匹配?foo.bar
、autoexec.bat
、sendmail.cf
?和?printers.conf
。
現在,考慮使更復雜一點的問題;如果你想匹配擴展名不是?bat
?的文件名怎么辦? 一些錯誤的嘗試:
.*[.][^b].*$
?上面的第一次嘗試試圖通過要求擴展名的第一個字符不是?b
?來排除?bat
。 這是錯誤的,因為模式也與?foo.bar
?不匹配。
.*[.]([^b]..|.[^a].|..[^t])$
當你嘗試通過要求以下一種情況匹配來修補第一個解決方案時,表達式變得更加混亂:擴展的第一個字符不是?b
。 第二個字符不?a
;或者第三個字符不是?t
。 這接受?foo.bar
?并拒絕?autoexec.bat
,但它需要三個字母的擴展名,并且不接受帶有兩個字母擴展名的文件名,例如?sendmail.cf
。 為了解決這個問題,我們會再次使模式復雜化。
.*[.]([^b].?.?|.[^a]?.?|..?[^t]?)$
在第三次嘗試中,第二個和第三個字母都是可選的,以便允許匹配的擴展名短于三個字符,例如?sendmail.cf
。
模式現在變得非常復雜,這使得它難以閱讀和理解。 更糟糕的是,如果問題發生變化并且你想要將?bat
?和?exe
?排除為擴展,那么該模式將變得更加復雜和混亂。
否定型前視可以解決所有這些困擾:
.*[.](?!bat$)[^.]*$
?否定型前視意味著:如果表達式?bat
?在當前位置不能匹配,則可以接著嘗試正則表達式的其余部分;如果?bat$
?能匹配,則整個正則表達式將匹配失敗。尾隨的?$
?是必需的,以確保可以匹配到像?sample.batch
?這樣以?bat
?開頭的文件名。當文件名中有多個點號時,?[^.]*
?可以確保表達式依然有效。
現在很容易排除另一個文件擴展名;只需在斷言中添加它作為替代。 以下模塊排除以?bat
?或?exe
:
.*[.](?!bat$|exe$)[^.]*$
修改字符串
到目前為止,我們只是針對靜態字符串執行搜索。 正則表達式通常也用于以各種方式修改字符串,使用以下模式方法:
方法 / 屬性 | 目的 |
---|---|
| 將字符串拆分為一個列表,在正則匹配的任何地方將其拆分 |
| 找到正則匹配的所有子字符串,并用不同的字符串替換它們 |
| 與? |
分割字符串
模式的?split()?方法在正則匹配的任何地方拆分字符串,返回一個片段列表。 它類似于?split()?字符串方法,但在分隔符的分隔符中提供了更多的通用性;字符串的?split()
?僅支持按空格或固定字符串進行拆分。 正如你所期望的那樣,還有一個模塊級?re.split()?函數。
.split(string[,?maxsplit=0])
通過正則表達式的匹配拆分?字符串。 如果在正則中使用捕獲括號,則它們的內容也將作為結果列表的一部分返回。 如果?maxsplit?非零,則最多執行?maxsplit?次拆分。
你可以通過傳遞?maxsplit?的值來限制分割的數量。 當?maxsplit?非零時,將最多進行?maxsplit?次拆分,并且字符串的其余部分將作為列表的最后一個元素返回。 在以下示例中,分隔符是任何非字母數字字符序列。:
>>>
>>> p = re.compile(r'\W+') >>> p.split('This is a test, short and sweet, of split().') ['This', 'is', 'a', 'test', 'short', 'and', 'sweet', 'of', 'split', ''] >>> p.split('This is a test, short and sweet, of split().', 3) ['This', 'is', 'a', 'test, short and sweet, of split().']
有時你不僅對分隔符之間的文本感興趣,而且還需要知道分隔符是什么。 如果在正則中使用捕獲括號,則它們的值也將作為列表的一部分返回。 比較以下調用:
>>>
>>> p = re.compile(r'\W+') >>> p2 = re.compile(r'(\W+)') >>> p.split('This... is a test.') ['This', 'is', 'a', 'test', ''] >>> p2.split('This... is a test.') ['This', '... ', 'is', ' ', 'a', ' ', 'test', '.', '']
模塊級函數?re.split()?添加要正則作為第一個參數,但在其他方面是相同的。:
>>>
>>> re.split(r'[\W]+', 'Words, words, words.') ['Words', 'words', 'words', ''] >>> re.split(r'([\W]+)', 'Words, words, words.') ['Words', ', ', 'words', ', ', 'words', '.', ''] >>> re.split(r'[\W]+', 'Words, words, words.', 1) ['Words', 'words, words.']
搜索和替換
另一個常見任務是找到模式的所有匹配項,并用不同的字符串替換它們。?sub()?方法接受一個替換值,可以是字符串或函數,也可以是要處理的字符串。
.sub(replacement,?string[,?count=0])
返回通過替換?replacement?替換?string?中正則的最左邊非重疊出現而獲得的字符串。 如果未找到模式,則?string?將保持不變。
可選參數?count?是要替換的模式最大的出現次數;count?必須是非負整數。 默認值 0 表示替換所有。
這是一個使用?sub()?方法的簡單示例。 它用?colour
?這個詞取代顏色名稱:
>>>
>>> p = re.compile('(blue|white|red)') >>> p.sub('colour', 'blue socks and red shoes') 'colour socks and colour shoes' >>> p.sub('colour', 'blue socks and red shoes', count=1) 'colour socks and red shoes'
subn()?方法完成相同的工作,但返回一個包含新字符串值和已執行的替換次數的 2 元組:
>>>
>>> p = re.compile('(blue|white|red)') >>> p.subn('colour', 'blue socks and red shoes') ('colour socks and colour shoes', 2) >>> p.subn('colour', 'no colours at all') ('no colours at all', 0)
僅當空匹配與前一個空匹配不相鄰時,才會替換空匹配。:
>>>
>>> p = re.compile('x*') >>> p.sub('-', 'abxd') '-a-b--d-'
如果?replacement?是一個字符串,則處理其中的任何反斜杠轉義。 也就是說,\n
?被轉換為單個換行符,\r
?被轉換為回車符,依此類推。 諸如?\&
?之類的未知轉義是孤立的。 后向引用,例如?\6
,被替換為正則中相應組匹配的子字符串。 這使你可以在生成的替換字符串中合并原始文本的部分內容。
這個例子匹配單詞?section
?后跟一個用?{
,}
?括起來的字符串,并將?section
?改為?subsection
>>>
>>> p = re.compile('section{ ( [^}]* ) }', re.VERBOSE) >>> p.sub(r'subsection{\1}','section{First} section{second}') 'subsection{First} subsection{second}'
還有一種語法用于引用由?(?P<name>...)
?語法定義的命名組。?\g<name>
?將使用名為?name
?的組匹配的子字符串,\g<number>
?使用相應的組號。 因此?\g<2>
?等同于?\2
,但在諸如?\g<2>0
?之類的替換字符串中并不模糊。 (\20
?將被解釋為對組 20 的引用,而不是對組 2 的引用,后跟字面字符?'0'
。) 以下替換都是等效的,但使用所有三種變體替換字符串。:
>>>
>>> p = re.compile('section{ (?P<name> [^}]* ) }', re.VERBOSE) >>> p.sub(r'subsection{\1}','section{First}') 'subsection{First}' >>> p.sub(r'subsection{\g<1>}','section{First}') 'subsection{First}' >>> p.sub(r'subsection{\g<name>}','section{First}') 'subsection{First}'
replacement?也可以是一個函數,它可以為你提供更多控制。 如果?replacement?是一個函數,則為?pattern?的每次非重疊出現將調用該函數。 在每次調用時,函數都會傳遞一個匹配的?匹配對象?參數,并可以使用此信息計算所需的替換字符串并將其返回。
在以下示例中,替換函數將小數轉換為十六進制:
>>>
>>> def hexrepl(match): ... "Return the hex string for a decimal number" ... value = int(match.group()) ... return hex(value) ... >>> p = re.compile(r'\d+') >>> p.sub(hexrepl, 'Call 65490 for printing, 49152 for user code.') 'Call 0xffd2 for printing, 0xc000 for user code.'
使用模塊級別?re.sub()?函數時,模式作為第一個參數傳遞。 模式可以是對象或字符串;如果需要指定正則表達式標志,則必須使用模式對象作為第一個參數,或者在模式字符串中使用嵌入式修飾符,例如:?sub("(?i)b+",?"x",?"bbbb?BBBB")
?返回?'x?x'
。
常見問題
正則表達式對于某些應用程序來說是一個強大的工具,但在某些方面,它們的行為并不直觀,有時它們的行為方式與你的預期不同。 本節將指出一些最常見的陷阱。
使用字符串方法
有時使用?re?模塊是一個錯誤。 如果你匹配固定字符串或單個字符類,并且你沒有使用任何?re?功能,例如?IGNORECASE?標志,那么正則表達式的全部功能可能不是必需的。 字符串有幾種方法可以使用固定字符串執行操作,它們通常要快得多,因為實現是一個針對此目的而優化的單個小 C 循環,而不是大型、更通用的正則表達式引擎。
一個例子可能是用另一個固定字符串替換一個固定字符串;例如,你可以用?deed
?替換?word
?。?re.sub()?看起來像是用于此的函數,但請考慮?replace()?方法。 注意?replace()
?也會替換單詞里面的?word
?,把?swordfish
?變成?sdeedfish
?,但簡單的正則?word
?也會這樣做。 (為了避免對單詞的部分進行替換,模式必須是?\bword\b
,以便要求?word
?在任何一方都有一個單詞邊界。這使得工作超出了?replace()
?的能力。)
另一個常見任務是從字符串中刪除單個字符的每個匹配項或將其替換為另一個字符。 你可以用?re.sub('\n',?'?',?S)
?之類的東西來做這件事,但是?translate()?能夠完成這兩項任務,并且比任何正則表達式都快。
簡而言之,在轉向?re?模塊之前,請考慮是否可以使用更快更簡單的字符串方法解決問題。
match() 和 search()
The?match()?function only checks if the RE matches at the beginning of the string while?search()?will scan forward through the string for a match. It's important to keep this distinction in mind. Remember,?match()
?will only report a successful match which will start at 0; if the match wouldn't start at zero,?match()
?will?not?report it.
>>>
>>> print(re.match('super', 'superstition').span()) (0, 5) >>> print(re.match('super', 'insuperable')) None
另一方面,?search()?將向前掃描字符串,報告它找到的第一個匹配項。:
>>>
>>> print(re.search('super', 'superstition').span()) (0, 5) >>> print(re.search('super', 'insuperable').span()) (2, 7)
有時你會被誘惑繼續使用?re.match()?,只需在你的正則前面添加?.*
?。抵制這種誘惑并使用?re.search()?代替。 正則表達式編譯器對正則進行一些分析,以加快尋找匹配的過程。 其中一個分析可以確定匹配的第一個特征必須是什么;例如,以?Crow
?開頭的模式必須與?'C'
?匹配。 分析讓引擎快速掃描字符串,尋找起始字符,只在找到?'C'
?時嘗試完全匹配。
添加?.*
?會使這個優化失效,需要掃描到字符串的末尾,然后回溯以找到正則的其余部分的匹配。 使用?re.search()?代替。
貪婪與非貪婪
當重復一個正則表達式時,就像在?a*
?中一樣,最終的動作就是消耗盡可能多的模式。 當你嘗試匹配一對對稱分隔符,例如 HTML 標記周圍的尖括號時,這個事實經常會讓你感到困惑。因為?.*
?的貪婪性質, 用于匹配單個 HTML 標記的簡單模式不起作用。
>>>
>>> s = '<html><head><title>Title</title>' >>> len(s) 32 >>> print(re.match('<.*>', s).span()) (0, 32) >>> print(re.match('<.*>', s).group()) <html><head><title>Title</title>
正則匹配?'<'
?中的?'<html>'
?和?.*
?消耗字符串的其余部分。 正則中還有更多的剩余東西,并且?>
?在字符串的末尾不能匹配,所以正則表達式引擎必須逐個字符地回溯,直到它找到匹配?>
?。最終匹配從?'<html>'
?中的?'<'
?擴展到?'</title>'
?中的?'>'
?,而這并不是你想要的結果。
在這種情況下,解決方案是使用非貪婪限定符?*?
,?+?
,???
?或?{m,n}?
,它們會匹配盡可能?少的?文本。 在上面的例子中,'>'
?會在第一個?'<'
?匹配后被立即嘗試,而當匹配失敗時,引擎將每次前進一個字符,并在每一步重試?'>'
。 這將產生正確的結果:
>>>
>>> print(re.match('<.*?>', s).group()) <html>
(請注意,使用正則表達式解析 HTML 或 XML 很痛苦。快而臟的模式將處理常見情況,但 HTML 和 XML 有特殊情況會破壞明顯的正則表達式;當你編寫正則表達式處理所有可能的情況時,模式將非常復雜。使用 HTML 或 XML 解析器模塊來執行此類任務。)
使用 re.VERBOSE
到目前為止,你可能已經注意到正則表達式是一種非常緊湊的表示法,但它們并不是非常易讀。 具有中等復雜度的正則可能會成為反斜杠、括號和元字符的冗長集合,使其難以閱讀和理解。
對于這樣的正則,在編譯正則表達式時指定?re.VERBOSE?標志可能會有所幫助,因為它允許你更清楚地格式化正則表達式。
re.VERBOSE
?標志有幾種效果。 正則表達式中的?不是?在字符類中的空格將被忽略。 這意味著表達式如?dog?|?cat
?等同于不太可讀的?dog|cat
?,但?[a?b]
?仍將匹配字符?'a'
?、?'b'
?或空格。 此外,你還可以在正則中放置注釋;注釋從?#
?字符擴展到下一個換行符。 當與三引號字符串一起使用時,這使正則的格式更加整齊:
pat = re.compile(r"""\s* # Skip leading whitespace(?P<header>[^:]+) # Header name\s* : # Whitespace, and a colon(?P<value>.*?) # The header's value -- *? used to# lose the following trailing whitespace\s*$ # Trailing whitespace to end-of-line """, re.VERBOSE)
這更具有可讀性:
pat = re.compile(r"\s*(?P<header>[^:]+)\s*:(?P<value>.*?)\s*$")
反饋
正則表達式是一個復雜的主題。 這份文檔是否有助于你理解它們? 是否存在不清楚的部分,或者你遇到的問題未在此處涉及? 如果是,請向作者發送改進建議。
關于正則表達式的最完整的書幾乎肯定是由 O'Reilly 出版的 Jeffrey Friedl 的 Mastering Regular Expressions 。 不幸的是,它專注于 Perl 和 Java 的正則表達式,并且根本不包含任何 Python 材料,因此它不能用作 Python 編程的參考。 (第一版涵蓋了 Python 現在刪除的?regex
?模塊,這對你沒有多大幫助。)考慮從你的圖書館中查找它。