Python中的re
模塊主要進行正則表達式的匹配,以及匹配後進行的相關操作。本文藉助幫助文檔和模塊源碼,從分析類方法和類接口的角度介紹了正則模塊的使用方式,並在最後引入了幾個實例。
什麼是正則表達式
經典意義上的正則表達式(regular expressions)是計算理論中的一個概念,它是用正則運算符( )構造描述語言的表達式。簡單來說,用正則表達式可以描述一類具有某個共同的結構特徵的字符串。比如, 表示的是由一個 或一個 後面跟着任意個 的所有字符串。關於計算理論的知識可以參閱我的這一篇博客:計算理論之正則語言。
在Python中,或者編程語言中,正則表達式的運算符變得更多,語法也更簡練,更易於表現字符串的結構特徵。我們可以使用多種多樣的符號表示字符,甚至可以引用表達式中的一部分作爲它的另一部分。這極大地增強了正則表達式的功能,使得用戶在處理文本時更加的方便、快捷。
Python中的正則模塊
在Python中,關於正則表達式的內容被封裝進一個稱爲re
的模塊中,這個模塊包括以下內容:
CLASSES
builtins.Exception(builtins.BaseException)
error
builtins.object
Match
Pattern
FUNCTIONS
compile(pattern, flags=0)
escape(pattern)
findall(pattern, string, flags=0)
finditer(pattern, string, flags=0)
fullmatch(pattern, string, flags=0)
match(pattern, string, flags=0)
purge()
search(pattern, string, flags=0)
split(pattern, string, maxsplit=0, flags=0)
sub(pattern, repl, string, count=0, flags=0)
subn(pattern, repl, string, count=0, flags=0)
template(pattern, flags=0)
DATA
A = <RegexFlag.ASCII: 256>
ASCII = <RegexFlag.ASCII: 256>
DOTALL = <RegexFlag.DOTALL: 16>
I = <RegexFlag.IGNORECASE: 2>
IGNORECASE = <RegexFlag.IGNORECASE: 2>
L = <RegexFlag.LOCALE: 4>
LOCALE = <RegexFlag.LOCALE: 4>
M = <RegexFlag.MULTILINE: 8>
MULTILINE = <RegexFlag.MULTILINE: 8>
S = <RegexFlag.DOTALL: 16>
U = <RegexFlag.UNICODE: 32>
UNICODE = <RegexFlag.UNICODE: 32>
VERBOSE = <RegexFlag.VERBOSE: 64>
X = <RegexFlag.VERBOSE: 64>
可以看到,這個模塊(實際上re
模塊建立於幾個基模塊之上,但我們可以暫且可以看做一個)定義了3個類、12個函數,以及14個常量(這只是幫助文檔中展示的公開接口,實際上不論是類、函數,還是常量,數量都會更多)。由於正則表達式工作的時候一般不需要用戶自行創建模塊中的對象,而是從函數中接收字符串,並自動地從這些字符串中創建對象,因此我們只需瞭解正則模塊的大致工作模式即可。
首先,從作用上來說,正則模塊主要完成正則表達式對字符串的匹配,然後可以在此基礎上進行進一步的操作。因此我們分兩個部分介紹該模塊,一個是匹配,另一個是操作。
(注:re
模塊可以處理兩種類型的字符串,bytes patterns
和string patterns
。前者是Python中的bytes
對象,後者則是str
對象。兩者在處理時基本相似,因此在本文中暫不做區別分析。)
匹配
幾乎所有的正則表達式函數(除compile()
,escape()
和purge()
以外)都至少接收兩個字符串型參數,一個叫pattern
,表示用戶給定的正則表達式,另一個叫string
,表示待匹配的文本。正則模塊將會在string
中尋找和pattern
匹配的部分。
在匹配之前,正則模塊會先將pattern
通過compile()
函數轉變爲內建的Pattern
對象(當然,傳一個Pattern
對象作爲pattern
也是可以的),然後使用Pattern
類中定義的方法進行之後的操作。對於大部分方法,會將匹配到的內容以Match
對象的形式返回。
能夠被合法轉變爲Pattern
對象的字符串有以下幾種:
- 僅包含普通字符的字符串,比如
"AbcDe"
,"19260817"
等。 - 僅包含特殊字符的字符串。
特殊字符包括以下幾種:'.'
,匹配除換行符以外的任意字符。'^'
,匹配一個字符串的開始。這裏請注意,它並不匹配字符,而是匹配位置。'$'
,匹配字符串的結尾,或字符串結尾處換行符之前。同樣,它也只匹配位置而不匹配字符。'*'
,貪心地匹配它之前正則表達式的0次或更多次重複。
這裏“貪心”的意思是儘可能向後匹配更多的字符。'+'
,貪心地匹配它之前正則表達式的1次或更多次重複。'?'
,貪心地匹配它之前正則表達式的0次或1次出現。
如果'?'
跟在'*'
,'+'
或'?'
後面,則會取消它們的貪心模式。'{m,n}'
,貪心地匹配它之前正則表達式的m
次到n
次出現。它的貪心模式同樣也可以被'?'
取消。'\'
,轉義它之後的字符。它既可以表示正常的轉義字符,如'\n'
等,也可以轉義特殊字符。'[]'
,表示匹配一組字符中的任意一個。
'[^A]'
,表示匹配表達式A
代表的字符串集合的補集。'A|B'
,匹配表達式A
或表達式B
。'(...)'
,匹配括號內的正則表達式,且會將括號內匹配到的內容作爲一個“組”(之後會介紹)。'(?aiLmsux)'
,放在字符串的開頭,設置flag
變量(之後會介紹)。'(?:...)'
,匹配表達式...
但不會將匹配到的內容作爲一個“組”。'(?P<name>...)'
,匹配並分組的同時,給這個組命名爲name
。'(?P=name)'
,匹配命名爲name
的組的內容。'(?#...)'
,註釋,不參與匹配。'A(?=B)'
,斷言(之後會詳細介紹),匹配滿足表達式A
且其後緊跟着的字符串滿足表達式B
的內容。'A(?!B)'
,斷言,匹配滿足表達式A
且其後緊跟着的內容不滿足表達式B
的內容。'(?<=B)A'
,斷言,匹配滿足表達式A
且之前的內容滿足表達式B
的內容。'(?<!B)A'
,斷言,匹配滿足表達式A
且之前的內容不滿足表達式B
的內容。'(?(id/name)yes|no)'
,如果組號爲id
或者組名爲name
的組匹配成功,則用yes
去匹配,否則用no
。
- 同時包含普通字符和特殊字符的字符串。
剛纔我們提到了“轉義字符”的概念,它是由一個'\'
字符再緊接着一個字符組成的用來表示一個用常規方法不太好表示的字符的寫法。下面是由'\'
和特殊字符組成的轉義字符,如果是由一般字符和'\'
組成轉義字符(不包括'\n'
,'\r'
等常用的轉義字符),則仍表示那個字符本身。
'\number'
,表示組號爲number
的組。'\A'
,匹配字符串的開頭,相當於'^'
。'\Z'
,匹配字符串的末尾。'\b'
,匹配單詞開頭或結尾處的空串。'\B'
,匹配非單詞開頭或結尾處的空串。'\d'
,匹配數字,相當於'[0-9]'
。'\D'
,匹配非數字,相當於'[^\d]'
。'\s'
,匹配空白字符,相當於'[ \t\n\r\f\v]'
。'\w'
,匹配單詞字符,相當於'[a-zA-Z0-9_]'
。'\W'
,匹配非單詞字符,相當於'[^\w]'
。
由於正則表達式內部也有自己定義的轉義字符,而我們平常使用的轉義字符在放入一對''
中時會默認被轉義,因此如果想表示正則表達式中的轉義字符,需要使用'\\'
表示一個'\'
,或者使用raw strings
。
下面介紹“分組”的概念。
Match
對象會將匹配到pattern
的出現根據用戶自定義的若干對'('
和')'
符號分成等量的“組”(group),每一組的內容都是pattern
這一次出現的一個子串。這些組之間可以引用,也可以嵌套,但不可以在組被定義之前引用它,比如'(abc)\\2(def)'
這樣的pattern
就是不合法的。引用方式除了組號(從1開始)之外,還可以引用組名(由'(?P<name>...)'
定義,由(?P=name)
引用)。
下面介紹flag
變量。
正則模塊定義了一些默認情況以外的匹配模式,這些模式以若干個flag
的形式被保存。這些flag
有:
A/ASCII
,僅對string patterns
使用,使'\w'
,'\W'
,'\b'
,'\B'
,'\d'
,'\D'
,'\s'
,'\S'
僅匹配ASCII編碼的對應字符。例如,'(?a)\s'
不會匹配全角空格,而'\s'
是可以的。I/IGNORECASE
,匹配的時候不區分大小寫。L/LOCALE
,僅對bytes patterns
使用,讓'\w'
,'\W'
,'\b'
,'\B'
根據當前的語言環境去匹配。(這個flag
變量不常用,官方也不建議使用)M/MULTILINE
,使得'^'
匹配字符串的開頭和每行的開頭;使'$'
匹配字符串的末尾和每行的末尾(默認情況下,'^'
僅匹配字符串的開頭,'$'
僅匹配字符串的末尾或字符串末尾的換行符之前)。S/DOTALL
,使得'.'
匹配任何字符,包括換行符。X/VERBOSE
,允許pattern
出現爲了美觀而多餘的不參與匹配的空格。且每一個換行符前若出現一個普通的'#'
符號(放在'[]'
內部或者和'\'
連用的不算),則將從'#'
到換行符的內容看成註釋。(筆者認爲這個flag
不是很常用,除非你十分想在正則表達式內部寫註釋)U/UNICODE
,和A/ASCII
類似,使得那些字符僅匹配Unicode編碼的對應版本(這個在Python3中是多餘的,因爲Python3默認就是用Unicode匹配)。這個flag
同樣無法用於bytes patterns
。
如上文所述,我們可以通過在pattern
前部加上'(?aiLmsux)'
來指定匹配時的flag
變量。其中'aiLmsux'
分別代表上述的七個flag
。flag
可以同時指定多個,如'(?ai)'
。此外,在re
模塊的大多數函數中,我們也可以通過傳入re.A
的形式來指定flag
。
下面介紹正則表達式中的斷言(assertion)。
有的時候,我們需要匹配那些前後內容滿足一定條件的字符串,比如前面帶有字母的空格、後面不跟着數字的字母等等。我們希望匹配的同時不把前後的所謂“條件字符”也加進來,因此引入了斷言。斷言提供了一個使某些內容參與條件判斷但不加入匹配結果的pattern
寫法。
斷言又分爲兩種,一種叫lookahead assertion,即先行斷言,另一種叫lookbehind assertion,即後行斷言。顧名思義,前者判斷之後的字符,後者判斷之前的。對於先行斷言,我們有兩種:(?=...)
和'(?!...)'
。前者叫做“零寬正向先行斷言”,表示判斷其後緊跟着的字符是否匹配...
,後者叫“零寬負向先行斷言”,表示判斷其後緊跟着的字符是否不匹配...
。類似的,(?<=...)
和'(?<!...)'
就表示判斷前面緊跟着的。有一點需要注意的是,在Python中,不允許出現不定長的斷言,即'(?<=a*bc)d'
是不可以的,但'(?<=abc)d'
可以。
操作
在講述正則模塊中的各函數之前,先介紹一下正則模塊中的Match
類,它是大部分函數返回的對象。
Match
類擁有以下的公開屬性值:
pos
,正則表達式匹配內容的第一個字符的下標。endpos
,正則表達式匹配內容的最後一個字符的下標。lastgroup
,最後一個組的組名,(無命名或者無組則返回None
)。lastindex
,最後一個組的編號。pos
,開始匹配處的下標。re
,傳遞給Match
對象的Pattern
對象。string
,傳遞給Match
對象的字符串。
Match
類的接口函數如下:
expand(template)
,用Match
對象匹配到的組替換template
中的對應內容,template
中可使用\n
,\1
,或\g<name>
等的形式。group([group1, ...])
,返回組號對應的組(字符串或字符串元組)。組號爲0
表示匹配的整個子串。Python3.6以後支持使用下標形式去訪問,即,對於一個Match
對象m
,m[3]
和m.group(3)
的含義相同。groups(default=None)
,返回匹配到的所有組,default
指定當對應組匹配失敗時返回的值。groupdict(default=None)
,以{group_name: match_string, ...}
的形式返回匹配。start(group)
和end(group)
,返回匹配字符串在原子串中的下標。group
默認取0
,也就是整個子串。當組存在但是對匹配沒有貢獻的時候會返回-1
。span([group])
,返回起始點和結束點的元組,相當於(m.start(group), m.end(group))
。
下面正式介紹正則模塊中的接口函數:
compile(pattern, flags=0)
,根據給定的字符串pattern
,返回其對應的Pattern
對象。escape(pattern)
,接收一個字符串,將其中的特殊符號保留其原始意義(自動加上'\'
)並返回。search(pattern, string, flags=0)
,在string
找到pattern
第一處匹配,返回對應的Match
對象。findall(pattern, string, flags=0)
,找出pattern
在string
中的所有出現,以列表形式返回。列表的每一項是一個字符串或着一個元組(如果用戶定義了組,會將組以元組形式返回,而不是返回整個出現)。finditer(pattern, string, flags=0)
,與findall
類似,返回一個迭代器。sub(pattern, repl, string, count=0, flags=0)
,根據repl
將pattern
在string
中對應的匹配做相應替換並返回。repl
可以是一個與pattern
類似的支持正則模塊語法的字符串,也可以是一個函數,該函數接受一個Match
對象,返回一個字符串。subn(pattern, repl, string, count=0, flags=0)
,和sub
類似,但是返回一個元組,元組包括替換後的字符串和替換的次數。purge()
,清理正則表達式緩存。
常用的正則表達式舉例
以下從幾個實例出發介紹正則表達式,以加深理解。
比如,我們要解析一段HTML中的標籤,假設傳進來的是一個字符串s
,包含有若干個形如'<tag>text</tag>'
的標籤,我們需要將這些標籤替換爲'tag:text'
的形式並返回。
比如一段文本:
<composer>Wolfgang Amadeus Mozart</composer>
<author>Samuel Beckett</author>
<city>London</city>
我們希望將其解析爲:
composer:Wolfgang Amadeus Mozart
author:Samuel Beckett
city:London
我們可以這樣編寫函數:
import re
def func(s):
pattern = '<(.*?)>(.*?)</\\1>'
repl = '\\1:\\2'
return re.sub(pattern, repl, s)
代碼中,我們使用'<(.*?)>(.*?)</\\1>'
作爲pattern
,它將匹配一個完整的標籤,且將標籤分出兩個組,第一組內容爲標籤的名字,第二組爲標籤的內容。替換的時候只要分別引用一下然後中間加上':'
即可。
在pattern
中出現了兩處'.*?'
,這是一個非常常用的寫法,它能以非貪心的方式匹配幾乎任何內容。
下面我們討論一個稍微難一些的例子。
比如,現在有一段文字,包含有若干個單詞,比如我們現在要找到其中以'abc'
開頭的單詞。
這個比較簡單,答案直接給出了:'\\b(abc\w*)'
。
那麼,如果我們要匹配不以'abc'
開頭的單詞呢?
我們可以利用斷言,匹配那些在單詞開頭後面不緊跟着'abc'
的內容,即:'\\b(?!abc)(\w+)'
。
那麼,如果我們要匹配不含有'abc'
的單詞呢?
這個可能不太容易想到,我們可以依次匹配單詞中的每一個字符,檢驗它之後是不是緊跟着'abc'
,因此答案爲:'\\b((?:(?!abc)\w)+)\\b'
。(事實上,那個(?:...)
的括號可以不加,但爲了使用findall
函數的方便還是加了)
學習資料推薦
第一推薦官方文檔,雖然是英文版本,但足夠權威和全面。此外,在github上面有一個學習正則表達式的項目learn-regex,有中文版本,可以用作爲輔助資料。