Python正則表達式
標簽: Python 正則表達式 翻譯
autor: Author: A.M. Kuchling amk@amk.ca
https://docs.python.org/2/howto/regex.html#
摘要
這份教程是Python中使用
re
模塊操作正則表達式的入門教程,相對於庫參考手冊,它提供了一份更優雅的介紹。
簡介
re
模塊在1.5版本加入Python,並提供Perl風格的正則模式。之前的Python版本提供了regex
模塊,它提供Emacs風格的正則模式,regex
模塊在Python2.5被完全移除。
正則表達式是嵌入Python的微小的、高度專門化的語言,可以通過re
模塊訪問。用這門微小語言,你可以為你想要匹配的字符串指定一個規則;這可能包括英文句子,電子郵件地址,Tex命令或其他任何你喜歡的。然后你可以問:這個字符串匹配該模式嗎?或,這個字符串是否存在一個匹配模式?使用re你還可以修改、切割字符串。
正則模式編譯成字節碼,然后被用C語言編寫的正則引擎執行。對於高級用法(advanced use),我們需要關注引擎如何執行一個給定的RE,並以合適的方式編寫RE使得字節碼可以更快執行。該文檔不會包含正則表達式優化,因為你需要你對匹配引擎內部有一個清晰的理解。
正則模式語言相對簡單,並很嚴格,所以並不是所有的字符串處理任務都可以用正則表達式完成。有些任務也可以用RE完成,但是表達式會很復雜。這種情況下,你最好編寫Python代碼完成;雖然Python代碼比精心構造的RE慢,但卻更好理解。
簡單模式
我們從最簡單的模式開始學習。由於RE被用來操作字符串,所有我們從最常見任務開始:字符匹配。
關於正則表達式所隱匿的計算機科學知識(確定和非確定有限自動機),你可以參考任一本有關編譯器構造的書。
匹配字符
大部分字符都只會匹配它本身。例如:正則模式test
會准確匹配字符串test。(當然,你可以使用大小寫不敏感模式,讓該模式匹配Test或TEST,后面會詳細介紹。)
該規則存在例外;有些字符是特殊元字符,它不會匹配它自身。這些元字符通常表示一些不尋常的匹配操作,或者通過重復、修改匹配意義來影響正則模式的其他部門。該文檔的大部分都是有關不同元字符的討論。
這里有元字符的完整列表,接下來會詳細討論他們的含義。
. ^ $ * + ? { } [ ] \ | ( )
我們討論的第一組元字符是[
和]
。他們用來指定字符類,表示你想要匹配的字符集合。字符集可以單獨列出,也可以通過兩個字符外加一個-
分隔符表明一個范圍。例如,[abc]
會匹配字符a,b,c中的任一個;它和[a-c]
相同,或者是用范圍表示字符集。如果你只想匹配小寫字符,你的正則模式可以是[a-z]
元字符在字符類中沒有特殊含義。比如,[akm$]
只能匹配a, k, m, $中的任一個;$通常是元字符,但是在字符類中,它就沒有特殊含義了。
你還可以通過集合的補集來匹配字符類中沒有列出的字符。可以通過在字符類前加上^
;^
在字符類之外只會匹配^
自身。如:[^5]
可以匹配除5之外的任何字符(需要消耗一個字符,不能匹配空)。
可能最重要的元字符就是反斜杠\
了。在Python字符串字面量中,\
之后可以跟不同的字符,表示特殊的序列。你也可以對元字符進行轉義,這樣在模式中也可以匹配他們。比如,你想匹配]
或\
,那你可以在他們之前加上\
:\]
和\\
。
一些以\
開頭的特殊序列代表我們經常使用的字符集,如數字集合,字母集合,或除空白之外的其他字符。下面我會討論這些可用的特殊序列子集。他們等價的字符類是字節串模式的。他們的完整列表和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僅有這點能力,它也確實沒有什么高級之處。另一個能力是指定正則模式的某部分的重復匹配次數。
我們先來看*
。它不匹配字符'*',它指定它之前的字符可以匹配0次或多次,而不是只能一次。
例如,ca*t
可以匹配'ct','cat','caaat'等等。在正則引擎內部有一個限制,由於C語言的int類型,所以最多可以匹配一個字符20億次。你幾乎不可能有足夠的內存構造這么大的字符串,所以你很少會碰到這個限制。
重復匹配默認是貪婪的,匹配引擎會匹配盡可能多的字符。如果模式的后部分不匹配,匹配引擎會回退,嘗試少一些的匹配次數。
通過詳細匹配步驟解釋該問題。讓我們考慮模式a[bcd]*b
,它會匹配a,0個或多個字符類[bcd]中的字符,最后以b結尾。現在想象用這個模式匹配字符串'abcbd'。
- 匹配a;
- 引擎匹配[bcd]*,匹配盡可能多的字符,然后到達字符串末尾;
- 嘗試匹配b,但是現在已經到達字符串末尾,所有失敗;
- 回退一個位置,[bcd]*少匹配一個字符;
- 嘗試匹配b,但是最后一個字符是d,匹配失敗;
- 再次回退,[bcd]*只匹配bc;
- 再次嘗試,當前位置是b,匹配成功。
至此已經到達模式末尾,它已經成功匹配了abcb。這證明,匹配引擎會匹配盡可能多的字符,如果找不到匹配,那么他會回退,並嘗試匹配模式的剩余部分。它會回退直到它匹配[bcd]*0次,如果還失敗,那引擎可以斷定字符串和模式不匹配。
另一個有關重復的元字符是+
,注意它和*
的不同,+至少需要匹配一次;
還有一些重復量詞?
,它匹配0次或1次,我們可以認為他是標記某些字符是可選的。
最復雜的重復量詞是{m,n}
,其中m和n是十進制整數。該量詞表示最少重復m次,最多n次。你還可以忽略m或者n;忽略m表示最少匹配0次,忽略n代表無窮大,實際上,上限是之前提到的20億。
精簡主義者可能發現其他的三種量詞可以用這種記法表示:{0,}和*等價,{1,}和+等價,{0,1}和?等價。但是你最好使用*,+和?,因為他們更簡潔易讀。
使用正則
我們已經學了一點正則模式了,那么在Python中如何使用他們呢?re
模塊提供一個正則引擎接口,允許你編譯re
對象,然后執行匹配操作。
編譯正則表達式
正則表達式編譯成模式對象,它擁有不同的方法,用來執行模式搜索和替換操作。
>>> import re
>>> p = re.compile('ab*')
>>> p
<_sre.SRE_Pattern object at 0x...>
re.compile
參數還有一個可選的flag參數,從而支持特殊的語法特性。比如:
>>> p = re.compile('ab*', re.IGNORECASE)
正則模式以字符串形式傳遞給re.compile
。之所以這樣做,因為正則表達式不是Python語音核心的一部分,也沒有什么特殊的語法表示他們。(並不是所有的應用程序都需要正則表達式,所以也沒有必要包含他們,使得Python語言規范更臃腫)。相反,re
模塊只是一個簡單的C擴展模塊,和socket,zlib模塊一樣。
正則模式放進字符串使得使得Python核心比較簡單,但是它也有一個很頭疼的問題,這就是下一節的主題。
麻煩的反斜杠\
這前面的描述中,我們知道正則表達式使用\
來表示特殊的字符序列(如\d
)和進行元字符轉義(如\[
)。這和Python字符串字面量的某些字符用法沖突。
比如你想寫一個正則模式匹配字符串"\section"(不包括引號),這在LaTeX文件中很常見。從想要匹配的字符串開始,你需要在每一個反斜杠和元字符前插入反斜杠進行轉義,所以正則模式是\\section
,這也是需要傳遞給re.compile()的字符串。但是,為了用字符串字面量表示這個模式,每一個反斜杠需要再一次轉義。
字符串 | 階段 |
---|---|
\section | 需要匹配的字符串 |
\\section | 為re.compile()對反斜杠轉義 |
"\\\\section" | 為字符串字面量對反斜杠轉義 |
注:在md源文件中,需要對\進行轉義,所有一個想要顯示一個反斜杠就要在md源文件中輸入兩個反斜杠。
簡而言之,為了匹配一個反斜杠'',正則模式字符串需要寫成"\\\\"(四個反斜杠),因為正則表達式是'\\',然后每一個包含在字符串字面量中的反斜杠需要表示為'\\'。這種大量的重復反斜杠,使得模式字符串很難理解。
譯注:據此,在Python中分析一個正則模式字符串時,先看模式字符串字面量,然后看傳遞給re.compile的模式,再在正則引擎中分析最終的匹配模式。
解決方法是使用Python的原始字符串標記法,在r前綴開頭的字符串字面量中,反斜杠不進行任何特殊處理。所以r"\n"
是一個包含兩個字符'','n'的字符串,而"\n"是包含一個換行符的字符串。正則表達式通常在Python代碼中寫成原始字符串形式。
正則模式 | 原始字符串 |
---|---|
"ab*" | r"ab*" |
"\\\\section" | r"\\section" |
"\\w+\\s+\1" | r"\w+\s+\1" |
執行匹配
假如你有一個編譯過的正則表達式對象,你會怎么做?模式對象具有很多的屬性和方法,這里只會列出最重要的。完整的參考手冊請看re庫手冊。
方法/屬性 | 目的 |
---|---|
match() | 模式是否匹配字符串開頭 |
search() | 掃描字符串,檢查和模式相匹配的位置 |
findall() | 查找所有和模式相匹配的子串,並以列表形式返回 |
finditer() | 查找所有和模式相匹配的子串,並以迭代器方式返回 |
如果不匹配,search()和match()返回None。如果匹配,則會返回一個match對象,該對象包含匹配信息:起始和終止信息,匹配的子串等。
你可以在交互環境學習re模塊,如果你可以訪問Thinter,那么你可以看看redemo.py,這是一個Python示范工程,允許你輸入一個模式和字符串,然后輸出匹配結果,它在調試復雜的正則表達式時很有用。Kodos也是開發和測試正則模式的有力交互工具。
我們的教程使用標准Python解釋器測試我們的例子:
Python 2.2.2 (#1, Feb 10 2003, 12:57:01)
>>> import re
>>> p = re.compile('[a-z]+')
>>> p #doctest: +ELLIPSIS
<_sre.SRE_Pattern object at 0x...>
現在你可以使用不同的字符串測試模式[a-z]+
,該模式不匹配空串,並返回None。
現在我們用"tempo"進行測試,這時,match()會返回一個match對象。
>>> m = p.match('tempo')
>>> m
<_sre.SRE_Match object at 0x...>
match對象具有很多方法和屬性,最重要的包括如下:
方法/屬性 | 目的 |
---|---|
group() | 返回和模式相匹配的字符串 |
start() | 匹配的起始位置 |
end() | 匹配的截止位置 |
span() | 返回匹配的位置元組:(start, end) |
>>> m.group()
'tempo'
>>> m.start(), m.end()
(0, 5)
>>> m.span()
(0, 5)
group()返回和模式相匹配的子串,由於match()只檢查字符串開始位置,因此start()函數總是返回0。而search()函數會掃描整個字符串,所以匹配開始位置可能不是0.
>>> print p.match('::: message')
None
>>> m = p.search('::: message'); print m
<_sre.SRE_Match object at 0x...>
>>> m.group()
'message'
>>> m.span()
(4, 11)
而在實用程序中,經常是在一個變量中保存match對象,然后檢查它是否為空。如:
p = re.compile( ... )
m = p.match( 'string goes here' )
if m:
print 'Match found: ', m.group()
else:
print 'No match'
還有另外兩個方法返回模式的所有匹配,findall()方法返回匹配字符串的列表。
>>> p = re.compile('\d+')
>>> p.findall('12 drummers drumming, 11 pipers piping, 10 lords a-leaping')
['12', '11', '10']
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()等。這些方法以模式字符串作為第一個參數,其他的對應參數保持不變,並依然返回None和match對象。
>>> print re.match(r'From\s+', 'Fromage amk')
None
>>> re.match(r'From\s+', 'From amk Thu May 14 19:12:10 1998')
<_sre.SRE_Match object at 0x...>
在這種情況下,這些方法也創建一個模式對象,然后調用合適的方法,他們也會在緩存里保存編譯過的模式對象, 所以使用相同的正則模式會更快。
一般來說,更推薦使用編譯過的模式對象然后調用它的方法,而不是模塊級函數。
ref = re.compile( ... )
entityref = re.compile( ... )
charref = re.compile( ... )
starttagopen = re.compile( ... )
編譯標志
編譯標志允許你修改正則表達式的某些工作方式,re模塊為編譯標志提供兩個名字,如re.IGNORECASE和re.I。可以使用位或運算指定多個編譯標志,如re.I | re.M
表示同時設定I和M標志。
可用標志列表:
標志 | 含義 |
---|---|
DOTALL, S | 使. 可以匹配換行符 |
IGNORECASE, I | 忽略大小寫 |
LOCALE, L | 本地化標志 |
MULTILINE, M | 多行匹配,影響^ 和$ 的含義 |
VERBOSE, X | 寬松排列,使得正則模式更易讀 |
UNICODE, U | 使得一些一些轉義如\w,\b,\s取決於字符編碼 |
- I,IGNORECASE
- 忽略大小寫,並且不考慮本地化。除非設置了LOCALE標志;
- L,LOCALE
-
使得\w,\W,\b,\B取決於本地化;
Locales是一個C庫特性,主要用來幫助處理程序中不同語言的差異,如果你在處理法文文本,你想用\w+來匹配單詞,但是\w只匹配字符集[A-Za-z];它不匹配'é'或'ç'。如果你的系統經過合適的配置並且選擇了法文本地化,那么C函數會告訴程序'é'應該被當成一個字母。在編譯模式對象時使用LOCALE對象會使編譯對象對\w使用那些C函數;這會更慢,但是卻可以讓\w+匹配法文單詞。 - M,MULTILINE
-
通常,
^
只會匹配字符串開頭,$
只會匹配字符串結尾和或者字符串結尾處的換行符。如果設立了這個標志,^
會匹配字符串開頭和每行字符串開頭(每個換行符之后),$
會匹配字符串結尾和每一行結尾(每個換行符之前)。 - S,DOTALL
-
使得
.
可以匹配換行符; - U,UNICODE
- 使得\w, \W, \b, \B, \d, \D, \s 和 \S取決於unicode字符集屬性;
- X,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操作符。它具有非常低的優先級,使得你處理多個字符串時可以合理工作,如crow|servo可以匹配crow或者servo,而不是cro,w或者s,然后ervo。為了匹配
|
,使用\\|
,或者放進字符集里如[|]
-
^
- 匹配字符串開頭,除非設置了MULTILINE標志。在MULTILINE標志里,它也可以匹配換行符之后的位置;
-
$
-
匹配字符串結尾,也可以匹配每一個換行符之前的位置。使用
\$
或者放進字符集里如[|]
匹配它自身。
>>> print re.search('}$', '{block}')
<_sre.SRE_Match object at 0x...>
>>> print re.search('}$', '{block} ')
None
>>> print re.search('}$', '{block}\n')
<_sre.SRE_Match object at 0x...>
-
\A
-
只匹配字符串開頭。在非MULTILINE模式中,
\A
和^
是等價的。在MULTILINE模式中,\A
還是只匹配字符串開頭,開始^
可以匹配任意換行符之前的位置。 -
\Z
-
和
\A
類似,只匹配字符串結尾。 -
\b
- 單詞邊界,這是一個零寬斷言,只匹配單詞的開始或者結尾處。單詞b被定義為字母和數字序列,所以單詞結尾由空白字符或者非字母數字字符標記。比如:
>>> p = re.compile(r'\bclass\b')
>>> print p.search('no class at all')
<_sre.SRE_Match object at 0x...>
>>> print p.search('the declassified algorithm')
None
>>> print p.search('one subclass is')
None
使用\b
有兩點微妙之處需要特別注意。它和Python的字符串字面量發生了沖突,在字符串字面量中,\b表示回退字符,ascii值為8。如果你不使用原始字符串,Python會把它轉換為回退字符,那么你的正則模式不會按你期望的進行匹配。比如上面的例子,我們忽略r試一試:
>>> p = re.compile('\bclass\b')
>>> print p.search('no class at all')
None
>>> print p.search('\b' + 'class' + '\b')
<_sre.SRE_Match object at 0x...>
第二,在字符類中,該斷言不會使用,\b表示回退字符,這和Python字符串字面量兼容。
-
\B
- 零寬斷言,和\b相反。匹配非單詞邊界位置。
分組
通常需要使用正則表達式捕獲更多的信息。通常,我們把正則表達式進行分組來匹配不同部分,這樣來字符串進行分析。如,RFC-822 header line被分成名字和值,他們之間用:
分開,像這樣:
From: author@example.com
User-Agent: Thunderbird 1.5.0.9 (X11/20061227)
MIME-Version: 1.0
To: editor@example.com
我們可以編寫一個模式來處理,匹配一個完整header line時,一個分組處理名字,另外一個分組處理值。
分組由元字符(
,)
標記,和數學表達式括號類似,他們把括號內的表達式分組,你可以使用重復量詞重復分組內容,比如*
,+
, ?
, {m,n}
。如,(ab)*
可以匹配0次或多次ab。
>>> p = re.compile('(ab)*')
>>> print p.match('ababababab').span()
(0, 10)
用括號標記的分組同時會捕獲他們匹配的文本的start和end索引;我們可以通過給group(),start(),end()和span()傳遞參數檢索到他們。分組編號從0開始,0分組總是存在,它代表整個正則表達式,所以match對象方法總是以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')
>>> p.search('Paris in the the spring').group()
'the the'
后面你會發現,反向引用在正則替換中非常有用。
非捕獲分組和命名分組
精巧的正則表達式可能誰使用很多分組,即用分組捕獲關心的子串,也用來分組正則模式自身。在復雜的模式中,跟蹤分組號往往很困難,我們使用兩特性來幫助處理該問題。
Perl 5為標准正則表達式增加了很多額外的特性,Python re模塊支持其中的大部分。想要在保持和標准正則表達式沒有明顯不同的情況下,選擇新的單擊元字符和以反斜杠開始的特殊序列很困難。例如,如果你使用&
作為新的元字符,老的表達式認為它是個普通字符,因此沒有通過\&
和[&]進行轉義。
perl開發者的解決方案是:使用(?...)
作為擴展語法。?
立即跟在括號后面是一個語法錯誤,因為?
它沒有重復任何東西,所以這不會引入兼容性問題。跟在?之后的字符表示擴展方式,所以(?=foo)
和(?:foo)
是不同的。(前者是前向斷言,后者是非捕獲分組)
Python又對perl擴展語法進行了擴展。如果?之后是P,表示這是Python特定擴展,當前存在兩種這類擴展:(?P<name>...)
定義了命名分組,(?P=name)
定義了命名分組的反向引用。如果將來perl 5的將來版本使用不同的語法增加了類似的特性,re模塊也會改變來支持新語法,同時為了保持兼容也會保留Python特定語法。
我們已經看到了一般的擴展語法,現在可以返回到使用該特性簡化復雜正則模式中的分組了。由於分組從左到右編號,在復雜正則表達式中很難正確跟蹤編號。更改這樣復雜的正則表達式更犯人,只要插入了一個括號,在此之后所有的括號編號全部改變。
有時你想使用分組收集正則表達式的一部分,但是你不想取回分組中的內容,你可以使用非捕獲分組(?:...)
, ...可以用任意其他的正則表達式代替。
>>> m = re.match("([abc])+", "abc")
>>> m.groups()
('c',)
>>> m = re.match("(?:[abc])+", "abc")
>>> m.groups()
()
譯注:這里有一個疑問,為什么m.groups()返回的是('c', ),而不是('a', )
除了不能取回分組匹配的內容外,非捕獲分組和捕獲分組的行為完全相同,你可以在里面放所有的東西,可以使用重復量詞,你可以使用分組進行嵌套(捕獲和非捕獲)。(?:...)
在修改一個已經存在的模式時很有用,因為你可以在不修改其他分組編號情況下插入新的分組。需要指出的是,在搜索時捕獲和非捕獲分組的性能並沒有差異,不存在一個比另外一個更快。
更有意義的一個特性是命名分組:分組可以使用名字引用。
命名分組語法是特定Python擴展:(?P<name>...)
。命名分組和捕獲分組行為完全相同,同時給分組加了一個額外的名字。match對象對捕獲分組的所有操作同樣對命名分組有用,既可以使用編號也可以使用名字引用分組。命名分組也有編號,所以你可以用兩種方式抽取數據:
>>> p = re.compile(r'(?P<word>\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<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'"')
相對於使用第9分組,顯然使用m.group('zonem')抽取數據更方便。
反向引用語法引用分組編號,如(...)\1
。這里有一個變種:使用組名而不是編號引用分組。這又是另外一個Python擴展:(?P=name)
表示命名分組內容在當前位置需要再次匹配。查找重復單詞的正則模式(\b\w+)\s+\1
也可以寫成(?P<word>\b\w+)\s+(?P=word)
:
>>>
>>> p = re.compile(r'(?P<word>\b\w+)\s+(?P=word)')
>>> p.search('Paris in the the spring').group()
'the the'
前向斷言
另外一種零寬斷言是前向斷言,同時存在正向和負向前向斷言兩種形式。
-
(?=...)
- 肯定前向斷言。如果由 ...表示的正則表達式成功匹配當前位置,則成功,否則失敗。但是,一旦包含的表達式已經被嘗試,匹配引擎不會再次向前處理;從斷言開始的位置開始向右嘗試剩下的模式。
-
(?!...)
- 否定前向斷言。和正向斷言相反,如果包含的表達式不匹配字符串當前位置,則匹配成功。
讓我們用一個例子來說明前向斷言的用處。比如,你想匹配包含擴展名的所有文件,模式相當簡單:
.*[.].*$
現在考慮對問題做微小的修改,如果你想匹配擴展名不是bat的文件,你會怎么解決?下面是一些錯誤的方法。
.*[.][^b].*$
該模式要求文件擴展名不以b開頭,但是這是錯誤的。因為它也不匹配其他的文件擴展名如:foo.bar
.*[.]([^b]..|.[^a].|..[^t])$
這種模式更混亂:它要求文件擴展名第一個字母不是b,第二個不是a,第三個不是t。該模式匹配foo.bar,也不匹配auto.bat。但是該模式要求文件擴展名是三個字母,所以不會匹配兩個字母的擴展名如mail.cf。然后對模式進行如下修改:
.*[.]([^b].?.?|.[^a]?.?|..?[^t]?)$
在該模式中,第二個和第三個字母是可選的,所以可以匹配少於三個字母的文件擴展名如mail.cf。
模式現在已經很復雜了,很難讀也難以理解。更糟糕的是,如果問題發生改變,不匹配bat和exe結尾的文件擴展名,模式會變得更加復雜。
否定前向斷言解決了所有這些困惑:
.*[.](?!bat$)[^.]*$
該否定前向斷言意思是:如果表達式bat不匹配該位置,那么嘗試匹配剩余的模式;如果bat$匹配,那么模式失敗。尾隨的$是必須的,因為這樣可以確保匹配類似sample.batch。[^.]*確保在文件名中有很多點號時也可以正確工作。
現在排除其他的文件擴展名也變得簡單:使它變成斷言的選擇結構即可。例如,下面的模式排除bat和exe結尾的擴展名:
.*[.](?!bat$|exe$)[^.]*$
修改字符串
目前為止,我們都是對固定字符串執行簡單的匹配操作。實際上,RE也可以用來修改字符串,使用以下的模式方法:
方法 | 目標 |
---|---|
split() | 切割字符串,返回list;只要匹配RE就切割。 |
sub() | 查找所有和RE匹配的子串,然后用一個不同的子串代替。 |
subn() | 和sub()一樣,但是返回新的字符串和替換的次數。 |
切割字符串
只要和RE匹配,模式的split()方法就可以切割字符串,返回一個list。和字符串的split()方法類似,但是分隔符可以更通用;字符串的split()方法只支持空白符或者固定的字符串。如你所願,也存在一個模塊級別re.split()函數。
- split(string[, maxsplit=0])
- 通過正則匹配切割字符串。如果模式中包含捕獲分組括號,那么捕獲分組的內容也會成為返回列表的一部分。如果maxsplit參數不為0,那么最多執行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中使用捕獲分組括號,那么他們的值也會成為list的一部分。試比較:
>>> 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('[\W]+', 'Words, words, words.')
['Words', 'words', 'words', '']
>>> re.split('([\W]+)', 'Words, words, words.')
['Words', ', ', 'words', ', ', 'words', '.', '']
>>> re.split('[\W]+', 'Words, words, words.', 1)
['Words', 'words, words.']
查找和替換
另一個常用操作是查找模式匹配,然后進行替換。sub()函數帶有一個replacement參數,它可以是一個字符串,也可以是函數,然后進行處理。
- .sub(replacement, string[, count=0])
- 在 string參數中,對最左邊的非重疊的模式匹配部分替換成 replacement,並返回新字符串。如果找不到匹配的模式,返回的字符串不便。可選的count參數代表替換的次數,默認為0,代表替換所有的匹配。
>>> 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()功能相同,但是它返回一個包含替換字符串和替換次數的二元組:
>>> 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'會轉化為換行符,未知的轉義會直接保留。而反向引用,如'\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參數也可以是函數,如果是函數,那么對於模式的每一次不重疊匹配,都會調用該函數一次。在每一次調用中,函數接收match對象,並執行操作。
例如下面的例子,replacement函數把十進制數轉化為16進制:
>>> 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()函數,那么模式作為第一個參數,模式可能通過對象或者字符串的形式提供。如果你需要指定RE標志,你可以使用模式對象作為第一個第一個參數,也可以在模式字符串中使用嵌入標志,如:sub("(?i)b+", "x", "bbbb BBBB")
返回'x x'。
常見問題
正則表達式是很強大的工具,不過由於它不太直觀,下面討論RE中的一些常見問題。
使用字符串方法
首先,在你使用RE之前,優先考慮使用字符串方法是否可以解決。對於一些固定的字符串,並不需要使用RE特性的操作,使用字符串方法可以更快。
match()和search()對比
- match()方法從字符串開頭進行匹配;
- search()掃描字符串,返回第一次匹配;
貪婪和非貪婪匹配
貪婪匹配嘗試匹配盡可能多的字符,而非貪婪匹配嘗試匹配盡可能少的字符。非貪婪匹配對於類似html標記的工作有用。但是需要指出的是,使用RE解析xml和html非常繁瑣,你最好使用專用的解析模塊。
使用re.VERBOSE
你已經注意到,RE記法非常緊湊,但是非常難讀。re的復雜性來源於反斜杠,括號,和元字符。對於這種RE,建議指定re.VERBOSE標志,這樣你可以以更清晰的方式組織正則模式。首先,模式中空白符會被忽略(字符類中的空白符除外,不能忽略),然后你還可以給模式添加注釋;
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*$")
更易讀。