正則表達式HOWTO?
- 作者
A.M. Kuchling <amk@amk.ca>
摘要
本文檔是在Python中使用 re
模塊使用正則表達式的入門教程。 它提供了比“標準庫參考”中相應部分更平和的介紹。
概述?
正則表達式(稱為RE,或正則,或正則表達式模式)本質上是嵌入在Python中的一種微小的、高度專業(yè)化的編程語言,可通過 re
模塊獲得。 使用這種小語言,你可以為要匹配的可能字符串集指定規(guī)則;此集可能包含英語句子,電子郵件地址,TeX命令或你喜歡的任何內容。 然后,您可以詢問諸如“此字符串是否與模式匹配?”或“此字符串中的模式是否匹配?”等問題。 你還可以使用正則修改字符串或以各種方式將其拆分。
正則表達式模式被編譯成一系列字節(jié)碼,然后由用 C 編寫的匹配引擎執(zhí)行。對于高級用途,可能需要特別注意引擎如何執(zhí)行給定的正則,并將正則寫入以某種方式生成運行速度更快的字節(jié)碼。 本文檔未涉及優(yōu)化,因為它要求你充分了解匹配引擎的內部結構。
正則表達式語言相對較小且受限制,因此并非所有可能的字符串處理任務都可以使用正則表達式完成。 還有一些任務 可以 用正則表達式完成,但表達式變得非常復雜。 在這些情況下,你最好編寫 Python 代碼來進行處理;雖然 Python 代碼比精心設計的正則表達式慢,但它也可能更容易理解。
簡單模式?
我們首先要了解最簡單的正則表達式。 由于正則表達式用于對字符串進行操作,因此我們將從最常見的任務開始:匹配字符。
有關正則表達式(確定性和非確定性有限自動機)的計算機科學的詳細解釋,你可以參考幾乎所有有關編寫編譯器的教科書。
匹配字符?
大多數(shù)字母和字符只會匹配自己。 例如,正則表達式 test
將完全匹配字符串 test
。 (你可以啟用一個不區(qū)分大小寫的模式,讓這個正則匹配 Test
或 TEST
,稍后會詳細介紹。)
這條規(guī)則有例外;一些字符是特殊的 metacharacters ,并且不匹配自己。 相反,它們表示應該匹配一些與眾不同的東西,或者通過重復它們或改變它們的含義來影響正則的其他部分。 本文檔的大部分內容都致力于討論各種元字符及其功能。
這是元字符的完整列表;它們的意思將在本HOWTO的其余部分討論。
. ^ $ * + ? { } [ ] \ | ( )
我們將看到的第一個元字符是 [
和 ]
。 它們用于指定字符類,它是你希望匹配的一組字符。 可以單獨列出字符,也可以通過給出兩個字符并用 '-'
標記將它們分開來表示一系列字符。 例如, [abc]
將匹配任何字符 a
、 b
或 c
;這與 [a-c]
相同,它使用一個范圍來表示同一組字符。 如果你只想匹配小寫字母,你的正則是 [a-z]
。
Metacharacters (except \
) are not active inside classes. For example, [akm$]
will
match any of the characters 'a'
, 'k'
, 'm'
, or '$'
; '$'
is
usually a metacharacter, but inside a character class it's stripped of its
special nature.
你可以通過以下方式匹配 complementing 設置的字符類中未列出的字符。這通過包含一個 '^'
作為該類的第一個字符來表示。 例如,[^5]
將匹配除 '5'
之外的任何字符。 如果插入符出現(xiàn)在字符類的其他位置,則它沒有特殊含義。 例如:[5^]
將匹配 '5'
或 '^'
。
也許最重要的元字符是反斜杠,\
。 與 Python 字符串文字一樣,反斜杠后面可以跟各種字符,以指示各種特殊序列。它也用于轉義所有元字符,因此您仍然可以在模式中匹配它們;例如,如果你需要匹配 [
或 \
,你可以在它們前面加一個反斜杠來移除它們的特殊含義:\[
或 \\
。
一些以 '\'
開頭的特殊序列表示通常有用的預定義字符集,例如數(shù)字集、字母集或任何非空格的集合。
讓我們舉一個例子:\w
匹配任何字母數(shù)字字符。 如果正則表達式模式以字節(jié)類表示,這相當于類 [a-zA-Z0-9_]
。如果正則表達式是一個字符串,\w
將匹配由 unicodedata
模塊提供的 Unicode 數(shù)據(jù)庫中標記為字母的所有字符。 通過在編譯正則表達式時提供 re.ASCII
標志,可以在字符串模式中使用更為受限制的 \w
定義。
以下特殊序列列表不完整。 有關 Unicode 字符串模式的序列和擴展類定義的完整列表,請參閱標準庫參考中的最后一部分 正則表達式語法 。通常,Unicode 版本匹配 Unicode 數(shù)據(jù)庫中相應類別中的任何字符。
\d
匹配任何十進制數(shù)字;這等價于類
[0-9]
。\D
匹配任何非數(shù)字字符;這等價于類
[^0-9]
。\s
匹配任何空白字符;這等價于類
[ \t\n\r\f\v]
。\S
匹配任何非空白字符;這相當于類
[^ \t\n\r\f\v]
。\w
匹配任何字母與數(shù)字字符;這相當于類
[a-zA-Z0-9_]
。\W
匹配任何非字母與數(shù)字字符;這相當于類
[^a-zA-Z0-9_]
。
這些序列可以包含在字符類中。 例如,[\s,.]
是一個匹配任何空格字符的字符類或者 ','
,或 '.'
。
本節(jié)的最后一個元字符是 .
。 它匹配除換行符之外的任何內容,并且有一個可選模式( re.DOTALL
)甚至可以匹配換行符。 .
常用于你想匹配“任何字符”的地方。
重復?
能夠匹配不同的字符集合是正則表達式可以做的第一件事,這對于字符串可用方法來說是不可能的。 但是,如果這是正則表達式的唯一額外功能,那么它們就不會有太大的優(yōu)勢。 另一個功能是你可以指定正則的某些部分必須重復一定次數(shù)。
重復中我們要了解的第一個元字符是 *
。 *
與字面字符 '*'
不匹配;相反,它指定前一個字符可以匹配零次或多次,而不是恰好一次。
例如,ca*t
將匹配 'ct'
(0個 'a'
字符),'cat'
(1個 'a'
), 'caaat'
(3個 'a'
字符),等等。
類似 *
這樣的重復是 貪婪的;當重復正則時,匹配引擎將嘗試盡可能多地重復它。 如果模式的后續(xù)部分不匹配,則匹配引擎將回退并以較少的重復次數(shù)再次嘗試。
一個逐步的例子將使這更加明顯。 讓我們考慮表達式 a[bcd]*b
。 這個正則匹配字母 'a'
,類 [bcd]
中的零或多個字母,最后以 'b'
結尾。 現(xiàn)在想象一下這個正則與字符串 'abcbd'
匹配。
步驟 |
匹配 |
說明 |
---|---|---|
1 |
|
正則中的 |
2 |
|
引擎盡可能多地匹配 |
3 |
失敗 |
引擎嘗試匹配 |
4 |
|
回退一次, |
5 |
失敗 |
再次嘗試匹配 |
6 |
|
再次回退,所以 |
6 |
|
再試一次 |
正則現(xiàn)在已經(jīng)結束了,它已經(jīng)匹配了 'abcb'
。 這演示了匹配引擎最初如何進行,如果沒有找到匹配,它將逐步回退并一次又一次地重試正則的其余部分。 它將回退,直到它為 [bcd]*
嘗試零匹配,如果隨后失敗,引擎將斷定該字符串與正則完全不匹配。
另一個重復的元字符是 +
,它匹配一次或多次。 要特別注意 *
和 +
之間的區(qū)別;*
匹配 零次 或更多次,因此重復的任何東西都可能根本不存在,而 +
至少需要 一次。 使用類似的例子,ca+t
將匹配 'cat'
(1 個 'a'
),'caaat'
(3 個 'a'
),但不會匹配 'ct'
。
There are two more repeating operators or quantifiers. The question mark character, ?
,
matches either once or zero times; you can think of it as marking something as
being optional. For example, home-?brew
matches either 'homebrew'
or
'home-brew'
.
The most complicated quantifier is {m,n}
, where m and n are
decimal integers. This quantifier means there must be at least m repetitions,
and at most n. For example, a/{1,3}b
will match 'a/b'
, 'a//b'
, and
'a///b'
. It won't match 'ab'
, which has no slashes, or 'a////b'
, which
has four.
你可以省略 m 或 n; 在這種情況下,將假定缺失值的合理值。 省略 m 被解釋為 0 下限,而省略 n 則為無窮大的上限。
Readers of a reductionist bent may notice that the three other quantifiers can
all be expressed using this notation. {0,}
is the same as *
, {1,}
is equivalent to +
, and {0,1}
is the same as ?
. It's better to use
*
, +
, or ?
when you can, simply because they're shorter and easier
to read.
使用正則表達式?
現(xiàn)在我們已經(jīng)看了一些簡單的正則表達式,我們如何在 Python 中實際使用它們? re
模塊提供了正則表達式引擎的接口,允許你將正則編譯為對象,然后用它們進行匹配。
編譯正則表達式?
正則表達式被編譯成模式對象,模式對象具有各種操作的方法,例如搜索模式匹配或執(zhí)行字符串替換。:
>>> import re
>>> p = re.compile('ab*')
>>> p
re.compile('ab*')
re.compile()
也接受一個可選的 flags 參數(shù),用于啟用各種特殊功能和語法變體。 我們稍后將介紹可用的設置,但現(xiàn)在只需一個例子
>>> p = re.compile('ab*', re.IGNORECASE)
正則作為字符串傳遞給 re.compile()
。 正則被處理為字符串,因為正則表達式不是核心Python語言的一部分,并且沒有創(chuàng)建用于表達它們的特殊語法。 (有些應用程序根本不需要正則,因此不需要通過包含它們來擴展語言規(guī)范。)相反,re
模塊只是Python附帶的C擴展模塊,就類似于 socket
或 zlib
模塊。
將正則放在字符串中可以使 Python 語言更簡單,但有一個缺點是下一節(jié)的主題。
反斜杠災難?
如前所述,正則表達式使用反斜杠字符 ('\'
) 來表示特殊形式或允許使用特殊字符而不調用它們的特殊含義。 這與 Python 在字符串文字中用于相同目的的相同字符的使用相沖突。
假設你想要編寫一個與字符串 \section
相匹配的正則,它可以在 LaTeX 文件中找到。 要找出在程序代碼中寫入的內容,請從要匹配的字符串開始。 接下來,您必須通過在反斜杠前面添加反斜杠和其他元字符,從而產(chǎn)生字符串 \\section
。 必須傳遞給 re.compile()
的結果字符串必須是 \\section
。 但是,要將其表示為 Python 字符串文字,必須 再次 轉義兩個反斜杠。
字符 |
階段 |
---|---|
|
被匹配的字符串 |
|
為 |
|
為字符串字面轉義的反斜杠 |
簡而言之,要匹配文字反斜杠,必須將 '\\\\'
寫為正則字符串,因為正則表達式必須是 \\
,并且每個反斜杠必須表示為 \\
在常規(guī)Python字符串字面中。 在反復使用反斜杠的正則中,這會導致大量重復的反斜杠,并使得生成的字符串難以理解。
解決方案是使用 Python 的原始字符串表示法來表示正則表達式;反斜杠不以任何特殊的方式處理前綴為 'r'
的字符串字面,因此 r"\n"
是一個包含 '\'
和 'n'
的雙字符字符串,而 "\n"
是一個包含換行符的單字符字符串。 正則表達式通常使用這種原始字符串表示法用 Python 代碼編寫。
此外,在正則表達式中有效但在 Python 字符串文字中無效的特殊轉義序列現(xiàn)在導致 DeprecationWarning
并最終變?yōu)?SyntaxError
。 這意味著如果未使用原始字符串表示法或轉義反斜杠,序列將無效。
常規(guī)字符串 |
原始字符串 |
---|---|
|
|
|
|
|
|
應用匹配?
一旦你有一個表示編譯正則表達式的對象,你用它做什么? 模式對象有幾種方法和屬性。 這里只介紹最重要的內容;請參閱 re
文檔獲取完整列表。
方法 / 屬性 |
目的 |
---|---|
|
確定正則是否從字符串的開頭匹配。 |
|
掃描字符串,查找此正則匹配的任何位置。 |
|
找到正則匹配的所有子字符串,并將它們作為列表返回。 |
|
找到正則匹配的所有子字符串,并將它們返回為一個 iterator。 |
如果沒有找到匹配, match()
和 search()
返回 None
。如果它們成功, 一個 匹配對象 實例將被返回,包含匹配相關的信息:起始和終結位置、匹配的子串以及其它。
你可以通過交互式實驗 re
模塊來了解這一點。 如果你有 tkinter
,你可能還想查看 Tools/demo/redemo.py,這是 Python 發(fā)行附帶的演示程序。 它允許你輸入正則和字符串,并顯示RE是匹配還是失敗。 redemo.py
在嘗試調試復雜的正則時非常有用。
本 HOWTO 使用標準 Python 解釋器作為示例。 首先,運行 Python 解釋器,導入 re
模塊,然后編譯一個正則
>>> import re
>>> p = re.compile('[a-z]+')
>>> p
re.compile('[a-z]+')
現(xiàn)在,你可以嘗試匹配正則 [a-z]+
的各種字符串。 空字符串根本不匹配,因為 +
表示“一次或多次重復”。 match()
在這種情況下應返回 None
,這將導致解釋器不打印輸出。 你可以顯式打印 match()
的結果,使其清晰。:
>>> p.match("")
>>> print(p.match(""))
None
現(xiàn)在,讓我們嘗試一下它應該匹配的字符串,例如 tempo
。在這個例子中 match()
將返回一個 匹配對象,因此你應該將結果儲存到一個變量中以供稍后使用。
>>> m = p.match('tempo')
>>> m
<re.Match object; span=(0, 5), match='tempo'>
現(xiàn)在你可以檢查 匹配對象 以獲取有關匹配字符串的信息。 匹配對象實例也有幾個方法和屬性;最重要的是:
方法 / 屬性 |
目的 |
---|---|
|
返回正則匹配的字符串 |
|
返回匹配的開始位置 |
|
返回匹配的結束位置 |
|
返回包含匹配 (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
并最終產(chǎn)生 SyntaxError
。 請參閱 反斜杠災難。
findall()
必須先創(chuàng)建整個列表才能返回結果。 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)
模塊級函數(shù)?
你不必創(chuàng)建模式對象并調用其方法;re
模塊還提供了頂級函數(shù) match()
,search()
,findall()
,sub()
等等。 這些函數(shù)采用與相應模式方法相同的參數(shù),并將正則字符串作為第一個參數(shù)添加,并仍然返回 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 '>
本質上,這些函數(shù)只是為你創(chuàng)建一個模式對象,并在其上調用適當?shù)姆椒ā?它們還將編譯對象存儲在緩存中,因此使用相同的未來調用將不需要一次又一次地解析該模式。
你是否應該使用這些模塊級函數(shù),還是應該自己獲取模式并調用其方法? 如果你正在循環(huán)中訪問正則表達式,預編譯它將節(jié)省一些函數(shù)調用。 在循環(huán)之外,由于有內部緩存,沒有太大區(qū)別。
編譯標志?
編譯標志允許你修改正則表達式的工作方式。 標志在 re
模塊中有兩個名稱,長名稱如 IGNORECASE
和一個簡短的單字母形式,例如 I
。 (如果你熟悉 Perl 的模式修飾符,則單字母形式使用和其相同的字母;例如, re.VERBOSE
的縮寫形式為 re.X
。)多個標志可以 通過按位或運算來指定它們;例如,re.I | re.M
設置 I
和 M
標志。
這是一個可用標志表,以及每個標志的更詳細說明。
旗標 |
含意 |
---|---|
|
使幾個轉義如 |
|
使 |
|
進行大小寫不敏感匹配。 |
|
進行區(qū)域設置感知匹配。 |
|
多行匹配,影響 |
|
啟用詳細的正則,可以更清晰,更容易理解。 |
- I
- IGNORECASE
執(zhí)行不區(qū)分大小寫的匹配;字符類和字面字符串將通過忽略大小寫來匹配字母。 例如,
[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 模式下匹配)。 此小寫不考慮當前區(qū)域設置;如果你還設置了LOCALE
標志,則將考慮。
- L
- LOCALE
使
\w
、\W
、\b
、\B
和大小寫敏感匹配依賴于當前區(qū)域而不是 Unicode 數(shù)據(jù)庫。區(qū)域設置是 C 庫的一個功能,旨在幫助編寫考慮到語言差異的程序。例如,如果你正在處理編碼的法語文本,那么你希望能夠編寫
\w+
來匹配單詞,但\w
只匹配字符類[A-Za-z]
字節(jié)模式;它不會匹配對應于é
或?
的字節(jié)。如果你的系統(tǒng)配置正確并且選擇了法語區(qū)域設置,某些C函數(shù)將告訴程序對應于é
的字節(jié)也應該被視為字母。在編譯正則表達式時設置LOCALE
標志將導致生成的編譯對象將這些C函數(shù)用于\w
;這比較慢,但也可以使\w+
匹配你所期望的法語單詞。在 Python 3 中不鼓勵使用此標志,因為語言環(huán)境機制非常不可靠,它一次只處理一個“文化”,它只適用于 8 位語言環(huán)境。默認情況下,Python 3 中已經(jīng)為 Unicode(str)模式啟用了 Unicode 匹配,并且它能夠處理不同的區(qū)域/語言。
- M
- MULTILINE
(
^
和$
還沒有解釋;它們將在以下部分介紹 更多元字符。)通常
^
只匹配字符串的開頭,而$
只匹配字符串的結尾,緊接在字符串末尾的換行符(如果有的話)之前。 當指定了這個標志時,^
匹配字符串的開頭和字符串中每一行的開頭,緊跟在每個換行符之后。 類似地,$
元字符匹配字符串的結尾和每行的結尾(緊接在每個換行符之前)。
- S
- DOTALL
使
'.'
特殊字符匹配任何字符,包括換行符;沒有這個標志,'.'
將匹配任何字符 除了 換行符。
- A
- ASCII
使
\w
、\W
、\b
、\B
、\s
和\S
執(zhí)行僅 ASCII 匹配而不是完整匹配 Unicode 匹配。 這僅對 Unicode 模式有意義,并且對于字節(jié)模式將被忽略。
- X
- 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
版本更難理解。
更多模式能力?
到目前為止,我們只介紹了正則表達式的一部分功能。 在本節(jié)中,我們將介紹一些新的元字符,以及如何使用組來檢索匹配的文本部分。
更多元字符?
我們還沒有涉及到一些元字符。 其中大部分內容將在本節(jié)中介紹。
要討論的其余一些元字符是 零寬度斷言 。 它們不會使解析引擎在字符串中前進一個字符;相反,它們根本不占用任何字符,只是成功或失敗。例如,\b
是一個斷言,指明當前位置位于字邊界;這個位置根本不會被 \b
改變。這意味著永遠不應重復零寬度斷言,因為如果它們在給定位置匹配一次,它們顯然可以無限次匹配。
|
或者“or”運算符。 如果 A 和 B 是正則表達式,
A|B
將匹配任何與 A 或 B 匹配的字符串。|
具有非常低的優(yōu)先級,以便在交替使用多字符字符串時使其合理地工作。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
字邊界。 這是一個零寬度斷言,僅在單詞的開頭或結尾處匹配。 單詞被定義為一個字母數(shù)字字符序列,因此單詞的結尾由空格或非字母數(shù)字字符表示。
以下示例僅當它是一個完整的單詞時匹配
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
這可以通過編寫與整個標題行匹配的正則表達式來處理,并且具有與標題名稱匹配的一個組,以及與標題的值匹配的另一個組。
Groups are marked by the '('
, ')'
metacharacters. '('
and ')'
have much the same meaning as they do in mathematical expressions; they group
together the expressions contained inside them, and you can repeat the contents
of a group with a quantifier, such as *
, +
, ?
, or
{m,n}
. For example, (ab)*
will match zero or more repetitions of
ab
.
>>> p = re.compile('(ab)*')
>>> print(p.match('ababababab').span())
(0, 10)
用 '('
,')'
表示的組也捕獲它們匹配的文本的起始和結束索引;這可以通過將參數(shù)傳遞給 group()
、start()
、end()
以及 span()
。 組從 0 開始編號。組 0 始終存在;它表示整個正則,所以 匹配對象 方法都將組 0 作為默認參數(shù)。 稍后我們將看到如何表達不捕獲它們匹配的文本范圍的組。:
>>> 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 的字符串文字也使用反斜杠后跟數(shù)字以允許在字符串中包含任意字符,因此正則中引入反向引用時務必使用原始字符串。
例如,以下正則檢測字符串中的雙字。:
>>> p = re.compile(r'\b(\w+)\s+\1\b')
>>> p.search('Paris in the the spring').group()
'the the'
像這樣的后向引用通常不僅僅用于搜索字符串 —— 很少有文本格式以這種方式重復數(shù)據(jù) —— 但是你很快就會發(fā)現(xiàn)它們在執(zhí)行字符串替換時 非常 有用。
非捕獲和命名組?
精心設計的正則可以使用許多組,既可以捕獲感興趣的子串,也可以對正則本身進行分組和構建。 在復雜的正則中,很難跟蹤組號。 有兩個功能可以幫助解決這個問題。 它們都使用常用語法進行正則表達式擴展,因此我們首先看一下。
Perl 5 以其對標準正則表達式的強大補充而聞名。 對于這些新功能,Perl 開發(fā)人員無法選擇新的單鍵擊元字符或以 \
開頭的新特殊序列,否則 Perl 的正則表達式與標準正則容易混淆。 例如,如果他們選擇 &
作為一個新的元字符,舊的表達式將假設 &
是一個普通字符,并且不會編寫 \&
或 [&]
。
Perl 開發(fā)人員選擇的解決方案是使用 (?...)
作為擴展語法。 括號后面的 ?
是一個語法錯誤,因為 ?
沒有什么可重復的,所以這并沒有引入任何兼容性問題。 緊跟在 ?
之后的字符表示正在使用什么擴展名,所以 (?=foo)
是一個東西(一個正向的先行斷言)和 (?:foo)
是其它東西( 包含子表達式 foo
的非捕獲組)。
Python 支持一些 Perl 的擴展,并增加了新的擴展語法用于 Perl 的擴展語法。 如果在問號之后的第一個字符為 P
,即表明其為 Python 專屬的擴展。
現(xiàn)在我們已經(jīng)了解了一般的擴展語法,我們可以回到簡化復雜正則中組處理的功能。
有時你會想要使用組來表示正則表達式的一部分,但是對檢索組的內容不感興趣。 你可以通過使用非捕獲組來顯式表達這個事實: (?:...)
,你可以用任何其他正則表達式替換 ...
。:
>>> m = re.match("([abc])+", "abc")
>>> m.groups()
('c',)
>>> m = re.match("(?:[abc])+", "abc")
>>> m.groups()
()
除了你無法檢索組匹配內容的事實外,非捕獲組的行為與捕獲組完全相同;你可以在里面放任何東西,用重復元字符重復它,比如 *
,然后把它嵌入其他組(捕獲或不捕獲)。 (?:...)
在修改現(xiàn)有模式時特別有用,因為你可以添加新組而不更改所有其他組的編號方式。 值得一提的是,捕獲和非捕獲組之間的搜索沒有性能差異;兩種形式?jīng)]有一種更快。
更重要的功能是命名組:不是通過數(shù)字引用它們,而是可以通過名稱引用組。
命名組的語法是Python特定的擴展之一: (?P<name>...)
。 name 顯然是該組的名稱。 命名組的行為與捕獲組完全相同,并且還將名稱與組關聯(lián)。 處理捕獲組的 匹配對象 方法都接受按編號引用組的整數(shù)或包含所需組名的字符串。 命名組仍然是給定的數(shù)字,因此你可以通過兩種方式檢索有關組的信息:
>>> 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'}
命名組很有用,因為它們允許你使用容易記住的名稱,而不必記住數(shù)字。 這是來自 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
,指的是組的編號。 當然有一種變體使用組名而不是數(shù)字。 這是另一個 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
。
現(xiàn)在,考慮使更復雜一點的問題;如果你想匹配擴展名不是 bat
的文件名怎么辦? 一些錯誤的嘗試:
.*[.][^b].*$
上面的第一次嘗試試圖通過要求擴展名的第一個字符不是 b
來排除 bat
。 這是錯誤的,因為模式也與 foo.bar
不匹配。
.*[.]([^b]..|.[^a].|..[^t])$
當你嘗試通過要求以下一種情況匹配來修補第一個解決方案時,表達式變得更加混亂:擴展的第一個字符不是 b
。 第二個字符不 a
;或者第三個字符不是 t
。 這接受 foo.bar
并拒絕 autoexec.bat
,但它需要三個字母的擴展名,并且不接受帶有兩個字母擴展名的文件名,例如 sendmail.cf
。 為了解決這個問題,我們會再次使模式復雜化。
.*[.]([^b].?.?|.[^a]?.?|..?[^t]?)$
在第三次嘗試中,第二個和第三個字母都是可選的,以便允許匹配的擴展名短于三個字符,例如 sendmail.cf
。
模式現(xiàn)在變得非常復雜,這使得它難以閱讀和理解。 更糟糕的是,如果問題發(fā)生變化并且你想要將 bat
和 exe
排除為擴展,那么該模式將變得更加復雜和混亂。
負面前向消除了所有這些困擾:
.*[.](?!bat$)[^.]*$
負向前向意味著:如果表達式 bat
此時不匹配,請嘗試其余的模式;如果 bat$
匹配,整個模式將失敗。 尾隨的 $
是必需的,以確保允許像 sample.batch
這樣的擴展只以 bat
開頭的文件能通過。 [^.]*
確保當文件名中有多個點時,模式有效。
現(xiàn)在很容易排除另一個文件擴展名;只需在斷言中添加它作為替代。 以下模塊排除以 bat
或 exe
:
.*[.](?!bat$|exe$)[^.]*$
修改字符串?
到目前為止,我們只是針對靜態(tài)字符串執(zhí)行搜索。 正則表達式通常也用于以各種方式修改字符串,使用以下模式方法:
方法 / 屬性 |
目的 |
---|---|
|
將字符串拆分為一個列表,在正則匹配的任何地方將其拆分 |
|
找到正則匹配的所有子字符串,并用不同的字符串替換它們 |
|
與 |
分割字符串?
模式的 split()
方法在正則匹配的任何地方拆分字符串,返回一個片段列表。 它類似于 split()
字符串方法,但在分隔符的分隔符中提供了更多的通用性;字符串的 split()
僅支持按空格或固定字符串進行拆分。 正如你所期望的那樣,還有一個模塊級 re.split()
函數(shù)。
- .split(string[, maxsplit=0])
通過正則表達式的匹配拆分 字符串。 如果在正則中使用捕獲括號,則它們的內容也將作為結果列表的一部分返回。 如果 maxsplit 非零,則最多執(zhí)行 maxsplit 次拆分。
你可以通過傳遞 maxsplit 的值來限制分割的數(shù)量。 當 maxsplit 非零時,將最多進行 maxsplit 次拆分,并且字符串的其余部分將作為列表的最后一個元素返回。 在以下示例中,分隔符是任何非字母數(shù)字字符序列。:
>>> 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', '.', '']
模塊級函數(shù) re.split()
添加要正則作為第一個參數(shù),但在其他方面是相同的。:
>>> 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()
方法接受一個替換值,可以是字符串或函數(shù),也可以是要處理的字符串。
- .sub(replacement, string[, count=0])
返回通過替換 replacement 替換 string 中正則的最左邊非重疊出現(xiàn)而獲得的字符串。 如果未找到模式,則 string 將保持不變。
可選參數(shù) count 是要替換的模式最大的出現(xiàn)次數(shù);count 必須是非負整數(shù)。 默認值 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()
方法完成相同的工作,但返回一個包含新字符串值和已執(zhí)行的替換次數(shù)的 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 也可以是一個函數(shù),它可以為你提供更多控制。 如果 replacement 是一個函數(shù),則為 pattern 的每次非重疊出現(xiàn)將調用該函數(shù)。 在每次調用時,函數(shù)都會傳遞一個匹配的 匹配對象 參數(shù),并可以使用此信息計算所需的替換字符串并將其返回。
在以下示例中,替換函數(shù)將小數(shù)轉換為十六進制:
>>> 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()
函數(shù)時,模式作為第一個參數(shù)傳遞。 模式可以是對象或字符串;如果需要指定正則表達式標志,則必須使用模式對象作為第一個參數(shù),或者在模式字符串中使用嵌入式修飾符,例如: sub("(?i)b+", "x", "bbbb BBBB")
返回 'x x'
。
常見問題?
正則表達式對于某些應用程序來說是一個強大的工具,但在某些方面,它們的行為并不直觀,有時它們的行為方式與你的預期不同。 本節(jié)將指出一些最常見的陷阱。
使用字符串方法?
有時使用 re
模塊是一個錯誤。 如果你匹配固定字符串或單個字符類,并且你沒有使用任何 re
功能,例如 IGNORECASE
標志,那么正則表達式的全部功能可能不是必需的。 字符串有幾種方法可以使用固定字符串執(zhí)行操作,它們通常要快得多,因為實現(xiàn)是一個針對此目的而優(yōu)化的單個小 C 循環(huán),而不是大型、更通用的正則表達式引擎。
一個例子可能是用另一個固定字符串替換一個固定字符串;例如,你可以用 deed
替換 word
。 re.sub()
看起來像是用于此的函數(shù),但請考慮 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)
有時你會被誘惑繼續(xù)使用 re.match()
,只需在你的正則前面添加 .*
。抵制這種誘惑并使用 re.search()
代替。 正則表達式編譯器對正則進行一些分析,以加快尋找匹配的過程。 其中一個分析可以確定匹配的第一個特征必須是什么;例如,以 Crow
開頭的模式必須與 'C'
匹配。 分析讓引擎快速掃描字符串,尋找起始字符,只在找到 'C'
時嘗試完全匹配。
添加 .*
會使這個優(yōu)化失效,需要掃描到字符串的末尾,然后回溯以找到正則的其余部分的匹配。 使用 re.search()
代替。
貪婪與非貪婪?
當重復一個正則表達式時,就像在 a*
中一樣,最終的動作就是消耗盡可能多的模式。 當你嘗試匹配一對對稱分隔符,例如 HTML 標記周圍的尖括號時,這個事實經(jīng)常會讓你感到困惑。因為 .*
的貪婪性質, 用于匹配單個 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>'
中的 '>'
,而這并不是你想要的結果。
In this case, the solution is to use the non-greedy quantifiers *?
, +?
,
??
, or {m,n}?
, which match as little text as possible. In the above
example, the '>'
is tried immediately after the first '<'
matches, and
when it fails, the engine advances a character at a time, retrying the '>'
at every step. This produces just the right result:
>>> print(re.match('<.*?>', s).group())
<html>
(請注意,使用正則表達式解析 HTML 或 XML 很痛苦??於K的模式將處理常見情況,但 HTML 和 XML 有特殊情況會破壞明顯的正則表達式;當你編寫正則表達式處理所有可能的情況時,模式將非常復雜。使用 HTML 或 XML 解析器模塊來執(zhí)行此類任務。)
使用 re.VERBOSE?
到目前為止,你可能已經(jīng)注意到正則表達式是一種非常緊湊的表示法,但它們并不是非常易讀。 具有中等復雜度的正則可能會成為反斜杠、括號和元字符的冗長集合,使其難以閱讀和理解。
對于這樣的正則,在編譯正則表達式時指定 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*$")
反饋?
正則表達式是一個復雜的主題。 這份文檔是否有助于你理解它們? 是否存在不清楚的部分,或者你遇到的問題未在此處涉及? 如果是,請向作者發(fā)送改進建議。
關于正則表達式的最完整的書幾乎肯定是由 O'Reilly 出版的 Jeffrey Friedl 的 Mastering Regular Expressions 。 不幸的是,它專注于 Perl 和 Java 的正則表達式,并且根本不包含任何 Python 材料,因此它不能用作 Python 編程的參考。 (第一版涵蓋了 Python 現(xiàn)在刪除的 regex
模塊,這對你沒有多大幫助。)考慮從你的圖書館中查找它。