正則表示式 HOWTO

作者:

A.M. Kuchling <amk@amk.ca>

簡介

正則表示式(稱為 RE、regex 或 regex 模式)本質上是一種嵌入在 Python 中並透過 re 模組提供的微型、高度專業化的程式語言。使用這種小語言,您可以指定要匹配的可能字串集合的規則;此集合可能包含英語句子、電子郵件地址、TeX 命令或您喜歡的任何內容。然後,您可以提出諸如“此字串是否與模式匹配?”或“此字串中是否有任何位置與模式匹配?”之類的問題。您還可以使用 RE 修改字串或以各種方式將其拆分。

正則表示式模式被編譯成一系列位元組碼,然後由用 C 編寫的匹配引擎執行。對於高階使用,可能需要仔細注意引擎將如何執行給定的 RE,並以某種方式編寫 RE,以便生成執行速度更快的位元組碼。本文件不介紹最佳化,因為它要求您對匹配引擎的內部結構有很好的瞭解。

正則表示式語言相對較小且受限,因此並非所有可能的字串處理任務都可以使用正則表示式完成。還有一些任務可以使用正則表示式完成,但表示式最終會變得非常複雜。在這些情況下,您最好編寫 Python 程式碼來進行處理;雖然 Python 程式碼比複雜的正則表示式慢,但它可能也更容易理解。

簡單模式

我們將從學習最簡單的正則表示式開始。由於正則表示式用於處理字串,我們將從最常見的任務開始:匹配字元。

有關正則表示式背後的計算機科學(確定性和非確定性有限自動機)的詳細說明,您可以參考幾乎任何關於編寫編譯器的教科書。

匹配字元

大多數字母和字元將簡單地匹配自身。例如,正則表示式 test 將完全匹配字串 test。(您可以啟用不區分大小寫的模式,讓此 RE 也匹配 TestTEST;稍後會詳細介紹。)

此規則有一些例外;某些字元是特殊的元字元,它們不匹配自身。相反,它們表示應該匹配一些不同尋常的內容,或者它們透過重複它們或更改其含義來影響 RE 的其他部分。本文件的大部分內容都致力於討論各種元字元及其作用。

以下是元字元的完整列表;它們的含義將在本 HOWTO 的其餘部分中討論。

. ^ $ * + ? { } [ ] \ | ( )

我們將看到的第一個元字元是 []。它們用於指定字元類,這是一組您希望匹配的字元。可以單獨列出字元,也可以透過給出兩個字元並用 '-' 分隔來指示字元範圍。例如,[abc] 將匹配字元 abc 中的任何一個;這與 [a-c] 相同,後者使用範圍來表示同一組字元。如果您只想匹配小寫字母,您的 RE 將是 [a-z]

元字元(除了 \)在類內部不活動。例如,[akm$] 將匹配字元 'a''k''m''$' 中的任何一個;'$' 通常是一個元字元,但在字元類內部,它會失去其特殊性質。

您可以透過補全該集合來匹配類中未列出的字元。這透過在類的第一個字元中包含 '^' 來表示。例如,[^5] 將匹配除 '5' 以外的任何字元。如果插入符號出現在字元類中的其他位置,它沒有特殊含義。例如:[5^] 將匹配 '5''^'

也許最重要的元字元是反斜槓 \。與 Python 字串文字一樣,反斜槓後面可以跟各種字元來表示各種特殊序列。它也用於轉義所有元字元,以便您仍然可以在模式中匹配它們;例如,如果您需要匹配 [\,您可以在它們前面加上反斜槓以刪除其特殊含義:\[\\

'\' 開頭的一些特殊序列表示通常有用的預定義字元集,例如數字集、字母集或任何不是空格的字元集。

讓我們舉一個例子:\w 匹配任何字母數字字元。如果正則表示式模式以位元組表示,則它等效於類 [a-zA-Z0-9_]。如果正則表示式模式是一個字串,\w 將匹配 unicodedata 模組提供的 Unicode 資料庫中標記為字母的所有字元。您可以透過在編譯正則表示式時提供 re.ASCII 標誌,在字串模式中使用 \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),在這種模式下,它甚至會匹配換行符。. 通常用於你想匹配“任何字元”的情況。

重複匹配

能夠匹配不同字元集是正則表示式的第一項能力,而這種能力是字串的現有方法無法實現的。然而,如果這只是正則表示式的唯一額外能力,它們就不會有太大的進步。另一個能力是你可以指定 RE 的某些部分必須重複一定的次數。

我們將看到的第一個用於重複匹配的元字元是 ** 不匹配字面字元 '*';相反,它指定前一個字元可以匹配零次或多次,而不是恰好一次。

例如,ca*t 將匹配 'ct'(0 個 'a' 字元)、'cat'(1 個 'a')、'caaat'(3 個 'a' 字元),等等。

諸如 * 之類的重複是貪婪的;當重複 RE 時,匹配引擎會嘗試儘可能多地重複它。如果模式的後面部分不匹配,匹配引擎將回溯並嘗試減少重複次數。

一個逐步的例子將使這一點更加明顯。讓我們考慮表示式 a[bcd]*b。它匹配字母 'a',零個或多個來自字元類 [bcd] 的字母,最後以 'b' 結尾。現在想象一下將此 RE 與字串 'abcbd' 進行匹配。

步驟

已匹配

解釋

1

a

RE 中的 a 匹配。

2

abcbd

引擎匹配 [bcd]*,儘可能地匹配到字串的末尾。

3

失敗

引擎嘗試匹配 b,但當前位置在字串的末尾,因此匹配失敗。

4

abcb

回溯,使 [bcd]* 少匹配一個字元。

5

失敗

再次嘗試匹配 b,但當前位置在最後一個字元,這是一個 'd'

6

abc

再次回溯,使 [bcd]* 僅匹配 bc

6

abcb

再次嘗試匹配 b。這次當前位置的字元是 'b',因此匹配成功。

現在已經到達 RE 的末尾,並且它已經匹配了 'abcb'。這說明了匹配引擎首先會盡可能地匹配,如果沒有找到匹配項,則會逐步回溯並一次又一次地重試 RE 的其餘部分。它會回溯,直到嘗試對 [bcd]* 進行零次匹配,如果隨後失敗,引擎將得出結論,該字串根本不匹配 RE。

另一個重複元字元是 +,它匹配一次或多次。請仔細注意 *+ 之間的區別;* 匹配次或多次,因此重複的任何內容可能根本不存在,而 + 要求至少出現一次。使用類似的例子,ca+t 將匹配 'cat'(1 個 'a')、'caaat'(3 個 'a'),但不會匹配 'ct'

還有兩個重複運算子或量詞。問號字元 ? 匹配一次或零次;你可以認為它將某事物標記為可選的。例如,home-?brew 匹配 'homebrew''home-brew'

最複雜的量詞是 {m,n},其中 mn 是十進位制整數。此量詞表示必須至少重複 m 次,最多重複 n 次。例如,a/{1,3}b 將匹配 'a/b''a//b''a///b'。它不會匹配沒有斜槓的 'ab',也不會匹配有四個斜槓的 'a////b'

你可以省略 mn;在這種情況下,會為缺失的值假定一個合理的值。省略 m 被解釋為下限為 0,而省略 n 則導致上限為無窮大。

最簡單的情況 {m} 精確匹配前一項 m 次。例如,a/{2}b 將僅匹配 'a//b'

具有還原論傾向的讀者可能會注意到,其他三個量詞都可以使用此表示法表示。{0,}* 相同,{1,} 等效於 +,而 {0,1}? 相同。最好儘可能使用 *+?,只是因為它們更短且更易於閱讀。

使用正則表示式

現在我們已經瞭解了一些簡單的正則表示式,如何在 Python 中實際使用它們呢?re 模組提供了正則表示式引擎的介面,允許你將 RE 編譯成物件,然後使用它們執行匹配。

編譯正則表示式

正則表示式被編譯成模式物件,這些物件具有各種操作的方法,例如搜尋模式匹配或執行字串替換。

>>> import re
>>> p = re.compile('ab*')
>>> p
re.compile('ab*')

re.compile() 還接受一個可選的 flags 引數,用於啟用各種特殊功能和語法變體。我們稍後將介紹可用的設定,但現在一個簡單的例子就足夠了

>>> p = re.compile('ab*', re.IGNORECASE)

RE 作為字串傳遞給 re.compile()。RE 被處理為字串,因為正則表示式不是 Python 核心語言的一部分,也沒有建立特殊的語法來表達它們。(有些應用程式根本不需要 RE,因此沒有必要透過包含它們來膨脹語言規範。)相反,re 模組只是 Python 中包含的 C 擴充套件模組,就像 socketzlib 模組一樣。

將 RE 放在字串中可以使 Python 語言更簡單,但有一個缺點,這是下一節的主題。

反斜槓瘟疫

如前所述,正則表示式使用反斜槓字元 ('\') 來指示特殊形式,或允許在不呼叫其特殊含義的情況下使用特殊字元。這與 Python 在字串文字中使用相同的字元用於相同目的相沖突。

假設你想編寫一個 RE 來匹配字串 \section,該字串可能會在 LaTeX 檔案中找到。要弄清楚在程式程式碼中寫什麼,請從要匹配的所需字串開始。接下來,你必須透過在反斜槓前面加上反斜槓來轉義任何反斜槓和其他元字元,從而得到字串 \\section。必須傳遞給 re.compile() 的結果字串必須是 \\section。但是,要將其表達為 Python 字串文字,必須再次轉義兩個反斜槓。

字元

階段

\section

要匹配的文字字串

\\section

re.compile() 轉義的反斜槓

"\\\\section"

為字串文字轉義的反斜槓

簡而言之,要匹配字面反斜槓,必須將 '\\\\' 寫為 RE 字串,因為正則表示式必須是 \\,並且每個反斜槓必須在常規 Python 字串文字中表示為 \\。在反覆出現反斜槓的 RE 中,這會導致大量重複的反斜槓,並使生成的字串難以理解。

解決方案是在正則表示式中使用 Python 的原始字串表示法;在以 'r' 為字首的字串字面量中,反斜槓不會被特殊處理,因此 r"\n" 是一個包含 '\''n' 兩個字元的字串,而 "\n" 是一個包含換行符的單字元字串。在 Python 程式碼中,正則表示式通常會使用這種原始字串表示法編寫。

此外,在正則表示式中有效,但作為 Python 字串字面量無效的特殊轉義序列,現在會導致 DeprecationWarning,並最終變為 SyntaxError,這意味著如果沒有使用原始字串表示法或轉義反斜槓,這些序列將無效。

普通字串

原始字串

"ab*"

r"ab*"

"\\\\section"

r"\\section"

"\\w+\\s+\\1"

r"\w+\s+\1"

執行匹配

一旦你擁有了一個表示已編譯正則表示式的物件,你該如何使用它呢?模式物件有幾個方法和屬性。這裡只介紹最重要的幾個;請查閱 re 文件以獲取完整列表。

方法/屬性

目的

match()

確定 RE 是否在字串的開頭匹配。

search()

掃描整個字串,查詢 RE 匹配的任何位置。

findall()

查詢所有 RE 匹配的子字串,並將它們作為列表返回。

finditer()

查詢所有 RE 匹配的子字串,並將它們作為 迭代器 返回。

match()search() 如果找不到匹配項,則返回 None。如果它們成功,則會返回一個 匹配物件 例項,其中包含有關匹配的資訊:它開始和結束的位置、它匹配的子字串等等。

你可以透過互動式地試驗 re 模組來了解這一點。

本 HOWTO 使用標準的 Python 直譯器作為示例。首先,執行 Python 直譯器,匯入 re 模組,並編譯一個 RE

>>> import re
>>> p = re.compile('[a-z]+')
>>> p
re.compile('[a-z]+')

現在,你可以嘗試用各種字串來匹配 RE [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'>

現在,你可以查詢 匹配物件 以獲取有關匹配字串的資訊。匹配物件例項也有幾個方法和屬性;最重要的幾個是

方法/屬性

目的

group()

返回 RE 匹配的字串

start()

返回匹配的起始位置

end()

返回匹配的結束位置

span()

返回一個包含匹配的(起始位置、結束位置)的元組

嘗試這些方法很快就會明確它們的含義

>>> m.group()
'tempo'
>>> m.start(), m.end()
(0, 5)
>>> m.span()
(0, 5)

group() 返回 RE 匹配的子字串。start()end() 返回匹配的起始和結束索引。span() 返回單個元組中的起始和結束索引。由於 match() 方法僅檢查 RE 是否在字串的開頭匹配,因此 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 = 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() 等等。這些函式接受與相應的模式方法相同的引數,並將 RE 字串作為第一個引數新增,並且仍然返回 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 的呼叫無需一遍又一遍地解析模式。

你應該使用這些模組級函式,還是應該獲取模式並自己呼叫其方法?如果你在迴圈中訪問正則表示式,則預編譯它可以節省一些函式呼叫。在迴圈之外,由於內部快取,差異不大。

編譯標誌

編譯標誌允許你修改正則表示式工作方式的某些方面。標誌在 re 模組中以兩個名稱提供,一個長名稱,例如 IGNORECASE,和一個短的單字母形式,例如 I。(如果你熟悉 Perl 的模式修飾符,單字母形式使用相同的字母;re.VERBOSE 的簡寫形式是 re.X,例如。)可以透過按位 OR 運算來指定多個標誌;例如,re.I | re.M 設定 IM 標誌。

以下是可用標誌的表格,隨後是對每個標誌的更詳細解釋。

標誌

含義

ASCII, A

使諸如 \w\b\s\d 等轉義符僅匹配具有相應屬性的 ASCII 字元。

DOTALL, S

使 . 匹配任何字元,包括換行符。

IGNORECASE, I

執行不區分大小寫的匹配。

LOCALE, L

執行與區域設定相關的匹配。

MULTILINE, M

多行匹配,影響 ^$

VERBOSE, X (表示“擴充套件”)

啟用詳細的 RE,它可以組織得更清晰易懂。

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),‘ſ’(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 標誌將導致生成的編譯物件對 \w 使用這些 C 函式;這速度較慢,但也使 \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 字串中的空白將被忽略,除非空白位於字元類中或以未轉義的反斜槓開頭;這使您可以更清晰地組織和縮排 RE。此標誌還允許您在 RE 中放置會被引擎忽略的註釋;註釋由 '#' 標記,該標記既不在字元類中,也沒有以未轉義的反斜槓開頭。

例如,以下是一個使用 re.VERBOSE 的 RE;看看它是否更易於閱讀?

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)

如果沒有詳細設定,RE 將如下所示

charref = re.compile("&#(0[0-7]+"
                     "|[0-9]+"
                     "|x[0-9a-fA-F]+);")

在上面的示例中,Python 的自動連線字串文字功能已被用於將 RE 分解為較小的部分,但它仍然比使用 re.VERBOSE 的版本更難理解。

更多模式能力

到目前為止,我們只介紹了正則表示式的部分功能。在本節中,我們將介紹一些新的元字元,以及如何使用組來檢索匹配文字的部分內容。

更多元字元

還有一些我們尚未介紹的元字元。其中大部分將在本節中介紹。

接下來要討論的一些剩餘的元字元是零寬度斷言。它們不會導致引擎在字串中前進;相反,它們根本不消耗任何字元,只是成功或失敗。例如,\b 是一個斷言,表示當前位置位於單詞邊界;\b 本身不會改變位置。這意味著零寬度斷言不應該重複,因為如果它們在給定位置匹配一次,顯然可以無限次匹配。

|

選擇,或“或”運算子。如果 AB 是正則表示式,A|B 將匹配任何匹配 AB 的字串。| 的優先順序非常低,以便在您選擇多字元字串時使其合理地工作。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'>

其次,在字元類中,這裡沒有使用此斷言的需要,為了與 Python 的字串字面值相容,\b 代表退格字元。

\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 始終存在;它是整個正則表示式,因此 match object 方法都將組 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 模組中的一個 RE 示例。

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.barautoexec.batsendmail.cfprinters.conf

現在,考慮將問題複雜化一點;如果你想匹配副檔名不是 bat 的檔名,該怎麼辦?以下是一些不正確的嘗試:

.*[.][^b].*$ 上面的第一次嘗試試圖透過要求副檔名的第一個字元不是 b 來排除 bat。這是錯誤的,因為該模式也不匹配 foo.bar

.*[.]([^b]..|.[^a].|..[^t])$

當你嘗試透過要求匹配以下情況之一來修補第一個解決方案時,表示式會變得更加混亂:副檔名的第一個字元不是 b;第二個字元不是 a;或者第三個字元不是 t。這接受 foo.bar 並拒絕 autoexec.bat,但它需要一個三字母的副檔名,並且不會接受帶有兩個字母副檔名的檔名,例如 sendmail.cf。我們將再次使模式複雜化,以努力修復它。

.*[.]([^b].?.?|.[^a]?.?|..?[^t]?)$

在第三次嘗試中,第二個和第三個字母都是可選的,以便允許匹配短於三個字元的副檔名,例如 sendmail.cf

模式現在變得非常複雜,這使得它難以閱讀和理解。更糟糕的是,如果問題發生變化,並且你想同時排除 batexe 作為副檔名,則模式會變得更加複雜和混亂。

負前瞻可以消除所有這些混亂:

.*[.](?!bat$)[^.]*$ 負前瞻意味著:如果表示式 bat 在此點不匹配,請嘗試模式的其餘部分;如果 bat$ 確實匹配,則整個模式將失敗。需要尾部的 $ 來確保允許類似 sample.batch 的內容,其中副檔名僅以 bat 開頭。[^.]* 確保當檔名中存在多個點時模式有效。

現在排除另一個檔名副檔名很容易;只需將其作為斷言中的替代項新增即可。以下模式排除以 batexe 結尾的檔名:

.*[.](?!bat$|exe$)[^.]*$

修改字串

到目前為止,我們只是對靜態字串執行搜尋。正則表示式通常也用於透過以下模式方法以各種方式修改字串:

方法/屬性

目的

split()

將字串拆分為列表,並在 RE 匹配的任何位置進行拆分。

sub()

查詢 RE 匹配的所有子字串,並將其替換為不同的字串。

subn()

sub() 執行相同的操作,但返回新字串和替換次數。

拆分字串

模式的 split() 方法會在 RE 匹配的任何位置拆分字串,並返回各個部分的列表。它類似於字串的 split() 方法,但在你可以拆分的分隔符中提供了更多的通用性;字串 split() 僅支援按空格或固定字串拆分。正如你所期望的那樣,也有一個模組級 re.split() 函式。

.split(string[, maxsplit=0])

透過正則表示式的匹配項拆分 *string*。如果在 RE 中使用捕獲括號,則它們的內容也將作為結果列表的一部分返回。如果 *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])

返回透過將string中最左側不重疊的 RE 出現的地方替換為替換值replacement而獲得的字串。如果未找到模式,則返回未更改的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)會被替換為 RE 中相應組匹配的子字串。這使你可以將原始文字的部分內容併入生成的替換字串中。

此示例匹配單詞 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 替換 wordre.sub() 看起來是用於此目的的函式,但請考慮 replace() 方法。請注意,replace() 也會替換單詞內的 word,將 swordfish 變為 sdeedfish,但樸素的 RE word 也會這樣做。(為了避免對單詞的部分執行替換,模式必須是 \bword\b,以便要求 word 在兩側都有單詞邊界。這使得該工作超出了 replace() 的能力範圍。)

另一個常見的任務是從字串中刪除單個字元的每次出現,或將其替換為另一個單個字元。你可以使用類似 re.sub('\n', ' ', S) 的方法來執行此操作,但 translate() 能夠執行這兩項任務,並且比任何正則表示式操作都快。

簡而言之,在轉向 re 模組之前,請考慮是否可以使用更快更簡單的字串方法解決你的問題。

貪婪與非貪婪

重複正則表示式時(如 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*$")

反饋

正則表示式是一個複雜的主題。本文件是否幫助您理解了它們?是否有不清楚的部分,或者您遇到的問題在這裡沒有涵蓋?如果是這樣,請向作者傳送改進建議。

關於正則表示式最完整的書籍幾乎可以肯定是 Jeffrey Friedl 的《精通正則表示式》,由 O'Reilly 出版。不幸的是,它完全專注於 Perl 和 Java 風格的正則表示式,並且根本不包含任何 Python 材料,因此它不能作為 Python 程式設計的參考。(第一版涵蓋了 Python 現在已移除的 regex 模組,這對您沒有太大幫助。)請考慮從您的圖書館借閱它。