python 學習筆記 10 -- 正則表達式

零、引言

在《Dive into Python》(深入python)中,第七章介紹正則表達式,開篇很好的引出了正則表達式,下面借用一下:我們都知道python中字符串也有比較簡單的方法,比如可以進行搜索(index,find和count),替換(replace)和解析(split),這在本系列前篇數據結構篇中有所涉及,但是有種種限制。比如要進行大小寫不敏感的搜索時,可能就需要先對字符串進行str.lower()或str.upper()將字符串先統一轉換成小寫或者大寫在進行搜索。

那麼,本篇的主角正則表達式呢?正則表達式本身是一種小型的、高度專業化的編程語言,它內嵌在Python中,並通過 re 模塊實現。使用這個小型語言,你可以爲想要匹配的相應字符串集指定規則;該字符串集可能包含英文語句、e-mail地址、TeX命令或任何你想搞定的東西。

下面借用《Dive into Python》中的例子比較一下正則表達式和字符串方法:

>>> str = "1000 NORTH MAIN ROAD"
>>> str.replace("ROAD","RD.")       -- 通常使用時可能會使用RD.代替ROAD
'1000 NORTH MAIN RD.'
>>> str = "1000 NORTH BROAD ROAD"   -- 對於BROAD中的ROAD也進行了替換,這並不是我們想要的
>>> str.replace("ROAD","RD.")
'1000 NORTH BRD. RD.'
>>> str[:-4]+str[-4:].replace("ROAD","RD.") -- 通過切片只對最後四個字符的ROAD進行替換
'1000 NORTH BROAD RD.'
>>> import re           
>>> re.sub('ROAD$',"RD.",str)       -- 使用正則表達式re中的sub函數對str中的"ROAD$"使用"RD."進行替換,而$在正則表達式中表示行末,所以此句只對最後的ROAD進行了替換,使用起來比上面使用字符串切片再匹配要簡單的多
'1000 NORTH BROAD RD.'

上面只是舉了簡單的小例子介紹Python中的正則表達式,在這種小例子中python的正則表達式要比字符串方法好用方便許多,更何況它還有更強大的功能?

一、簡介

上文已經提及,正則表達式(RE)自身是一種小型的、高度專業化的編程語言,所以,在提供了正則表達式的語言裏,正則表達式的語法都是一樣的,區別只在於不同的編程語言實現支持的語法數量不同;但不用擔心,不被支持的語法通常是不常用的部分。如果已經在其他語言裏使用過正則表達式,只需要簡單看一看就可以s上手了。這裏給大家推薦一個學習正則表達式的好資源《正則表達式30分鐘入門教程》。


上文顯示了正則表達式的工作流程,首先語言中的正則表達式引擎會將用戶使用的正則表達式文本編程成正則表達式對象,然後依次拿出表達式對象和文本中的字符比較,如果每一個字符都能匹配,則匹配成功;一旦有匹配不成功的字符則匹配失敗。如果表達式中有量詞或邊界,這個過程會稍微有一些不同,但也是很好理解的,看下圖中的示例以及自己多使用幾次就能明白。


二、正則表達式元字符語法

這裏偷個小懶,直接將《Python正則表達式指南》[2]中大神總結的正則表達式語法圖拿過來用了:


注: 正則表達式之所以複雜難懂,除了它有上面如此多的元字符外,這些元字符之間的組合模式使得RE變得非常難懂。比如: \S 匹配非空白字符,而加上 + 後 \S+ 則表示不包含空白字符的字符串! 

此外, ^ 字符在上面圖片中解釋爲字符串開始,其實並不完善,在[ a ] 這樣的匹配中,^ 表示 “非” ,比如 [ a ]匹配字符 a,而[ ^a ] 則匹配除 a 以外的任意字符。


對於上面圖文中沒有列出來的,再擴展幾條:

2.1  反斜槓!!

與大多數編程語言相同,正則表達式裏使用"\"作爲轉義字符,這就可能造成反斜槓困擾。比如你想要在匹配文本中匹配製表符"\t",你需要寫成"\\t",直接匹配反斜槓需寫成"\\",更多時會顯得尤爲複雜。
在Python中,原生字符串很好地解決了這個問題,方法爲在匹配文本前加r,如r'\'表示匹配'\'本身。同樣,匹配一個數字的"\\d"可以寫成r"\d"。有了原生字符串,你再也不用擔心是不是漏寫了反斜槓,寫出來的表達式也更直觀。


2.2  貪婪與懶惰

當正則表達式中包含能接受重複的限定符時,通常的行爲是(在使整個表達式能得到匹配的前提下)匹配儘可能多的字符。例如: a.*b ,它將會匹配 最長的以 a 開始,以 b 結束的字符串 。如果用它來搜索 aabab 的話,它會匹配整個字符串 aabab 。這被稱爲 貪婪 匹配
有時,我們更需要 懶惰 匹配,也就是匹配儘可能少的字符。前面給出的限定符都可以被轉化爲懶惰匹配模式,只要在它後面加上一個問號 ? 。這樣  .*? 就意味着 匹配任意數量的重複, 但是在能使整個匹配成功的前提下使用最少的重複 。比如前面提到的 a.*b ,它的懶惰版爲: a.*?b 匹配 最短的,以 a 開始,以 b 結束的字符串 。如果把它應用於 aabab 的話,它會匹配 aab (第一到第三個字符)和 ab (第四到第五個字符)

>>> str = "aabab"
>>> pattern = "a.*b"
>>> print re.match(pattern,str).group(0)
aabab
>>> pattern = "a.*?b"
>>> print re.match(pattern,str).group(0)

注: 你可能發問了:既然是懶惰匹配,爲什麼第一個匹配是aab(第一到第三個字符)而不是ab(第二到第三個字符)?簡單地說,因爲正則表達式有另一條規則,且比懶惰 / 貪婪規則的優先級更高:最先開始的匹配擁有最高的優先權

——The match that begins earliest wins


三、一些Python 的RE方法及其應用

在此我們可以使用本系列前篇 -- Python自省中的apihelper模塊來查看一下python的 re 模塊中有哪些方法:

>>> import re
>>> import apihelper as ah
>>> ah.info(re)
Scanner    None
_compile   None
... 
compile    Compile a regular expression pattern, returning a pattern object.
error      None
escape     Escape all non-alphanumeric characters in pattern.
findall    Return a list of all non-overlapping matches in the string. If one or more groups are present in the pattern, return a list of groups; this will be a list of tuples if the pattern has more than one group. Empty matches are included in the result.
finditer   Return an iterator over all non-overlapping matches in the string. For each match, the iterator returns a match object. Empty matches are included in the result.
match      Try to apply the pattern at the start of the string, returning a match object, or None if no match was found.
purge      Clear the regular expression cache
search     Scan through string looking for a match to the pattern, returning a match object, or None if no match was found.
split      Split the source string by the occurrences of the pattern, returning a list containing the resulting substrings.
sub        Return the string obtained by replacing the leftmost non-overlapping occurrences of the pattern in string by the replacement repl. repl can be either a string or a callable; if a string, backslash escapes in it are processed. If it is a callable, it's passed the match object and must return a replacement string to be used.
subn       Return a 2-tuple containing (new_string, number). new_string is the string obtained by replacing the leftmost non-overlapping occurrences of the pattern in the source string by the replacement repl. number is the number of substitutions that were made. repl can be either a string or a callable; if a string, backslash escapes in it are processed. If it is a callable, it's passed the match object and must return a replacement string to be used.
template   Compile a template pattern, returning a pattern object

所以,我們可以大概知道,RE模塊主要包括,compile, escape, findall, ...等方法,而具體如何使用,我們可以使用 “ >>> help(re.match) ” 這樣查看每個方法的幫助文檔。


閒話不多說,下面將對這些函數一一進行簡單介紹:

3.1 python的 re 模塊簡單使用

前文以及提及,Python通過 re 模塊提供對正則表達式的支持。使用 re 的一般步驟是先將正則表達式的字符串形式編譯爲Pattern實例,然後使用Pattern實例處理文
本並獲得匹配結果(一個Match實例),最後使用Match實例獲得信息,進行其他的操作。

>>> import re
>>> pattern = re.compile(r'hello')          # 將正則表達式編譯成Pattern對象
>>> match = pattern.match('hello world!')   # 使用Pattern匹配文本,獲得匹配結果,無法匹配時將返回None
>>> if match:   
...     print match.group()                 # 如果匹配,打印匹配信息
... 
hello


RE的編譯方法:
        re.compile(strPattern[, flag])
這個方法是Pattern類的工廠方法,用於將字符串形式的正則表達式編譯爲Pattern對象。 其第二個參數flag是匹配模式,取值可以使用按位或運算符 '|' 表示同時生效,比如 re.I | re.M。編譯標誌讓你可以修改正則表達式的一些運行方式。在 re 模塊中標誌可以使用兩個名字,一個是全名如 IGNORECASE,一個是縮寫,一字母形式如 I。(標誌及其對應的flag及縮寫都在下表中清晰的顯示)

另外,你也可以在regex字符串中指定模式,比如re.compile('pattern', re.I | re.M)與re.compile('(?im)pattern')是等價的。
對於flag 可選值有:

名稱 修飾符 說明
IGNORECASE(忽略大小寫) re.I
re.IGNORECASE
忽略大小寫,使匹配對大小寫不敏感
MULTILINE (多行模式) re.M
re.MULTILINE
多行模式,改變'^'和'$'的行爲
DOTALL(點任意匹配模式) re.S
re.DOTALL
點任意匹配模式,改變'.'的行爲,使 '.' 匹配包括換行在內的所有字符;沒有這個標誌, "." 匹配除了換行外的任何字符。
LOCALE(使預定字符類) re.L
re.LOCALE
做本地化識別(locale-aware)匹配,使預定字符類 \w \W \b \B \s \S 取決於當前區域設定。locales 是 C 語言庫中的一項功能,是用來爲需要考慮不同語言的編程提供幫助的。舉個例子,如果你正在處理法文文本,你想用 "w+ 來匹配文字,但 "w 只匹配字符類 [A-Za-z];它並不能匹配 "é" 或 "?"。如果你的系統配置適當且本地化設置爲法語,那麼內部的 C 函數將告訴程序 "é" 也應該被認爲是一個字母。
UNICODE(Unicode模式) re.U
re.UNICODE
根據Unicode字符集解析字符,使預定字符類 \w \W \b \B \s \S \d \D 取決於unicode定義的字符屬性
VERBOSE(詳細模式) re.X
re.VERBOSE
這個模式下正則表達式可以是多行,忽略空白字符,並可以加入註釋。詳見4.1 鬆散正則表達式

re 模塊還提供了一些其他方法可以不用先編譯pattern實例,直接使用方法匹配文本,如上面這個例子可以簡寫爲:(使用compile 編譯的好處是可重用)

m = re.match(r'hello', 'hello world!')
print m.group()

re模塊還提供了一個方法escape(string),用於將string中的正則表達式元字符如*/+/?等之前加上轉義符再返回>,在需要大量匹配元字符時有那麼一點用。
>>> re.escape('.')
'\\.'
>>> re.escape('+')
'\\+'
>>> re.escape('?')
'\\?'


3.2 python RE模塊之match 方法

對於 re 的match 方法,首先我們使用help 查看其介紹:

match(pattern, string, flags=0)
    Try to apply the pattern at the start of the string, returning a match object, or None if no match was found.

解釋: match()函數只檢測RE是不是在string的開始位置匹配, 也就是說match()只有在0位置匹配成功的話纔有返回,如果不是開始位置匹配成功的話,match()就返回none。

eg.

>>> print re.match(r'hello', 'hello world!').group()
hello
>>> print re.match(r'hello', 'My name is zhou, hello world!').group()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute 'group'

其實Match對象是一次匹配的結果,包含了很多關於此次匹配的信息,可以使用Match提供的可讀屬性或方法來獲取這些信息。

屬性:
    string: 匹配時使用的文本。
    re: 匹配時使用的Pattern對象。
    pos: 文本中正則表達式開始搜索的索引。值與Pattern.match()和Pattern.seach()方法的同名參數相同。
    endpos: 文本中正則表達式結束搜索的索引。值與Pattern.match()和Pattern.seach()方法的同名參數相同。
    lastindex: 最後一個被捕獲的分組在文本中的索引。如果沒有被捕獲的分組,將爲None。
    lastgroup: 最後一個被捕獲的分組的別名。如果這個分組沒有別名或者沒有被捕獲的分組,將爲None。
方法:
    group([group1, …]):
    獲得一個或多個分組截獲的字符串;指定多個參數時將以元組形式返回。group1可以使用編號也可以使用別名;編號0代表整個匹配的子串;>不填寫參數時,默認返回group(0);沒有截獲字符串的組返回None;截獲了多次的組返回最後一次截獲的子串。
    groups([default]):
    以元組形式返回全部分組截獲的字符串。相當於調用group(1,2,…last)。default表示沒有截獲字符串的組以這個值替代,默認爲None。
    groupdict([default]):
    返回以有別名的組的別名爲鍵、以該組截獲的子串爲值的字典,沒有別名的組不包含在內。default含義同上。
    start([group]):
    返回指定的組截獲的子串在string中的起始索引(子串第一個字符的索引)。group默認值爲0。
    end([group]):
    返回指定的組截獲的子串在string中的結束索引(子串最後一個字符的索引+1)。group默認值爲0。
    span([group]):
    返回(start(group), end(group))。

如:

>>> match = re.match(r'hello', 'hello world!')
>>> print match.string              -- 打印匹配使用的文本
hello world!
>>> print match.re                  -- 匹配使用的Pattern對象
<_sre.SRE_Pattern object at 0xb7522520>
>>> print match.pos                 -- 文本中正則表達式開始搜索的索引
0
>>> print match.endpos              -- 文本中正則表達式結束搜索的索引,一般與len(string)相等
12
>>> print match.lastindex
None
>>> print match.lastgroup
None
>>> print match.groups()
()
>>> print match.group()             -- 不帶參數,默認爲group(0),打印整個匹配的子串
hello
>>> print match.groupdict()
{}
>>> print match.start()             -- 匹配到的字符串在string中的起始索引
0
>>> print match.end()               -- 匹配到的字符串在string中的結束索引
5
>>> print match.span()              -- 打印(start,end)
(0, 5)



3.3 python RE模塊之search 方法

同樣,我們首先使用help 查看其介紹:

search(pattern, string, flags=0)
    Scan through string looking for a match to the pattern, returning a match object, or None if no match was found.

解釋: re.search()會掃描整個字符串並返回第一個成功的匹配,如果匹配不上返回 None。

3.4 python RE模塊之兩種匹配方法比較(match VS search)

從上面兩小節的介紹中我們也可以知道 re.match 從字符串的開始處開始匹配,如果字符串開始不符合正則表達式,則匹配失敗,函數返回None;而re.search 查找整個字符串,直到找到一個匹配;若無匹配返回None。
從例子中我們可以輕易的看出區別:

>>> match = re.match(r'hello', 'my name is zhou, hello world!')
>>> search = re.search(r'hello', 'my name is zhou, hello world!')
>>> match.group()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute 'group'
>>> search.group()
'hello'
>>> search.start()
17
>>> search.end()
22



3.5 python RE模塊之sub 方法

照舊,我們使用help:

sub(pattern, repl, string, count=0, flags=0)
    Return the string obtained by replacing the leftmost  non-overlapping occurrences of the pattern in string by the replacement repl.  repl can be either a string or a callable;  if a string, backslash escapes in it are processed.  If it is a callable, it's passed the match object and must return a replacement string to be used.

解釋:

使用repl替換string中每一個匹配的子串後返回替換後的字符串。
當repl是一個字符串時,可以使用\id或\g<id>、\g<name>引用分組,但不能使用編號0。 
當repl是一個方法時,這個方法應當只接受一個參數(Match對象),並返回一個字符串用於替換(返回的字符串中不能再引用分組)。
count用於指定最多替換次數,不指定時全部替換。

eg.

>>> import re
>>> p = re.compile(r'(\w+) (\w+)')      -- 匹配p爲以空格連接的兩個單詞
>>> s = 'i say, hello world!'           
>>> print p.sub(r'\2 \1', s)            -- 以\id引用分組,將每個匹配中的兩個單詞掉轉位置
say i, world hello!
>>> def func(m):                        -- 一個函數,將匹配單詞的首字母改爲大寫
...     return m.group(1).title() + ' ' + m.group(2).title()
...
>>> print p.sub(func, s)                -- 使用sub調用func 函數
I Say, Hello World!



3.6 python RE模塊之subn 方法

照舊,我們使用help:

subn(pattern, repl, string, count=0, flags=0)
    Return a 2-tuple containing (new_string, number).
    new_string is the string obtained by replacing the leftmost non-overlapping occurrences of the pattern in the source string by the replacement repl.  number is the number of substitutions that were made. repl can be either a string or a callable; if a string, backslash escapes in it are processed. If it is a callable, it's passed the match object and must return a replacement string to be used.

解釋: 此函數的參數與 sub 函數完全一致,只不過其返回值是包括兩個元素的元組:(new_string, number);第一個返回值 new_string 爲sub 函數的結果,第二個 number 爲匹配及替換的次數。

例子我們可以直接在上面 sub 的例子上測試:

>>> import re
>>> p = re.compile(r'(\w+) (\w+)') 
>>> s = 'i say, hello world!'
>>> print p.subn(r'\2 \1', s)
('say i, world hello!', 2)
>>> def func(m):
...     return m.group(1).title() + ' ' + m.group(2).title()
... 
>>> print p.subn(func, s)
('I Say, Hello World!', 2)



3.7 python RE模塊之findall 方法

照舊,我們使用help:

findall(pattern, string, flags=0)
    Return a list of all non-overlapping matches in the string.
    If one or more groups are present in the pattern, return a list of groups; this will be a list of tuples if the pattern has more than one group.
    Empty matches are included in the result.

解釋: 搜索string,以列表形式返回全部能匹配的子串。返回值格式爲一個列表

eg. 

>>> re.findall(r'\d','one1two2three3four4')
['1', '2', '3', '4']
>>> re.findall(r'one','one1two2three3four4')
['one']
>>> re.findall(r'one2','one1two2three3four4')
[]


3.8 python RE模塊之finditer 方法

照舊,我們使用help:

finditer(pattern, string, flags=0)
    Return an iterator over all non-overlapping matches in the string.  For each match, the iterator returns a match object.
    Empty matches are included in the result.

解釋:找到 RE 匹配的所有子串,並把它們作爲一個迭代器返回。這個匹配是從左到右有序地返回。如果無匹配,返回空列表。

eg.

>>> re.finditer(r'\d','one1two2three3four4').group()    -- python的迭代器並不能像findall那樣直接打印所有匹配
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'callable-iterator' object has no attribute 'group'
<callable-iterator object at 0xb744496c>                -- 迭代器只能一個接着一個的打印
>>> for m in re.finditer(r'\d','one1two2three3four4'):
...     print m.group()
...
1
2
3
4


3.9 python RE模塊之purge 方法

照舊,我們使用help:

purge()
    Clear the regular expression cache

解釋:清空緩存中的正則表達式。

eg.

>>> p = re.compile(r'(\w+) (\w+)')
>>> p.search("hello world 123 zhou write").group()      -- 匹配字符串中的兩個單詞正常
'hello world'
>>> p = re.purge()                                      -- 清空RE緩存
>>> p.search("hello world 123 zhou write").group()      -- 再次使用search,報錯!此時p 變成了'NoneType'對象,而不是Pattern對象
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute 'search'


3.10 python RE模塊之split 方法

照舊,我們使用help:

split(pattern, string, maxsplit=0, flags=0)
    Split the source string by the occurrences of the pattern, returning a list containing the resulting substrings.

解釋:按照能夠匹配的子串將string分割後返回列表。maxsplit用於指定最大分割次數,不指定使用默認值0將全部分割。如設定好分割次數後,最後未分割部分將作爲列表中的一個元素返回。

eg.

>>> re.split(r'\d','one1two2three3four4')   
['one', 'two', 'three', 'four', '']
>>> re.split(r'\d','one1two2three3four4',2)   
['one', 'two', 'three3four4']
>>> re.split(r'\d','one1two2three3four4',0)   
['one', 'two', 'three', 'four', '']



四、About : python 中的正則表達式

4.1 鬆散正則表達式

對於一個正則表達式,可能過一段時間你就需要重新分析這個表達式的實際意思,因爲它本身就是由多個元字符組成,可能你還需要比較着上面的元字符的意思再分析你的表達式文本,那麼有什麼好的方法能夠解決這個問題?比如說下面這個表達式:(dive into python ,P129)

pattern = "^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$"

Python支持使用所謂的鬆散正則表達式來解決這個問題:

>>> import re
>>> pattern = """
# 本正則表達式爲匹配一個羅馬數字,而羅馬數字中MDCCCLXXXVIII表示1888,所以需要找到其規律寫成相應的匹配文本,使用該表達式匹配通過的爲羅馬數字,否則不是!
# 羅馬數字中: I = 1; V = 5; X = 10; L = 50; C = 100; D = 500; M = 1000
# 羅馬數字的規則:
# 1. 字符是疊加的,如I表示1,而II表示2,III表示3
#   2. 含十字符(I,X,C,M)至多可以重複三次,而應該利用下一個最大的含五字符進行減操作,如:對於4不能使用IIII而應該是IV,40不是XXXX而是XL,41爲XLI,而44爲XLIV
#   3. 同樣,對於9需要使用下一個十字符減操作,如9表示爲IX,90爲XC,900爲CM
#   4. 含五字符(V,L,D)不能重複,如:10不應該表示爲VV而是X

#   5. 羅馬數字從高位到低位書寫,從左到右閱讀,因此不同順序的字符意義不同,如,DC爲600而CD爲400.CI 表示101 而IC 爲不合法的羅馬數字
^ # 字符串開頭
M{0,3} # 千位,支持0到3個M
(CM|CD|D?C{0,3}) # 百位,900(CM),400(CD),0~300(0~3個C),500~800(D後接0~3個C)
(XC|XL|L?X{0,3}) # 十位,90(XC),40(XL),0~30(0~3個X),50~80(L後接0~3個X)
(IX|IV|V?I{0,3}) # 個位,9(IX),4(IV),0~3(0~3個I),5~8(V後接0~3個I)
$ # 字符串結尾
"""
>>> re.search(pattern,"M",re.VERBOSE) -- 匹配M成功。注:使用鬆散正則表達式必須添加re.VERBOSE(或者re.X)作爲其第三個參數flag
<_sre.SRE_Match object at 0xb7557e30>
>>> re.match(pattern,"M",re.VERBOSE).group() -- 打印匹配文本
'M'
>>> re.match(pattern,"MMMDD",re.VERBOSE).group() -- 五字符D重複2次,不符合羅馬數字規範,不匹配
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute 'group'
>>> re.match(pattern,"MMMDCCCLXXXVIII",re.VERBOSE).group()     -- MMMDCCCLXXXVIII匹配成功
'MMMDCCCLXXXVIII'

所以,使用鬆散正則表達式,我們可以添加足夠詳盡的註釋內容,可以讓你在N年後打開代碼,很快讀懂。這就是鬆散正則表達式的作用!


===================================

引用:

[1] 《Dive into Python》-- Chapter 7 正則表達式:免費下載通道(羅嗦一句,深入Python,新手必看,以實例入手介紹python,好書)

[2] 神文:《Python正則表達式指南》http://www.cnblogs.com/huxi/archive/2010/07/04/1771073.html

[3] 《正則表達式30分鐘入門教程》:免費下載通道

[4] Python 正則表達式 http://www.w3cschool.cc/python/python-reg-expressions.html

===================================

拓展閱讀:

[1] 《精通正則表達式》 中文版 / 英文版


發佈了90 篇原創文章 · 獲贊 434 · 訪問量 106萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章