1. 程式人生 > >Python正則表示式:如何使用正則表示式

Python正則表示式:如何使用正則表示式

正則表示式(簡稱RE)本質上可以看作一個小的、高度專業化的程式語言,在Python中可以通過re模組使用它。使用正則表示式,你需要為想要匹配的字串集合指定一套規則,字串集合可以包含英文句子、e-mail地址、TeX命令或者其它任何你希望的字串。然後您能提這樣的問題:“這個字串匹配這個模式嗎?”,或者“在這個字串中存在這個模式的匹配嗎?”。你也能使用正則表示式修改一個字串或者分離它。
正則表示式被編譯到一系列的位元組碼,然後被C語言實現的匹配引擎執行。在一些高階應用場景,必須關注引擎怎麼執行一個RE,以根據引擎的特徵編寫RE提高位元組碼的處理效率。這篇文章不包含優化,優化需要對匹配引擎的內部實現有好的理解。

正則表示式相對小並且存在限制,所以不是所有的字串處理任務都能用正則表示式解決。也存在有些任務可以用正則表示式做,但表示式非常複雜。在這些情況下,更好的選擇是使用Python程式碼處理,但Python程式碼相對正則表示式會更慢,但卻可能更好理解。

正則表示式簡述

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

匹配字串

大部分字母和字元將匹配他們自身,例如,正則表示式test將匹配字串test(你可以開始大小寫不敏感模式,這樣RE可以匹配Test或者TEST)。
這個規則存在例外,一些字元是特殊元字元,不匹配他們自身。他們暗示一些不尋常的事將被匹配,或者他們影響RE的其它部分,例如重複他們或者改變他們的含義。文章的其餘部分都主要討論各種元字元和他們的含義。
下面是元字元的列表,後面將介紹他們的含義:
. ^ $ * + ? { } [ ] \ | ( )
首先我們看[和],他們被用於指定一個字元類,表示你希望匹配的一套字符集。字元能被單獨列出,或者使用'-'來指示字元的範圍,例如:[abc]將匹配字元a、b或c的任意一個;[a-c]也是匹配a、b或c中的任意一個。如果你想匹配僅小寫字母,那麼RE應該為[a-z]。
在類中([ ]內)的元字元不是啟用的,例如:[akm$]將匹配'a'、'k'、'm'或'$'中的任意一個,'$'是一個元字元,但是在字元類中它作為普通字元使用。
你也能排除類中列出的字符集,通過將'^'作為類的第一個字元,注意在類之外的'^'將僅僅匹配'^'字元,例如:[^5]將匹配除了5之外的任何字元。
或許最重要的元字元是反斜槓\。在Python中,反斜槓能被各種字元跟隨作為各種特殊序列使用。它也能被用於取出元字元的特殊性將其作為本身匹配,例如:如果你需要匹配一個[或者\,你能在它們之前帶上一個反斜槓移除它們的特殊含義,即\[或者\\。
以'\'開始的特殊序列中的一些表示了經常被使用的預定義字符集,例如數字集合、字元集合、或者非空白的任意字元集合。
讓我們看一個例子:\w匹配任何字母數字。如果正則表示式模式以位元組為單位,這等價於類[a-zA-Z0-9_]。如果正則表示式模式是字串,則\w將匹配所有被unicodedata模組提供的在Unicode資料庫總的字元。當編譯正則表示式時,你能新增re.ASCII標誌給\w更嚴格的限制。
下面提供了部分特殊序列供參考:
\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(0個a)、cat(1個a)、caaat(3個a)、等等。RE引擎內部會限制a的匹配的數量,但通常足夠了。
重複(例如*)演算法是貪婪的,對於重複的RE,匹配引擎將嘗試儘可能多的重複次數,如果模式的後面部分不匹配,則匹配引擎將回退並再次嘗試更少的重複次數。
例如,考慮表示式a[bcd]*b,這匹配單詞'a',0個或者多個來自類[bcd]的字母,最後以'b'結束。下面是RE匹配abcbd的過程:
1、匹配a:RE匹配a成功;
2、匹配abcbd:引擎匹配[bcd]*,由於儘可能的匹配更多,所以匹配了整個字串;
3、匹配失敗:引擎試著匹配b,但是已經到達字串結尾,因此失敗;
4、匹配abcb:回退,[bcd]*匹配減少一個字元;
5、匹配失敗:再次嘗試b,但當前位置的字元為d;
6、匹配abc:繼續回退,以至於[bcd]*僅匹配bc;
7、匹配abcb:再次嘗試b,這次當前位置的字元為b,匹配成功,結束。
RE最終匹配abcb,整個過程演示了匹配引擎的匹配過程,首先匹配儘可能多的字元,如果不匹配,則不斷回退再次嘗試。它將回退直到[bcd]*匹配0個字元,如果任然失敗,則引擎得出結論“字串不匹配RE”。
另一個重複的元字元是+,匹配一次或者多次。小心*和+之間的不同,*匹配0次或者多次,即可以匹配空;+則需要至少出現一次。例如:ca+t將匹配cat(1個a),caaat(3個a),但不匹配ct。
另外還有兩個重複限定符,其一是問號'?',表示匹配一次或者0次,例如: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表示無限制。
你可能已經注意到,使用最後一個限定符可以取代前面3個限定符:{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模組僅僅是Python包含的一個C語言擴充套件模組,就像socket和zlib模組一樣。
將RE作為字串保持了Python語言的簡單,但也存在不利,例如下一節將講述的內容。

反斜槓問題

如前所述,正則表示式使用反斜槓來表示一些特殊組合或者允許特殊字元作為普通字元使用。這一點和Python對於發斜槓的使用衝突。
你如果想寫一個RE匹配字串\section,我們看看怎麼構造一個正則表示式物件:首先,我們使用整個字串作為正則表示式;其次,找出反斜槓和其它元字元,在它們前面新增反斜槓,變為\\section;最後,字串被傳入到re.compile(),由於傳入的必須為\\section,結合Python語法,每個\的前面必須再次新增一個\,因此,最終在Python中傳入的字串為"\\\\section"。
簡而言之,為了匹配一個反斜槓,在Python中你需要寫'\\\\'作為RE字串。這導致了很多重複的反斜槓,使語法很難於理解。
解決方案是為正則表示式使用Python的原生字串註釋。當字串帶有字首'r'時,反斜槓將不以特殊字元處理,於是r"\n"是包含'\'和'n'的兩個字元的字串,而"\n"是包含換行符的一個字元的字串。在Python中正則表示式將經常採用這種方式編寫。

執行匹配

一旦你有一個已編譯的正則表示式物件,你就可以使用該物件的方法和屬性,下面做一個簡單的介紹。
1)match()
確定RE是否匹配字串的開頭。
2)search()
掃描字串,查詢和RE匹配的任何位置。
3)findall()
找到所有RE匹配的子字串,並作為一個列表返回。
4)finditer()
發現所有RE匹配的子字串,並作為一個iterator返回。
如果找到匹配,match()和search()返回None;如果匹配成功,則返回一個匹配物件例項,包含匹配的資訊:開始和結束點、匹配的子字串、等等。
下面來看看Python中怎麼使用正則表示式。
首先,執行Python直譯器,匯入re模組,並且編譯一個RE:
>>> import re
>>> p = re.compile('[a-z]+')
>>> p
re.compile('[a-z]+')
現在,你能嘗試匹配各種字串,一個空字串將根本不匹配,由於+意味著‘一個或者更多’,match()將返回None,你能直接列印結果:
>>> p.match("")
>>> print(p.match(""))
None
接下來,我們嘗試一個匹配的字串,這時,match()將返回一個匹配物件,因此你應該儲存結果在一個變數中以供後面使用:
>>> m = p.match('tempo')
>>> m  
<_sre.SRE_Match object; span=(0, 5), match='tempo'>
現在你能詢問匹配物件關於匹配字串的資訊。匹配物件也有幾個方法和屬性,最重要的幾個是:
1)group()
返回被RE匹配的字串
2)start()
返回匹配的開始位置
3)end()
返回匹配的結束位置
4)span()
返回包含匹配位置的元組(開始,結束)
下面是一些使用這些方法的例子:
>>> m.group()
'tempo'
>>> m.start(), m.end()
(0, 5)
>>> m.span()
(0, 5)
由於match()僅檢查RE是否匹配字串的開始,start()將總是返回0。然而,search()方法掃描整個字串,因此開始位置不一定為0:
>>> print(p.match('::: message'))
None
>>> m = p.search('::: message'); print(m)  
<_sre.SRE_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('\d+')
>>> p.findall('12 drummers drumming, 11 pipers piping, 10 lords a-leaping')
['12', '11', '10']
findall()在返回結果前必須建立完整的列表,而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)

模組級函式

你不是一定需要建立一個模式物件然後呼叫它的方法,re模組也提供了模組級的函式match()、search()、findall()、sub()、等等。這些函式採用和對應的模式方法同樣的引數,也同樣返回None或者匹配物件例項:
>>> 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; span=(0, 5), match='From '>
這些函式建立一個模式物件,並呼叫它上面的方法,它們也儲存編譯後的物件到快取中,以至於未來使用同樣的RE將不需要重新編譯。
你應該用這些模組及的函式,還是應該通過模組物件來呼叫呢?如果你正在做一個正則表示式的迴圈,則預編譯將節省許多函式呼叫,否則,兩個方式沒有太大區別。

編譯標誌

編譯標誌讓你修改正則表示式如何工作的一些方面。在re模組中標誌可以使用兩種名稱,長名稱,例如IGNORECASE,和短名稱,例如I。通過位或運算,多個標誌能被指定,例如re.I | re.M設定I和M標誌。
下面是可用標誌的列表和每個標誌的解釋:
1)ASCII, A
當使用\w、\b、\s和\d時僅匹配ASCII字元;
2)DOTALL, S
使'.'匹配任何字元,包括新行;
3)IGNORECASE, I
忽略大小寫匹配;
4)LOCALE, L
做地域相關匹配;
5)MULTILINE, M
多行匹配,影響^和$;
6)VERBOSE, X (for ‘extended’)
啟動詳細的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.VERBOSE,則RE將是這樣:
charref = re.compile("&#(0[0-7]+"
                     "|[0-9]+"
                     "|x[0-9a-fA-F]+);")
在上面的例子中,Python的自動字串串聯被用於將RE分化到多個片段,但是它任然比使用re.VERBOSE更難理解。

正則表示式的更多特性

到目前為止我們僅覆蓋了正則表示式的部分特性,在這裡,我們將探索一些新的特性。

更多元字元

這裡我們將介紹更多的元字元。
1)|
表示‘或’操作,如果A和B都是正則表示式,則A|B表示匹配A或者匹配B。為了能有效的工作,|有很低的優先順序,例如:Crow|Servo將匹配Crow或者Servo,而不是Cro,一個‘w’或者一個‘S’,再接上ervo。
為了匹配字元'|',你需要使用\|,或者封裝它到一個字元類中,作為[|]。
2)^
匹配行的開始。除非設定了MULTILINE標誌,這將僅匹配字串的開始。在MULTILINE模式下,這也匹配每個新行的開始。
例如:如果你希望匹配在行的開始匹配單詞From,RE中將使用^From。
>>> print(re.search('^From', 'From Here to Eternity'))  
<_sre.SRE_Match object; span=(0, 4), match='From'>
>>> print(re.search('^From', 'Reciting From Memory'))
None
3)$
匹配行的結尾。可以是字串的結尾,或者是被新行符跟隨的部分。
>>> print(re.search('}$', '{block}'))  
<_sre.SRE_Match object; span=(6, 7), match='}'>
>>> print(re.search('}$', '{block} '))
None
>>> print(re.search('}$', '{block}\n'))  
<_sre.SRE_Match object; span=(6, 7), match='}'>
為了匹配字元'$',需要使用\$或者將它分裝到字元類中,作為[$]。
4)\A
僅匹配字串的開始。當不使用MULTILINE模式時,\A和^是相同的;在MULTILINE模式,他們是不同的:\A任然僅匹配字串的開始,而^可以匹配每個新行的開始。
5)\Z
匹配僅在字串末尾。
6)\b
單詞邊界。這是一個零寬度斷言,僅匹配單詞的開始和結束。一個單詞被定義為字母的序列,以至於單詞的結束被表示為空白或者一個非字母字元。
下面是一個例子,匹配單詞class,但它位於一個單詞內部時將不被匹配:
>>> p = re.compile(r'\bclass\b')
>>> print(p.search('no class at all'))  
<_sre.SRE_Match object; span=(3, 8), match='class'>
>>> print(p.search('the declassified algorithm'))
None
>>> print(p.search('one subclass is'))
None
在使用時有兩點需要注意:首先,在Python中,\b表示退格字元,ASCII值是8,如果你不用原始字串,那麼Python將轉換\b到一個退格字元,你的RE將不按你的設想匹配。下面的例子和我們上面的例子相似,僅有的區別是RE字串少了'r'字首:
>>> p = re.compile('\bclass\b')
>>> print(p.search('no class at all'))
None
>>> print(p.search('\b' + 'class' + '\b'))  
<_sre.SRE_Match object; span=(0, 7), match='\x08class\x08'>
第二,在字元類裡,\b表示退格字元,和Python中的含義表示一致。
7)\B
另一個零寬度斷言,和\b相反,僅匹配當前位置不是單詞邊界。

分組

組通過'('和')'元字元標識,'('和')'在這裡和數學表示式中的含義相同,它們將內部的表示式歸為一個分組,你能指定一個分組重複的次數,通過使用重複元字元*、+、?或者{m,n}。例如,(ab)*將匹配0個或者多個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探測同時出現兩個相同單詞的情況:
>>> p = re.compile(r'(\b\w+)\s+\1')
>>> p.search('Paris in the the spring').group()
'the the'
這種匹配方式在搜尋中很少使用,但在字串替換時卻非常有用。

非捕獲和命名組

RE可以使用許多組,用於捕獲感興趣的子字串或者使複雜的RE結構更清晰,這使通過組編號進行跟蹤變得非常困難。有兩個方法可以解決這個問題,我們首先看第一個。
有時你將想要使用一個組表示正則表示式的一部分,但是不想要獲取該組的內容。這時,你能使用非捕獲組:(?:...),將...替換為任何正則表示式。
>>> 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'
命名組是便利的,因為名稱比編號更容易記憶,下面是一個來自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的內容應該和當前點的內容匹配。為發現2個連續重複單詞的正則表示式,(\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'

預測先行斷言

另一個零寬度斷言是預測先行斷言。預測先行斷言可以在正、負形式使用,像這樣:
1)(?=...)
正預測先行斷言。如果包含...表示的正則表示式在當前位置被成功匹配,則成功,否則失敗。但是,雖然包含的正則表示式被嘗試,但匹配引擎並不會前進,模式的其餘部分還是從斷言開始的地方開始匹配。
2)(?!...)
負預測先行斷言。和正預測先行斷言相反,如果它包含的正則表示式不匹配當前位置的字串,則成功。
為了使描述更加具體,我們看一個例子說明預測先行的作用。考慮一個簡單的模式,用於匹配一個檔名,並將它拆分為檔名和副檔名。例如,news.rc中,news表示檔名,rc表示副檔名。
匹配的模式很簡單:


.*[.].*$


注意.需要放到字元類中,因為它是一個元字元;也注意$,用於確保所有字串的其餘部分被包含在擴充套件中。這個正則表示式可以匹配foo.bar、autoexec.bat、sendmail.cf和printers.conf。
現在,考慮一個稍複雜點的情況,如果你想匹配副檔名不是bat的檔名該怎麼做?下面是一些不正確的嘗試:
1).*[.][^b].*$
這個嘗試要求副檔名的第一個字元不是b來排除bat。這時錯誤的,因為該模式也不匹配foo.bar。
2).*[.]([^b]..|.[^a].|..[^t])$
這個比上一個更復雜一點,要求:擴充套件的第一個字元不匹配b,或者第二個字元不匹配a,或者第三個字元不匹配t。這個模式匹配foo.bar,不匹配autoexec.bat,但是它要求副檔名必須為3個字元,將不匹配帶有2個字元副檔名的檔案,例如sendmail.cf。我們將繼續完善它。
3).*[.]([^b].?.?|.[^a]?.?|..?[^t]?)$
在這個嘗試中,第二個和第三個字元都是可選的,為了匹配的副檔名小於三個字元的情況,例如sendmail.cf。
現在模式開始複雜起來了,開始難於閱讀和理解。更糟的是,如果問題改變,你想同時排除副檔名bat和exe,模式將變得更為複雜和難於理解。
一個負預測先行斷言可以解決這個問題。
.*[.](?!bat$).*$
含義為:如果當前點表示式bat不匹配,則嘗試模式的其餘部分;如果bat$匹配,整個模式將失敗。結尾的$用於防止出現sample.batch的情況。
排除另一個副檔名現在也容易了,簡單的增加它作為斷言的二選一。下面的模式同時排除bat和exe:
.*[.](?!bat$|exe$).*$

修改字串

目前為止,我們僅適用正則表示式查詢字串,正則表示式也可用於修改字串,使用下面的方法:
1)split()
從RE匹配的地方將字串分解為字串列表;
2)sub()
找到RE匹配的所有子字串,並使用不同的字串取代它們;
3)subn()
和sub做的事相同,但是返回新字串和替換的次數。

分解字串

split()方法用於分解一個字串,使用RE匹配的子字串作為分隔符,返回分解後的子字串列表。它和字串的split()方法是類似的,但是提供了更為通用的分隔符;字串的split()方法僅支援空格或者固定的字串。re也提供了一個模組級的re.split()函式。
split(string[, maxsplit=0]) 
通過正則表示式的匹配分解字串。如果在RE中使用了括號,則正則表示式的匹配也將出現在結果列表中。如果maxsplit值大於0,則最多做maxsplit次分解。
你能通過設定maxsplit的值限制分解的數量。當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中使用了括號,那麼他們的值也將出現在返回列表中。比較下面的呼叫:
>>> 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('[\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]) 
返回替換後的字串,替換採用從左到右並且非重疊的方式。如果模式未被發現,返回未改變的字串。
可選引數count用於指定替換的最大次數;count必須非負。預設值0意味著替換所有。
下面是一個簡單的例子。它使用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被轉換為回車符,等等。未知的轉義符例如\j被遺留。反向引用,例如\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>將通過組名來匹配,\g<number>將通過組編號來匹配。因此\g<2>等價於\2,當可以避免歧義,例如\g<2>0表示匹配組2,而\20則會被解釋為匹配組20。下面替換的例子都是等價的,但是使用了3種不同的方式:
>>> 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是一個函式,函式會處理每一個模式匹配的非重疊的子字串。在每次呼叫,函式被傳遞一個匹配物件作為引數,函式可以使用這個資訊計算替換字串並返回它。
在下面的例子中,replacement函式轉換10進位制數到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()函式時,模式作為第一個引數傳入。模式可以為一個物件或者字串;如果你需要指定正則表示式標誌,你必須使用一個模式物件作為第一個引數,或者在模式字串中用嵌入的修飾語,例如:sub("(?i)b+", "x", "bbbb BBBB")返回'x x'。

常見問題

正則表示式在一些應用中是有用的工具,但他們的行為不是直觀的,有時並不按照你所期望的方式工作。這節將描述一些常見的陷阱。

用String方法

有時使用re模組是錯誤的。如果你正在匹配一個固定的字串,或者一個單個的字元類,並且你並沒有用到任何re特徵,例如IGNORECASE標誌,那麼正則表示式的威力並不被需要。String有幾個為固定的字串執行操作的方法,並且它們通常更快,因為他們的實現是單個的C迴圈,並且針對該場景做了優化。
一個常見的例子是替換一個固定的字串為另一個,例如,你想替換word為deed,re.sub()似乎可以用於這種場景,但是你應該考慮replace()方法。注意replace()也將替換單詞內的word,例如修改swordfish為sdeedfish,但是簡單的RE word也將做那。(為了避免單詞內的替換,模式將必須是\bword\b,為了要求word是一個獨立的單詞。這一點超出了replace()的能力。)
另一個常見任務是探測字串中某個字元的位置,或者使用另一個字元替換它。你可以使用類似這樣的操作來實現:re.sub('\n', ' ', S),但是translate()也可以完成這樣的任務,並且比任何正則表示式的操作都更快。
總之,使用re模組之前,考慮你的問題是否能使用更快、更簡單的字串方法解決。

match() VS search()

match()函式僅檢查RE是否在字串的開始匹配,而search()將掃描整個字串。記住這一點非常重要,match()將僅報告在起點為0進行的成功匹配;如果匹配的起點不為0,match()將不報告它。
>>> 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)
有時你會被引誘使用re.match(),僅僅增加.*到你的RE之前。你應該拒絕這個誘惑,轉而使用re.search()。正則表示式編譯器會做一些RE的分析,為了加速查詢匹配的處理。一個如此的分析是分析出匹配的首字元必定是什麼;例如,一個以Crow開始的模式必須匹配首字元'C'。這個分析使引擎快速掃描字串查詢開始字元,當'C'被發現時才繼續向下匹配。
增加.*將使這個優化無效,要求掃描到字串的結尾,在回溯發現RE其餘部分的一個匹配。因此,優先使用re.search()。

貪婪 VS 非貪婪

當重複一個正則表示式時,例如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解析器來完成這樣的任務。)

使用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*$")