正則表示式HOWTO

作者:

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

引言

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

正則表示式模式被編譯成一系列位元組碼,然後由一個用 C 編寫的匹配引擎執行。對於高階用法,可能需要仔細注意引擎如何執行給定的 RE,並以某種方式編寫 RE 以生成執行更快的位元組碼。本文件不涉及最佳化,因為它需要你對匹配引擎的內部原理有很好的理解。

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

簡單模式

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

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

匹配字元

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

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

以下是元字元的完整列表;它們在本文件的其餘部分將進行討論。

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

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

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

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

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

一些以 '\' 開頭的特殊序列表示預定義的字元集,這些字元集通常很有用,例如數字集、字母集或非空白字元集。

我們來看一個例子:\w 匹配任何字母數字字元。如果正則表示式模式以位元組表示,這等價於類 [a-zA-Z0-9_]。如果正則表示式模式是字串,\w 將匹配 unicodedata 模組提供的 Unicode 資料庫中標記為字母的所有字元。你可以在字串模式中使用更受限的 \w 定義,方法是在編譯正則表示式時提供 re.ASCII 標誌。

以下特殊序列列表不完整。有關序列的完整列表以及 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},其中 *m* 和 *n* 是十進位制整數。此量詞表示必須至少重複 *m* 次,最多重複 *n* 次。例如,a/{1,3}b 將匹配 'a/b''a//b''a///b'。它不會匹配沒有斜槓的 'ab',也不會匹配有四個斜槓的 'a////b'

你可以省略 *m* 或 *n*;在這種情況下,會為缺失的值假設一個合理的值。省略 *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。)多個標誌可以透過按位或運算來指定;例如,re.I | re.M 設定 IM 兩個標誌。

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

Flag

含義

ASCII, A

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

DOTALL, S

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

IGNORECASE, I

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

LOCALE, L

執行本地化感知的匹配。

MULTILINE, M

多行匹配,影響 ^$

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

啟用詳細 REs,可以更清晰、更易懂地組織它們。

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 位區域設定。Unicode 匹配在 Python 3 中對於 Unicode (str) 模式預設已啟用,並且能夠處理不同的區域設定/語言。

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 而改變。這意味著零寬度斷言永遠不應該重複,因為如果它們在給定位置匹配一次,那麼它們顯然可以匹配無限多次。

|

交替,或者“或”運算子。如果 *A* 和 *B* 是正則表示式,A|B 將匹配任何匹配 *A* 或 *B* 的字串。| 具有非常低的優先順序,以便在交替多字元字串時使其合理工作。Crow|Servo 將匹配 'Crow''Servo',而不是 'Cro'、一個 'w' 或一個 'S',以及 'ervo'

要匹配字面值 '|',請使用 \|,或將其括在字元類中,如 [|]

^

匹配行的開頭。除非設定了 MULTILINE 標誌,否則這隻會匹配字串的開頭。在 MULTILINE 模式下,這也會在字串中每個換行符之後立即匹配。

例如,如果你只想匹配行開頭的單詞 From,則使用的 RE 是 ^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

僅匹配字串的末尾。

\Z

\z 相同。為了與舊版 Python 相容。

\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 轉換為退格符,你的 RE 將無法按預期匹配。以下示例看起來與我們之前的 RE 相同,但省略了 RE 字串前面的 '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 相反,僅在當前位置不在單詞邊界時匹配。

分組

通常你需要獲取比 RE 是否匹配更多的資訊。正則表示式常用於透過將 RE 分成幾個子組來剖析字串,這些子組匹配不同的感興趣的元件。例如,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 總是存在的;它是整個 RE,因此 匹配物件 方法都將分組 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 的字串字面量也使用反斜槓後跟數字來允許在字串中包含任意字元,因此在 RE 中包含反向引用時務必使用原始字串。

例如,以下 RE 檢測字串中的重複單詞。

>>> p = re.compile(r'\b(\w+)\s+\1\b')
>>> p.search('Paris in the the spring').group()
'the the'

像這樣的反向引用通常只用於字串搜尋——很少有文字格式以這種方式重複資料——但你很快就會發現它們在執行字串替換時 *非常* 有用。

非捕獲組和命名組

複雜的 REs 可能會使用許多分組,既用於捕獲感興趣的子字串,也用於對 RE 本身進行分組和結構化。在複雜的 REs 中,跟蹤分組編號變得困難。有兩個功能可以幫助解決這個問題。它們都使用通用的正則表示式擴充套件語法,所以我們先來看一下。

Perl 5 以其對標準正則表示式的強大補充而聞名。對於這些新功能,Perl 開發者無法選擇新的單鍵元字元或以 \ 開頭的新特殊序列,而不會使 Perl 的正則表示式與標準 REs 產生令人困惑的差異。例如,如果他們選擇 & 作為新元字元,那麼舊錶達式會假定 & 是一個普通字元,並且不會透過編寫 \&[&] 來轉義它。

Perl 開發者選擇的解決方案是使用 (?...) 作為擴充套件語法。? 緊跟在括號後是一個語法錯誤,因為 ? 沒有要重複的內容,所以這沒有引入任何相容性問題。緊跟在 ? 後面的字元指示正在使用什麼擴充套件,所以 (?=foo) 是一回事(一個正向先行斷言),而 (?:foo) 是另一回事(一個包含子表示式 foo 的非捕獲組)。

Python 支援 Perl 的幾種擴充套件,並在 Perl 的擴充套件語法之上添加了一個擴充套件語法。如果問號後的第一個字元是 P,你就知道它是 Python 特有的擴充套件。

現在我們已經瞭解了通用擴充套件語法,我們可以回到簡化複雜 REs 中分組操作的功能。

有時你會想用一個組來表示正則表示式的一部分,但對檢索組的內容不感興趣。你可以透過使用一個非捕獲組來明確這一點:(?:...),其中你可以用任何其他正則表示式替換 ...

>>> 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().']

有時您不僅對分隔符之間的文字感興趣,還需要知道分隔符是什麼。如果在 RE 中使用了捕獲括號,則它們的值也將作為列表的一部分返回。比較以下呼叫:

>>> 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 作為第一個引數新增,但其他方面是相同的。

>>> 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 中最左邊不重疊的 RE 出現項而獲得的字串。如果未找到模式,則返回 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 迴圈,而不是大型、更通用的正則表示式引擎。

一個例子可能是用另一個字串替換一個固定的字串;例如,您可能將 word 替換為 deedre.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>

RE 匹配 '<html>' 中的 '<',而 .* 則消耗了字串的其餘部分。不過,RE 中還有更多內容,> 無法在字串末尾匹配,因此正則表示式引擎必須逐個字元地回溯,直到找到與 > 匹配的內容。最終匹配從 '<html>' 中的 '<' 擴充套件到 '</title>' 中的 '>',這並非您想要的。

在這種情況下,解決方案是使用非貪婪量詞 *?+???{m,n}?,它們匹配 *儘可能少* 的文字。在上面的示例中,在第一個 '<' 匹配後立即嘗試 '>',當它失敗時,引擎一次前進一個字元,在每一步都重試 '>'。這會產生正確的結果:

>>> print(re.match('<.*?>', s).group())
<html>

(請注意,使用正則表示式解析 HTML 或 XML 是痛苦的。快速而簡陋的模式可以處理常見情況,但 HTML 和 XML 有一些特殊情況會破壞顯而易見的正則表示式;等到你編寫了一個能夠處理所有可能情況的正則表示式時,模式將變得 *非常* 複雜。對於此類任務,請使用 HTML 或 XML 解析器模組。)

使用 re.VERBOSE

到目前為止,您可能已經注意到正則表示式是一種非常緊湊的表示法,但它們的可讀性並不強。中等複雜度的 RE 可能會變成一長串的反斜槓、括號和元字元的集合,使它們難以閱讀和理解。

對於此類 RE,在編譯正則表示式時指定 re.VERBOSE 標誌可能會有所幫助,因為它允許您更清晰地格式化正則表示式。

re.VERBOSE 標誌有幾個作用。正則表示式中 *不在* 字元類內部的空白符將被忽略。這意味著像 dog | cat 這樣的表示式等價於可讀性較差的 dog|cat,但 [a b] 仍將匹配字元 'a''b' 或空格。此外,您還可以在 RE 內部放置註釋;註釋從 # 字元延伸到下一個換行符。當與三引號字串一起使用時,這使得 RE 可以更整齊地格式化:

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 模組,這對您幫助不大。)請考慮從您的圖書館借閱它。