非捕獲組命名組
精心設計的正規表示式可能會劃分很多組,這些組不僅可以匹配相關的子串,還能夠對正規表示式本身進行分組和結構化。在複雜的正規表示式中,由於有太多的組,因此通過組的序號來跟蹤和使用會變得困難。有兩個新的功能可以幫你解決這個問題——非捕獲組和命名組——它們都使用了乙個公共的正規表示式擴充套件語法。我們先來看看這個表示式擴充套件語法是什麼。
正規表示式的擴充套件語法
眾所周知,perl 5 為標準的正規表示式增加了許多強大的功能。perl 的開發者們並不能選擇乙個新的元字元或者通過反斜槓構造乙個新的特殊序列來實現擴充套件的功能。因為這樣會和標準的正規表示式發生衝突。比如你想選擇
&作為擴充套件功能的元字元(在標準正規表示式中,
&沒有特殊意義),但這樣的話,已經按照標準語法寫出來的正規表示式就不得不修改,因為它們中包含的
'&'意願上只是把它當做普通字元來匹配而已。
小甲魚解釋:看起來很是頭疼的相容性問題,perl 的開發者們是如何解決的呢?請接著看......
最終,perl 的開發者們決定使用
(?...)
作為擴充套件語法。問號
?緊跟在左小括號
(後邊,本身是乙個語法錯誤的寫法,因為
?前邊沒有東西可以重複,所以這樣就解決了相容性的問題
(理由是語法正確的正規表示式肯定不會這麼寫嘛~)
。然後,緊跟在
?後邊的字元則表示哪些擴充套件語法會被使用。例如
(?=foo)
表示一種新的擴充套件功能(前向斷言),
(?:foo)
則表示另一種擴充套件功能(乙個包含子串
foo的非捕獲組)。
python 支援 perl 的一些擴充套件語法,並且在此基礎上還增加了乙個擴充套件語法。如果緊跟在問號
?後邊的是
p,那麼可以肯定這是乙個 python 的擴充套件語法。
好,既然我們已經知道了如何對正規表示式的標準語法進行擴充套件,那我們回來看看這些擴充套件語法在複雜的正規表示式中是如何應用的。
非捕獲組
第乙個我們要講的是非捕獲組。有時候你知識需要用乙個組來表示部分正規表示式,你並不需要這個組去匹配任何東西,這時你可以通過非捕獲組來明確表示你的意圖。非捕獲組的語法是
(?:...)
,這個
...你可以替換為任何正規表示式。
>>> m = re.match("([abc])+", "abc")
>>> m.groups()
('c',)
>>> m = re.match("(?:[abc])+", "abc")
>>> m.groups()()
複製**
小甲魚解釋:「捕獲」就是匹配的意思啦,普通的子組都是捕獲組,因為它們能從字串中匹配到資料。
除了你不能從非捕獲組獲得匹配的內容之外,其他的非捕獲組跟普通子組沒有什麼區別了。你可以在裡邊放任何東西,使用重複功能的元字元,或者跟其他子組進行巢狀(捕獲的或者非捕獲的子組都可以)。
當你需要修改乙個現有的模式的時候,(?:...) 是非常有用的。原始是新增乙個非捕獲組並不會影響到其他(捕獲)組的序號。值得一提的是,在搜尋的速度上,捕獲組和非捕獲組的速度是沒有任何區別的。
命名組我們再來看另外乙個重要功能:命名組。普通子組我們使用序列來訪問它們,命名組則可以使用乙個有意義的名字來進行訪問。
命名組的語法是 python 特有的擴充套件語法:
(?p)
。很明顯,
< >
裡邊的
name
就是命名組的名字啦。命名組除了有乙個名字標識之外,跟其他捕獲組是一樣的。
匹配物件的所有方法不僅可以處理那些由數字引用的捕獲組,還可以處理通過字串引用的命名組。除了使用名字訪問,命名組仍然可以使用數字序號進行訪問:
>>> p = re.compile(r'(?p\b\w+\b)')
>>> m = p.search( '(((( lots of punctuation )))' )
>>> m.group('word')
'lots'
>>> m.group(1)
'lots'
複製**
命名組非常好用,因為它讓你可以使用乙個好記的名字代替一些毫無意義的數字。下邊是來自 imaplib 模組的例子:
internaldate = re.compile(r'internaldate "'
r'(?p[ 123][0-9])-(?p[a-z][a-z][a-z])-'
r'(?p[0-9][0-9][0-9][0-9])'
r' (?p[0-9][0-9]):(?p[0-9][0-9]):(?p[0-9][0-9])'
r' (?p[-+])(?p[0-9][0-9])(?p[0-9][0-9])'
r'"')
複製**
很明顯,使用
m.group('zonem')
訪問匹配內容要比使用數字 9 更簡單明瞭。
正規表示式中,反向引用的語法像
(...)\1
是使用序號的方式來訪問子組;在命名組裡,顯然也是有對應的變體:使用名字來代替序號。其擴充套件語法是
(?p=name)
,含義是該
name
指向的組需要在當前位置再次引用。那麼搜尋兩個單詞的正規表示式可以寫成
(\b\w+)\s+\1
,也可以寫成
(?p\b\w+)\s+(?p=word)
:>>> p = re.compile(r'(?p\b\w+)\s+(?p=word)')
>>> p.search('paris in the the spring').group()
'the the'
複製**
前向斷言
我們要講解的另乙個零寬斷言是前向斷言,前向斷言可以分為前向肯定斷言和前向否定斷言兩種形式。
(?=...)
前向肯定斷言。如果當前包含的正規表示式(這裡以 ... 表示)在當前位置成功匹配,則代表成功,否則失敗。一旦該部分正規表示式被匹配引擎嘗試過,就不會繼續進行匹配了;剩下的模式在此斷言開始的地方繼續嘗試。
(?!...)
前向否定斷言。這跟前向肯定斷言相反(不匹配則表示成功,匹配表示失敗)。
為了使大家更易懂,我們舉個例子來證明這玩意是真的很有用。大家考慮乙個簡單的正規表示式模式,這個模式的作用是匹配乙個檔名。我們都知道,檔名是用
.將名字和副檔名分隔開的。例如在 fishc.txt 中,fishc 是檔案的名字,.txt 是副檔名。
這個正規表示式其實挺簡單的:
.*[.].*$
注意,這裡用於分隔的
.是乙個元字元,所以我們使用
[.]剝奪了它的特殊功能。還有
$,我們使用
$確保字串剩餘的部分都包含在副檔名中。所以這個正規表示式可以匹配 fishc.txt,foo.bar,autoexec.bat,sendmail.cf,printers.conf 等。
現在我們來考慮一種複雜一點的情況,如果你想匹配副檔名不是 bat 的檔案,你的正規表示式應該怎麼寫呢?
我們先來看下你有可能寫錯的嘗試:
.*[.][^b].*$
這裡為了排除
bat,我們先嘗試排除副檔名的第乙個字元為非
b。但這是錯誤的開始,因為
foo.bar
字尾名的第乙個字元也是 b。
為了彌補剛剛的錯誤,我們試了這一招:
.*[.]([^b]..|.[^a].|..[^t])$
我們不得不承認,這個正規表示式變得很難看......但這樣第乙個字元不是
b,第二個字元不是
a,第三個字元不是
t......這樣正好可以接受
foo.bar
,排除
autoexec.bat
。但問題又來了,這樣的正規表示式要求副檔名必須是三個字元,比如
sendmail.cf
就會被排除掉。
好吧,我們接著修復問題:
.*[.]([^b].?.?|.[^a]?.?|..?[^t]?)$
在第三次嘗試中,我們讓第二個和第三個字元變成可選的。這樣就可以匹配稍短的副檔名,比如
sendmail.cf
。不得不承認,我們把事情搞砸了,現在的正規表示式變得艱澀難懂外加奇醜無比!!
更慘的是如果需求改變了,例如你想同時排除
bat和
exe副檔名,這個正規表示式模式就變得更加複雜了......
噹噹噹噹!主角登場,其實,乙個前向否定斷言就可以解決你的難題:
.*[.](?!bat$).*$
我們來解釋一下這個前向否定斷言的含義:如果正規表示式
bat在當前位置不匹配,嘗試剩下的部分正規表示式;如果
bat匹配成功,整個正規表示式將會失敗(因為是前向否定斷言嘛^_^)。
(?!bat$)
末尾的
$是為了確保可以正常匹配像
sample.batch
這種以
bat開始的副檔名。
同樣,有了前向否定斷言,要同時排除
bat和
exe副檔名,也變得相當容易:
.*[.](?!bat$|exe$).*$
Python3 如何優雅地使用正規表示式(詳解六)
修改字串 我們已經介紹完如何對字元進行搜尋,接下來我們講講正規表示式如何修改字串。正規表示式使用以下方法修改字串 方法用途 split 在正規表示式匹配的地方進行分割,並返回乙個列表 sub 找到所有匹配的子字串,並替換為新的內容 subn 跟 sub 幹一樣的勾當,但返回新的字串以及替換的數目 分...
Python3 如何優雅地使用正規表示式(詳解二)
現在我們開始來寫一些簡單的正規表示式吧。python 通過 re 模組為正規表示式引擎提供乙個介面,同時允許你將正規表示式編譯成模式物件,並用它們來進行匹配。解釋 re 模組是使用 c 語言編寫,所以效率比你用普通的字串方法要高得多 將正規表示式進行編譯 compile 也是為了進一步提高效率 後邊...
Python3 如何優雅地使用正規表示式(詳解六)
我們已經介紹完如何對字元進行搜尋,接下來我們講講正規表示式如何修改字串。正規表示式使用以下方法修改字串 方法用途 split 在正規表示式匹配的地方進行分割,並返回乙個列表 sub 找到所有匹配的子字串,並替換為新的內容 subn 跟 sub 幹一樣的勾當,但返回新的字串以及替換的數目 正規表示式的...