一、概述
但凡有過語言開發經驗的童鞋都清楚,很多時候需要進行字符串的匹配搜索、查找替換等處理,此時正則表達式就是解決問題的不二法門。正則表達式並不是Python的一部分。正則表達式是用於處理字符串的強大工具,擁有自己獨特的語法以及一個獨立的處理引擎,效率上可能不如str自帶的方法,但功能十分強大。得益於這一點,在提供了正則表達式的語言里,正則表達式的語法都是一樣的,區別只在於不同的編程語言實現支持的語法數量不同;但不用擔心,不被支持的語法通常是不常用的部分。如果已經在其他語言里使用過正則表達式,只需要簡單看一看就可以上手了。
下圖展示了使用正則表達式進行匹配的流程:
正則表達式的大致匹配過程是:依次拿出表達式和文本中的字符比較,如果每一個字符都能匹配,則匹配成功;一旦有匹配不成功的字符則匹配失敗。如果表達式中有量詞或邊界,這個過程會稍微有一些不同,但也是很好理解的(以上文字和圖片版權歸大神AstralWind所有,引用自http://www.cnblogs.com/huxi/archive/2010/07/04/1771073.html)。
匹配過程和規則詳見下一章節。
二、正則表達式元字符和語法
正則表達式之所以功能強大,在於豐富的元字符。Python中常用的元字符和語法如下(感謝大神AstralWind的分享,圖片引用自http://www.cnblogs.com/huxi/archive/2010/07/04/1771073.html):
三、re模塊的常見匹配方法
- re.match(pattern, string, flags=0)
功能:在string的開始位置開始匹配指定的字符串1 >>> import re 2 #從字符串開始位置匹配,匹配失敗 3 >>> print(re.match('abc', '1abc123').group()) 4 Traceback (most recent call last): 5 File "<stdin>", line 1, in <module> 6 AttributeError: 'NoneType' object has no attribute 'group' 7 #匹配失敗返回None 8 >>> print(re.match('abc', '1abc123')) 9 None 10 #匹配成功 11 >>> print(re.match('abc', 'abc123').group()) 12 abc 13 >>> print(re.match('abc', 'abc123')) 14 <_sre.SRE_Match object; span=(0, 3), match='abc'>
- re.search(pattern, string, flags=0)
功能:在string中查找指定的字符串,位置任意1 >>> import re 2 #把match改為search后匹配成功 3 >>> print(re.search('abc', '1abc123').group()) 4 abc 5 >>> print(re.search('abc', '1abc123')) 6 <_sre.SRE_Match object; span=(1, 4), match='abc'> 7 #使用元字符匹配 8 >>> print(re.search('[a-z]+\d', 'abc123abc')) 9 <_sre.SRE_Match object; span=(0, 4), match='abc1'>
- re.findall(pattern, string, flags=0)
功能:把匹配到的字符以列表的形式返回1 >>> import re 2 #默認最長匹配,能匹配多少次就匹配多少次,下文會有專門章節講述 3 >>> print(re.findall('abc', '1abc123abc')) 4 ['abc', 'abc'] 5 >>> print(re.findall('abc', '1abc123ab2c')) 6 ['abc']
- re.split(pattern, string, maxsplit=0, flags=0)
功能:以指定的pattern作為分隔符來分割指定的字符串,然后以列表形式返回分割后的結果1 >>> import re 2 >>> 3 >>> print(re.split('\d', 'a1b2c3')) 4 ['a', 'b', 'c', ''] 5 >>> print(re.split('\d', '1a1b2c3')) 6 ['', 'a', 'b', 'c', ''] 7 >>>
1 >>> import re 2 >>> print(re.split('\d', 'abc')) 3 ['abc'] 4 >>> print(re.split('\s', 'abc')) 5 ['abc']
- re.sub(pattern, repl, string, count=0, flags=0)
功能:查找匹配string中的patter,並用repl進行替換。默認替換全部,可通過count來限定替換的次數。1 >>> import re 2 >>> print(re.sub('\d', '\s', 'abc123abc')) 3 abc\s\s\sabc 4 >>> print(re.sub('[a-z]', '0', 'abc123abc')) 5 000123000 6 #限定僅替換3次 7 >>> print(re.sub('[a-z]', '0', 'abc123abc', count=3)) 8 000123abc
- finditer(pattern, string)
功能:返回一個迭代器1 >>> import re 2 >>> print(re.finditer('abc', 'abc123')) 3 <callable_iterator object at 0x00000000025AD2B0> 4 >>> print(re.finditer('abc2', 'abc123')) 5 <callable_iterator object at 0x00000000025AD390>
四、re模塊的特殊用法和匹配模式
4.1 特殊用法
- group([group1, ...])
獲得一個或多個分組截獲的字符串;指定多個參數時將以元組形式返回。group1可以使用編號也可以使用別名;編號0代表整個匹配的子串(即匹配到pattern的完整子串);不填寫參數時,返回group(0);沒有截獲字符串的組返回None;截獲了多次的組返回最后一次截獲的子串。
這里的多個參數指的是匹配到了多個分組,然后通過分組編號或別名形式指定返回的分組。
需要注意的是如果要匹配多個分組,每個分組都需要在pattern中用圓括號括起來,以明確表示這是一個分隔的單獨分組;另外re模塊的常見匹配函數中,只有match、search有group方法,其他的函數沒有。1 >>> import re 2 >>> a=re.search('(\w+)\s(\w+)', 'hello world') 3 >>> print(a.group(1,2)) 4 ('hello', 'world') 5 >>> print(a.group()) 6 hello world 7 #為分組4定義一個別名 8 >>> b=re.match('(\w+)\s(\w+)\s(\w+)\s(?P<gender>\w+)', 'Maxwell is a man') 9 >>> print(b.group()) 10 Maxwell is a man 11 #通過分組編號獲取匹配的分組 12 >>> print(b.group(4)) 13 man 14 #通過別名獲取匹配分組,注意別名要引起來 15 >>> print(b.group('gender')) 16 man
- groups(default=None)
以元組形式返回全部分組截獲的字符串。相當於調用group(1,2,…last)。default表示沒有截獲字符串的組以這個值替代,默認為None。這個要跟分組匹配結合起來使用'(...)'。由於返回的結果是元組形式,因此可以通過len(a.groups())來獲取分組的個數。1 >>> import re 2 >>> a=re.search('(\w+)\s(\w+)', 'hello world') 3 >>> print(a.groups()) 4 ('hello', 'world') 5 #獲取分組個數 6 >>> print(len(a.groups())) 7 2 8 9 #由於返回的是元組形式,因此可以通過len()獲取到匹配到的分組個數
- groupdict(default=None)
返回以有別名的組的別名為鍵、以該組截獲的子串為值的字典,沒有別名的組不包含在內。default含義同上。這個是跟另外一個分組匹配結合起來用的,即:'(?P<name>...)'。
這里的前提是先定義匹配分組的別名,方法很簡單,每一個別名都通過(?P<name>pattern)定義即可,當然引號是所有分組共用的啦1 >>> import re 2 >>> a=re.match('(?P<province>\d{4})(?P<city>\d{2})(?P<birthday>\d{8})(?P<id>\d{4})','510123199502162018') 3 >>> print(a.group()) 4 510123199502162018 5 >>> print(a.groupdict()) 6 {'province': '5101', 'city': '23', 'birthday': '19950216', 'id': '2018'} 7 >>> print(a.groupdict('birthday')) 8 {'province': '5101', 'city': '23', 'birthday': '19950216', 'id': '2018'}
- span([group])
返回(start(group), end(group)),返回指定匹配的pattern在源字符串中的起始位置和結束位置(實際在match和search的返回結果中有顯示)1 >>> import re 2 >>> print(re.search('[a-z]+\d', 'abc123abc')) 3 <_sre.SRE_Match object; span=(0, 4), match='abc1'> 4 >>> print(re.search('[a-z]+\d', 'abc123abc').span()) 5 (0, 4)
- start([group])
返回匹配的完整子串在源string中的起始索引位置,即span()輸出的起始索引位置。1 >>> import re 2 >>> a=re.search('(\w+)\s+(\w+)', 'hello world') 3 >>> a.group() 4 'hello world' 5 >>> a.group(1) 6 'hello' 7 >>> a.group(2) 8 'world' 9 >>> a.start() 10 0 11 >>> a.span() 12 (0, 11)
- end([group])
返回匹配的完整子串在源string中的結束索引位置,即span()輸出的結束索引位置。1 >>> import re 2 >>> a=re.search('(\w+)\s+(\w+)', 'hello world') 3 >>> a.group() 4 'hello world' 5 >>> a.group(1) 6 'hello' 7 >>> a.group(2) 8 'world' 9 >>> a.end() 10 11 11 >>> a.span() 12 (0, 11)
- compile(pattern[, flags])
這個方法是Pattern類的工廠方法,用於將字符串形式的正則表達式編譯為Pattern對象。還記得文章開頭概述部分講到的正則表達式匹配流程嗎?第一步就是通過正則表達式引擎把正則表達式文本pattern編譯為正則表達式pattern對象,因此compile這個環節無需手動完成,個人理解通過手動編譯好的正則表達式對象對匹配只是理論上時間少那么一丟丟吧,完全可以忽略了。1 >>> import re 2 >>> a=re.compile('(\w+)\s+(\w+)') 3 >>> b=a.search('hello world') 4 >>> b.group() 5 'hello world'
使用compile有一個好處是可以復用pattern對象的一些屬性,如輸出定義的pattern,groups數量,定義有別名的分組的索引等,可能有那么一點用處
把。
1 >>> a=re.compile('(\w+)\s+(\w+)') 2 >>> print(a.pattern) 3 (\w+)\s+(\w+) 4 >>> print(a.groups) 5 2 6 >>> print(a.flags) 7 32 8 >>> print(a.groupindex) 9 {} 10 >>> b=re.compile('(\w+)\s+(?P<keyword>\w+)') 11 >>> print(b.groupindex) 12 {'keyword': 2} 13 >>> print(b.groups) 14 2
- 反斜杠的困擾
與大多數編程語言相同,正則表達式里使用"\"作為轉義字符,這就可能造成反斜杠困擾。假如你需要匹配文本中的字符"\",那么使用編程語言表示的正則表達式里將需要4個反斜杠"\\\\":前兩個和后兩個分別用於在編程語言里轉義成反斜杠,轉換成兩個反斜杠后再在正則表達式里轉義成一個反斜杠。Python里的原生字符串很好地解決了這個問題,這個例子中的正則表達式可以使用r"\\"表示。同樣,匹配一個數字的"\\d"可以寫成r"\d"。有了原生字符串,你再也不用擔心是不是漏寫了反斜杠,寫出來的表達式也更直觀。
這里的加r直白理解是可以免去人工處理轉義符的麻煩,說的官方一些是跳脫解釋器對轉義符的解釋,保留轉義符’\’原本的字符串反斜杠意義,算是返璞歸真吧。1 >>> import re 2 >>> a='hello\nworld' 3 >>> print(a) 4 hello 5 world 6 #跳脫轉義符,\n被當做普通字符串處理 7 >>> a=r'hello\nworld' 8 >>> print(a) 9 hello\nworld 10
注意:
細心的同學會發現上述論述存在一個看似相互矛盾的問題,在不考慮加r的情況下,為什么匹配一個反斜杠需要4個反斜杠來轉義3次,而匹配一個數字的\\d只需要兩個反斜杠轉義一次就可以了呢?這是因為\d是一個元字符,python解釋器內部作了特殊處理,除元字符外,其他情況下的反斜杠匹配,在不加r的前提下都需要不厭其煩地寫4次從而轉義3次。
當然,這里的’\\d’兩個反斜杠前面的那個對后面的反斜杠進行轉義,最后給到解釋器的就是‘\d’, 這里有一個斜杠是多余了,加r也多余了,忘掉“同樣,匹配一個數字的"\\d"可以寫成r"\d"”這段文字把,簡單就好啊!1 >>> import re 2 >>> a = re.split("\\\\","C:\download\test") 3 >>> print(a) 4 ['C:', 'download\test'] 5 >>> a = re.split(r"\\","C:\download\test") 6 >>> print(a) 7 ['C:', 'download\test'] 8 >>> 9 >>> 10 >>> b=re.split('\\d','a1b2c3') 11 >>> print(b) 12 ['a', 'b', 'c', ''] 13 >>> b=re.split(r'\d','a1b2c3') 14 >>> print(b) 15 ['a', 'b', 'c', ''] 16 >>> b=re.split('\d','a1b2c3') 17 >>> print(b) 18 ['a', 'b', 'c', ''] 19 >>>
4.2 匹配模式
正則表達式提供了一些可用的匹配模式,用於改變默認的匹配模式,比如忽略大小寫、忽略換行進行多行匹配等,可以滿足我們在實際應用中的復雜匹配需求,比如爬蟲需要爬js代碼等。這些匹配模式都是在匹配方法中通過flags參數來指定的。
- re.I(flags=
re.IGNORECASE
)
說明:忽略大小寫(括號內是完整的寫法,下同)1 >>> import re 2 >>> a=re.search('hello', 'Hello world') 3 >>> print(a.group()) 4 Traceback (most recent call last): 5 File "<stdin>", line 1, in <module> 6 AttributeError: 'NoneType' object has no attribute 'group' #匹配失敗 7 >>> a=re.search('hello', 'Hello world', re.I) 8 >>> print(a.group()) 9 Hello 10 >>>
- re.M(flags=
MULTILINE
)
說明:多行模式,改變'^'和'$'的匹配行為,使得他們中間也能包括換行符。1 >>> import re 2 >>> a='This is the first line.\nThis is the second line.' 3 >>> print(a) 4 This is the first line. 5 This is the second line. 6 >>> b=re.search('This.*line\.', a) 7 >>> print(b.group()) 8 This is the first line. 9 >>> b=re.search('This.*line\.', a, re.S) 10 >>> print(b.group()) 11 This is the first line. 12 This is the second line. 13 >>> b=re.findall('^This.*line\.$',a) 14 >>> print(b) 15 [] 16 #pattern添加?改成非貪婪模式匹配 17 >>> b=re.findall('^This.*?line\.$',a,re.M) 18 >>> print(b) 19 ['This is the first line.', 'This is the second line.']
- re.S(flags=
DOTALL
)
說明:點任意匹配模式,改變'.'的行為。有些地方也成為單行模式,使得點號也能匹配換行符(點號原本是可以匹配任何字符,唯獨換行符要除外)。1 >>> import re 2 >>> a='This is the first line.\nThis is the second line.' 3 >>> print(a) 4 This is the first line. 5 This is the second line. 6 >>> b=re.search('This.*line\.', a) 7 >>> print(b.group()) 8 This is the first line. 9 >>> b=re.search('This.*line\.', a, re.S) 10 >>> print(b.group()) 11 This is the first line. 12 This is the second line.
上面三種匹配模式,忽略大小寫非常實用,而多行模式和單行模式,則是匹配跨行文本的利器。
上面示例程序中,即便是把匹配用的pattern通過添加?改變成非貪婪模式匹配(貪婪模式與非貪婪模式下面章節會詳細闡述),依然能匹配成功,這是因為在多行模式下,^除了匹配整個字符串的起始位置,還匹配換行符后面的位置;$除了匹配整個字符串的結束位置,還匹配換行符前面的位置。簡而言之,多行模式允許 ^和$中間出現換行符。
五、貪婪匹配與非貪婪匹配
OK,要用好正則表達式,貪婪匹配與非貪婪匹配非得整明白不可。Python里數量詞默認是貪婪的(在少數語言里也可能是默認非貪婪),總是嘗試匹配盡可能多的字符(最長匹配模式);非貪婪的則相反,總是嘗試匹配盡可能少的字符。例如:正則表達式”ab*”如果用於查找”abbbc”,將找到”abbb”。而如果使用非貪婪的數量詞”ab*?”,將找到”a”。
引用一段網上找到的博文把:
標准量詞修飾的子表達式,在可匹配可不匹配的情況下,總會先嘗試進行匹配,稱這種方式為匹配優先,或者貪婪模式。此前介紹的一些量詞,“{m}”、“{m,n}”、“{m,}”、“?”、“*”和“+”都是匹配優先的。
一些NFA正則引擎支持忽略優先量詞,也就是在標准量詞后加一個“?”,此時,在可匹配可不匹配的情況下,總會先忽略匹配,只有在由忽略優先量詞修飾的子表達式,必須進行匹配才能使整個表達式匹配成功時,才會進行匹配,稱這種方式為忽略優先,或者非貪婪模式。忽略優先量詞包括“{m}?”、“{m,n}?”、“{m,}?”、“??”、“*?”和“+?”(以上論述引用自http://blog.csdn.net/haoxizh/article/details/44648069)。
上述文字有部分地方講得略生澀,通俗一些理解:貪婪模式是盡可能多盡可能長地匹配,能匹配多長能匹配多少就是多少,所以叫匹配優先;非貪婪模式恰好相反,只要滿足最小條件下的匹配即可,即盡可能少盡可能短地匹配,它的宗旨是匹配一次即可,多余的統統忽略不管。
還是來幾個栗子更清楚直觀了:
1 >>> import re 2 >>> print(re.findall('ab*','abbbc')) 3 ['abbb'] 4 >>> print(re.findall('ab*?','abbbc')) 5 ['a'] 6 >>> print(re.findall('ab{2}','abbbc')) 7 ['abb'] 8 #對於單個指定匹配多少次的情況,非貪婪模式與貪婪模式匹配的結果相同 9 >>> print(re.findall('ab{2}?','abbbc')) 10 ['abb'] 11 >>> print(re.findall('ab{1,3}','abbbc')) 12 ['abbb'] 13 >>> print(re.findall('ab{1,3}?','abbbc')) 14 ['ab'] 15 >>> print(re.findall('ab{1,}','abbbc')) 16 ['abbb'] 17 >>> print(re.findall('ab{1,}?','abbbc')) 18 ['ab'] 19 >>> print(re.findall('ab?','abbbc')) 20 ['ab'] 21 >>> print(re.findall('ab??','abbbc')) 22 ['a'] 23 >>> print(re.findall('ab+','abbbc')) 24 ['abbb'] 25 >>> print(re.findall('ab+?','abbbc')) 26 ['ab']
從上述示例程序可以看出,非貪婪模式改變了數量詞的默認匹配行為,對於數量詞指定的匹配的下限次數和上限次數({m,n},{m,})情況,以及匹配時的次數可少也可以多的情況(*,?,+),統統按最少匹配次數(最短匹配)來處理。
另外需要注意的是,引用的博文論述中對於{m}?的描述不太准確,示例程序顯示這種情況下由於匹配的次數已經指定且是唯一的,因此非貪婪模式的匹配效果不能直觀體現出來,忘掉這個吧。
由於Python中默認的匹配模式是貪婪模式,實際應用中非貪婪模式用的很多的。