文章出處

本節內容


  1. re模塊介紹
  2. 使用re模塊的步驟
  3. re模塊簡單應用示例
  4. 關于匹配對象的說明
  5. 說說正則表達式字符串前的r前綴
  6. re模塊綜合應用實例
  7. 參考文檔

提示: 由于該站對MARKDOWN的表格支持的不是很好,所以本文中的表格均以圖片的形式提供,大家如果看著比較模糊,可以放大來看或下載圖片在本地查看。

正則表達式(Regluar Expressions)又稱規則表達式,在代碼中常簡寫為REs,regexes或regexp(regex patterns)。它本質上是一個小巧的、高度專用的編程語言。 通過正則表達式可以對指定的文本實現
匹配測試、內容查找、內容替換、字符串分割 等功能。正則表達式的語法不是本節要講的內容(關于正則表達式的詳細介紹請參考另一篇博文《正則表達式總結》),本節主要介紹的是Python中是如何使用re模塊來完成正則表達式的相關操作的。

一、re模塊介紹


Python中的re模塊提供了一個正則表達式引擎接口,它允許我們將正則表達式編譯成模式對象,然后通過這些模式對象執行模式匹配搜索和字符串分割、子串替換等操作。re模塊為這些操作分別提供了模塊級別的函數以及相關類的封裝。

1. re模塊提供的類

Python中的re模塊中最重要的兩個類:

描述
Regular Expression Objects 正則表達式對象,用于執行正則表達式相關操作的實體
Match Objects 正則表達式匹配對象,用于存放正則表達式匹配的結果并提供用于獲取相關匹配結果的方法

正則表達式對象中的方法和屬性

通過re模塊的compile()函數編譯得到的正則表達式對象(下面用regex表示)支持如下方法:

參數說明:
  • string: 要匹配或處理的字符串
  • pos: 可選參數,表示從string字符串的哪個位置開始,相當于先對字符串做切片處理string[pos:]
  • endpos: 可選參數,表示到string字符串的哪個位置結束(不包含該位置)
  • maxsplit: regex.split()方法的可選參數,表示最大切割次數;默認值為0,表示能切割多少次就盡可能多的切割多少次
  • count: regex.sub()和regex.subn()方法的可選參數,表示最大替換次數;默認為0,表示能替換多少次就盡可能多的替換多少次
  • repl: sub和subn函數中的repl表示replacement,用于指定將匹配到的子串替換成什么內容,需要說明的是該參數的值可以是一個字符串,也可以是一個函數

說明: 如果指定了pos和endpos參數,就相當于在進行正則處理之前先對字符串做切片操作 string[pos, endpos],如rx.search(string, 5, 50)就等價于rx.search(string[5:50]),也等價于rx.search(string[:50], 5);如果endpos比pos的值小,則不會找到任何匹配。

匹配對象中的方法和屬性

調用正則表達式對象的regex.match()、regex.fullmatch()和regex.search()得到的結果就是一個匹配對象,匹配對象支持以下方法和屬性:

參數說明:
  • template: m.expand()方法中的template參數是一個模板字符串,這個字符串中可以使用分組對應的的數值索引進行后向引用(如:\1,\2)或命名后向引用(如\g<1>,\g<NAME>)來表示某個分組的占位符;m.expand()方法的執行過程實際上就是通過sub()方法把template字符串中的這些分組占位符用當前匹配對象中的數據進行替換。
  • default: m.groups()與m.groupdict()方法中的default都是為未匹配成功的捕獲組提供默認匹配值的。
  • group: m.group()、m.start()、m.end()和m.span()方法中的group參數都表示要選擇的分組索引值,1表示第一個分組,2表示第二個分組,依次類推,group參數的默認值是0,表示整個正則表達式所匹配的內容。

2. re模塊提供的函數

re模塊提供了以下幾個模塊級別的函數

將pattern參數只能是字符串;

  • string: 需要用正則表達式來匹配的字符串對象
  • flags: 一個標志位,它會影響正則表達式對象的匹配行為,可取值下面會介紹;但是有一點需要說明的是,只有當pattern參數是字符串時才能指定這個flags參數,否則會報錯;如果pattren參數是一個正則表達式對象,則flags參數需要在調用re.compile()函數時指定。
  • repl: sub和subn函數中的repl表示replacement,用于指定將匹配到的子串替換成什么內容,需要說明的是該參數的值可以是一個字符串,也可以是一個函數
  • count: sub和subn函數中的count表示最多可替換次數
  • maxsplit: split函數中的maxsplit蠶食表示最大分隔次數

說明: 通過對比會發現,上面這些re模塊級別的函數除了re.compile、re.purge和re.escape這幾個函數外,其它函數名都與正則表達式對象支持的方法同名。實際上re模塊的這些函數都是對正則表達式對象相應方法的封裝而已,功能是相同的。只是少了對pos和endpos參數的支持,但是我們可以手動通過字符串切片的方式來達到相應的需求。個人認為,我們應該盡可能的使用模塊級別的函數,這樣可以增強代碼的兼容性。

3. 標志位flags

由上面的描述可知,flags參數在上面這些模塊函數中是要個可選參數,re模塊中預定了該參數可取的值:

說明: 這些flag可以單獨使用,也可以通過邏輯或操作符'|'進行拼接來聯合使用。

二、使用re模塊的步驟


我們有必要對re模塊中所包含的類及其工作流程進行一下簡單的、整體性的說明,這講有利于我們對下面內容的理解。

1. 使用re模塊進行正則匹配操作的步驟

  • 1)編寫表示正則表達式規則的Python字符串str;
  • 2)通過re.compile()函數編譯該Python字符串獲得一個正則表達式對象(Pattern Object)p;
  • 3)通過正則表達式對象的p.match()或p.fullmatch()函數獲取匹配結果--匹配對象(Match Object)m;
  • 4)通過判斷匹配對象m是否為空可知是否匹配成功,也可以通過匹配對象m提供的方法獲取匹配內容。

2. 使用re模塊進行內容查找、替換和字符串分隔操作的步驟

  • 1)編寫表示正則表達式規則的Python字符串str;
  • 2)通過re.compile()函數編譯該Python字符串獲得一個正則表達式對象(Pattern Object)p;
  • 3)通過正則表達式對象的p.search()或p.findall()或p.finditer()或p.sub()或p.subn()或p.split()函數完內容查找、替換和字符串分隔操作并獲取相應的操作結果;

總結: 關于正則表達式的語法和編寫示例請參考《正則表達式總結》)。根據上面的描述可知,將一個表示正則表達式的Python字符串編譯成一個正則表達式對象是使用正則表達式完成相應功能的首要步驟,re模塊中用于完成正則表達式編譯功能的函數為re.compile()。

三、re模塊簡單應用示例


在上面的內容中,我們已經列出了re模塊所提供的類,以及這些類的對象所支持的函數和屬性,還有re模塊所提供的模塊級別的函數。這里我們要討論的是怎樣合理的使用這些方法和函數,換句話說就是在什么情況下使用這些方法和函數,以及如何使用的問題。我們之前說過,正則表達式主要可以用來提供以下幾個功能:

  • 匹配測試
  • 子串/內容查找
  • 子串/內容替換
  • 字符串分割

下面我們就分別通過對這幾個功能的實例實現對上面這些函數和方法以及參數和屬性的使用做下說明和具體的解釋。

1. 匹配測試

匹配測試,意思是通過特定的正則表達式對一個指定的字符串進行匹配來判斷該字符串是否符合這個正則表達式所要求的格式。常見的使用場景舉例:

  • 查看某個字符串(通常來自用戶輸入)是否是一個郵箱或電話號碼
  • 用戶注冊時填寫的用戶名和密碼是否符合指定需求
使用的函數或方法

通過re模塊執行匹配測試時可以使用的函數或方法是:

  • re模塊級別的match()和fullmatch()函數
  • 正則表達式對象的match()和fullmatch()方法
實例1

前面提到過,match()函數或方法只是匹配字符串的開始位置,而fullmatch匹配的是整個字符串;fullmatch()函數或方法就相當于給match()函數或方法的pattern或string參數加上行首邊界元字符'^'和行尾邊界元字符'$',下面來看個例子:

import re

# 定義一個函數來對匹配結果進行展示 
def display_match_obj(match_obj):
    if match_obj is None:
        print('Regex Match Fail!')
    else:
        print('Regex Match Success!', match_obj)

if __name__ == '__main__':
    p = re.compile(r'[a-z]+')
    display_match_obj(p.match('hello'))
    display_match_obj(p.match('hello123'))
    display_match_obj(p.fullmatch('hello'))
    display_match_obj(p.fullmatch('hello123'))
    
    display_match_obj(p.match('123hello'))
    display_match_obj(p.match('123hello', 3))

輸出結果:

Regex Match Success! <_sre.SRE_Match object; span=(0, 5), match='hello'>
Regex Match Success! <_sre.SRE_Match object; span=(0, 5), match='hello'>
Regex Match Success! <_sre.SRE_Match object; span=(0, 5), match='hello'>
Regex Match Fail!
Regex Match Fail!
Regex Match Success! <_sre.SRE_Match object; span=(3, 8), match='hello'>

分析:

  • '[a-z]+'能與'hello'和'hello123'的開頭部分匹配
  • '[a-z]+'能與'hello'完全匹配
  • '[a-z]+'不能與'hello123'完全匹配
  • '[a-z]+'不能與'123hello'的開頭部分匹配
  • '[a-z]+'能與'123hello'的切片'123hello'[3:]的開頭部分匹配
實例2

上面使用的是正則表達式對象的match()和fullmatch()方法,我們也可以通過re提供的模塊級別的函數來實現:

if __name__ == '__main__':
    p = re.compile(r'[a-z]+')
    display_match_obj(re.match(p, 'hello'))
    display_match_obj(re.match(p, 'hello123'))
    display_match_obj(re.fullmatch(p, 'hello'))
    display_match_obj(re.fullmatch(p, 'hello123'))
    
    display_match_obj(re.match(p, '123hello'))
    display_match_obj(re.match(p, '123hello'[3:]))  # 唯一不同的是這里

輸出結果跟上面是一樣的

re模塊的match()和fullmatch()函數不支持pos和endpos參數,所以只能通過字符串切片先對字符串進行切割。

實例3

再來看個對比:

if __name__ == '__main__':
    display_match_obj(re.match(r'[a-z]+', 'hello123'))
    display_match_obj(re.match(r'[a-z]+$', 'hello123'))
    display_match_obj(re.match(r'^[a-z]+$', 'hello123'))

輸出結果:

Regex Match Success! <_sre.SRE_Match object; span=(0, 5), match='hello'>
Regex Match Fail!
Regex Match Fail!

分析:

  • re.match()和re.fullmatch()中的pattern參數可以是正則表達式對象,也可以是Python字符串
  • re.match()函數中的正則表達式參數加上邊界元字符'^'和'$'就相當于re.fullmatch()了
  • 當匹配過程是從字符串的第一個字符開始匹配時re.match(r'^[a-z]+$', 'hello123') 與 re.match(r'[a-z]+$', 'hello123')效果是一樣的,因為re.match()本來就是從字符串開頭開始匹配的;但是,如果匹配過程不是從字符串的第一個字符開始匹配時,它們是有區別的,具體請看下面這個例子。
實例4
if __name__ == '__main__':
    p1 = re.compile(r'[a-z]+$')
    p2 = re.compile(r'^[a-z]+$')
    display_match_obj(p1.match('123hello', 3))
    display_match_obj(p2.match('123hello', 3))

輸出結果:

Regex Match Success! <_sre.SRE_Match object; span=(3, 8), match='hello'>
Regex Match Fail!

這個很好理解,因為元字符'^'匹配的是表示字符串開始位置的特殊字符,而不是字符串內容的第一個字符。match()匹配的是字符串內容的第一個字符,因此即使是在MULTILINE模式,re.match()也將只匹配字符串的開始位置,而不是該字符串每一行的行首。

2. 內容查找

內容查找,意思是通過特定的正則表達式對一個指定的字符串的內容進行掃描來判斷該字符串中是否包含與這個正則表達式相匹配的內容。

使用的函數或方法

通過re模塊執行匹配測試時可以使用的函數或方法是:

  • re模塊級別的search()、findall()和 finditer()函數
  • 正則表達式對象的search()、findall()和finditer()方法
實例1:search() vs match()

這個例子中,我們來看下search()函數的使用,以及它與match()函數的對比。
Python提供了兩個不同的基于正則表達式的簡單操作:

  • re.match(): 該函數僅是在字符串的開始位置進行匹配檢測
  • re.search(): 該函數會在字符串的任意位置進行匹配檢測
import re

string = 'abcdef'
print(re.match(r'c', string))
print(re.search(r'c', string))

輸出結果:

None
<_sre.SRE_Match object; span=(2, 3), match='c'>

這個比較簡單,不做過多解析。

我們應該能夠想到,當正則表達式以'^'開頭時,search()也會從字符串的開頭進行匹配:

import re

string = 'abcdef'
print(re.match(r'c', string))
print(re.search(r'^c', string))
print(re.search(r'^a', string))

輸出結果:

None
None
<_sre.SRE_Match object; span=(0, 1), match='a'>

這里再重復一下上面提到過的內容,就是元字符'^'與match()的從字符串開始位置開始匹配并不是完全等價的。因為元字符'^'匹配的是表示字符串開始位置的特殊字符,而不是字符串內容的第一個字符。match()匹配的是字符串內容的第一個字符,因此即使是在MULTILINE模式,re.match()也將只匹配字符串的開始位置,而不是該字符串每一行的行首;相關search('^...')卻可以匹配每一行的行首。

下面來看個例子:

string = '''
A
B
X
'''

print(re.match(r'X', string, re.MULTILINE))
print(re.search(r'^X', string, re.MULTILINE))

輸出結果:

None
<_sre.SRE_Match object; span=(5, 6), match='X'>
實例2:findall()與finditer()

findall()與finditer()也是用來查找一個字符串中與正則表達式相匹配的內容,但是從名字上就能看出來,findall()與finditer()會講這個字符串中所有與正則表達式匹配的內容都找出來,而search()僅僅是找到第一個匹配的內容。另外findall()返回的是所有匹配到的子串所組成的列表,而finditer()返回的是一個迭代器對象,該迭代器對象會將每一次匹配到的結果都作為一個匹配對象返回。下面來看一個例子:

嘗試找出一個字符串中的所有副詞(英語的副詞通常都是以字母‘ly’結尾):

import re

text = "He was carefully disguised but captured quickly by police."
print(re.findall(r"\w+ly", text))

輸出結果:

['carefully', 'quickly']

如果我們想要獲取關于所有匹配內容的更多信息,而不僅僅是文本信息的話,就可以使用finditer()函數。finditer()可以提供與各個匹配內容相對應的匹配對象,然后我們就可以通過這個匹配對象的方法和屬性來獲取我們想要的信息。

我們來嘗試獲取一個字符串中所有副詞以及它們各自在字符串中的切片位置:

import re

text = "He was carefully disguised but captured quickly by police."

for m in re.finditer(r"\w+ly", text):
    print("%02d-%02d: %s" % (m.start(), m.end(), m.group()))

輸出結果:

07-16: carefully
40-47: quickly

3. 內容替換

傳統的字符串操作只能替換明確指定的子串,而使用正則表達式默認是對一個字符串中所有與正則表達式相匹配的內容進行替換,也可以指定替換次數。

可使用的函數或方法
  • re模塊的sub()和subn()函數
  • 正則表達式對象的sub()和subn()方法

sub函數返回的是被替換后的字符串,如果字符串中沒有與正則表達式相匹配的內容,則返回原始字符串;subn函數除了返回被替換后的字符串,還會返回一個替換次數,它們是以元組的形式返回的。下面來看個例子:將一個字符串中的所有的'-'字符刪除

import re

text = 'pro----gram-files'
print(re.sub(r'-+', '', text))
print(re.subn(r'-+', '', text))

輸出結果:

programfiles
('programfiles', 2)

說明: 被替換的內容(repl參數)可以是一個字符串,還可以是一個函數名。該函數會在每一次匹配時被調用,且該函數接收的唯一的參數是當次匹配相對應的匹配對象,通過這個函數我們可以來做一些邏輯更加復雜的替換操作。

比如,上面那個例子中,如果我們想要得到'program files'這個結果,我們就需要把多個連續的'-'和單個'-'分別替換為 空字符串 和 一個空白字符:

def dashrepl(match_obj):
    if match_obj.group() == '-':
        return ' '
    else:
        return ''

if __name__ == '__main__':
    text = 'pro----gram-files'
    print(re.sub(r'-+', dashrepl, text))
    print(re.subn(r'-+', dashrepl, text))

輸出結果:

program files
('program files', 2)

說明: 當被替換的內容(repl參數)是一個字符串時可以使用'\1'、'\g<NAME>'來引用正則表達式中的捕獲組所匹配到的內容,但是需要注意的是這個字符串必須帶上r前綴。

下面來看個例子:把一個函數名前面加上'py_'前綴

p = r'def\s+([A-Za-z_]\w*)\s*\((?P<param>.*)\)'
repl = r'def py_\1(\g<param>)'
text = 'def myfunc(*args, **kwargs):'
print(re.sub(p, repl, text))

輸出結果:

def py_myfunc(*args, **kwargs):

4. 字符串分割

通過正則表達式對字符串進行分割的過程是:掃描整個字符串,查找與正則表達式匹配的內容,然后以該內容作為分割符對字符串進行分割,最終返回被分割后的子串列表。這對于將文本數據轉換為Python易于讀取和修改的結構化數據非常有用。

可以使用的函數或方法
  • re模塊的split()函數
  • 正則表達式對象的split()方法

我們可以通過maxsplit參數來限制最大切割次數。

實例1:簡單示例
print(re.split(r'\W+', 'Words, words, words.'))
print(re.split(r'\W+', 'Words, words, words.', 1))
print(re.split(r'[a-f]+', '0a3B9', flags=re.IGNORECASE))

輸出結果:

['Words', 'words', 'words', '']
['Words', 'words, words.']
['0', '3', '9']

分析:

  • 第一行代碼中,一共分割了3次(分隔符分別為:兩個', '和一個'.'),因此返回的列表中有4個元素;
  • 第二行代碼中,限制了最大分割次數為1,因此返回的列表中只有2個元素;
  • 第三行代碼中,指定分隔符為一個或多個連續的小寫字字母,但是指定的flag為忽略大小寫,因此大寫字母也可以作為分隔符使用;那么從小寫字母'a'和大寫字母'B'分別進行切割,所以返回的列表中有3個元素。
實例2:捕獲組與匹配空字符串的正則表達式
  • 如果用于作為分割符的正則表達式包含捕獲組,那么該捕獲組所匹配的內容也會作為一個結果元素被返回;
  • 從Python 3.5開始,如果作為分隔符的正則表達式可以匹配一個空字符串,將會引發一個warning;
  • 從Python 3.5開始,如果作為分隔符的正則表達式只能匹配一個空字符串,那么它將會被拒絕使用并拋出異常。
print(re.split(r'(\W+)', 'Words, words, words.'))
print(re.split(r'x*', 'abcxde'))
print(re.split(r'^$', '\nfoo\nbar\n', re.M))

輸出結果:

['Words', ', ', 'words', ', ', 'words', '.', '']

C:\Python35\lib\re.py:203: FutureWarning: split() requires a non-empty pattern match.
  return _compile(pattern, flags).split(string, maxsplit)
['abc', 'de']

Traceback (most recent call last):
  File "C:/Users/wader/PycharmProjects/PythonPro/regex2.py", line 68, in <module>
    print(re.split(r'^$', '\nfoo\nbar\n', re.M))
  File "C:\Python35\lib\re.py", line 203, in split
    return _compile(pattern, flags).split(string, maxsplit)
ValueError: split() requires a non-empty pattern match.
實例3:實現一個電話簿

我們現在要使用Python正則表達式的字符串分割功能實現一個電話簿,具體流程是:從一個給定的文本中讀取非空的行,然后把這些非空行進行切割得到相關信息。

text = """Ross McFluff: 834.345.1254 155 Elm Street

Ronald Heathmore: 892.345.3428 436 Finley Avenue
Frank Burger: 925.541.7625 662 South Dogwood Way


Heather Albrecht: 548.326.4584 919 Park Place"""

entries = re.split(r'\n+', text)
phonebook = [re.split(r':?\s+', entry, 3) for entry in entries]
print(phonebook)

輸出結果:

[
    ['Ross', 'McFluff', '834.345.1254', '155 Elm Street'], 
    ['Ronald', 'Heathmore', '892.345.3428', '436 Finley Avenue'],
    ['Frank', 'Burger', '925.541.7625', '662 South Dogwood Way'],
    ['Heather', 'Albrecht', '548.326.4584', '919 Park Place']
]

說明:實際輸出中是沒有這樣的縮進格式的,這里只是為了方便大家查看。

分析:

  • 上面這個例子總體上有兩個分割過程,第一個分割是通過換行符為分割符得到所有的非空行列表,第二個分割是對得到的每一行文本進行切割,得到每一個聯系人的相關信息;
  • 每一行的內容分別為:firstname, lastname, tel, addr;
  • 由于lastname與tel之間包含一個冒號,而其他內容之間只是包含空白字符,所以用于作為分隔符的正則表達式應該是':?\s+';
  • 由于最后的那個addr是由多個段組成,且這些段之間也有空白字符,此時可以利用maxsplit參數限定最大分割次數,從而使得最后的3個字段作為一個addr整體返回。

四、關于匹配對象的說明


當我們通過re.match或re.search函數得到一個匹配對象m后,可以通過if m is None來判斷是否匹配成功。在匹配成功的條件下,我們可能還想要獲取匹配的值。Python中的匹配對象主要是以“組”的形式來獲取匹配內容的,下面我們來看下具體操作:我們現在要通過一個正則表達式獲取一個字符串中的'姓名'、'年齡'和'手機號碼'

import re

p = re.compile(r'.*name\s+is\s+(\w+).*am\s+(?P<age>\d{1,3})\s+years.*tel\s+is\s+(?P<tel>\d{11}).*', re.DOTALL)

string = '''
My name is Tom,
I am 16 years old,
My tel is 13972773480.
'''
m = re.match(p, string)
# 或
# m = p.match(string)

if m is None:
    print('Regex match fail.')
else:
    result = '''
    name: %s
    age:  %s
    tel:  %s
    '''
    print(result % (m.group(1), m.group(2), m.group('tel')))

輸出結果:

name: Tom
age:  16
tel:  13972773480

分析:

  • 由于要匹配的字符串中包括換行符,為了讓元字符'.'能夠匹配換行符,所以編譯正則表達式需要指定re.DOTALL這個flag;
  • 調用匹配對象的方法或屬性前需要判斷匹配是否成功,如果匹配失敗,得到的匹配對象將是None,其方法和屬性調用會報錯;
  • 對于"非命名捕獲組"只能通過分組索引數字來獲取其匹配到的內容,如m.group(1);而對于命名捕獲組既可以通過分組索引數字來獲取其匹配到的內容,如m.group(2),也可以通過分組名稱來獲取其匹配到的內容,如m.group('tel')。
使用match.expand(template)方法

我們在上面介紹匹配對象所提供的方法時對expand方法進行過說明:

match.expand(template)方法可用于通過得到的匹配對象來構造并返回一個新的字符串,template是一個字符串,用于指定新字符串的格式;從Python 3.5開始,未匹配到的分組將會替換為一個空字符串

這里我們用它來實現上面這個例子的輸出效果,大家體會下它的功能:

import re

p = re.compile(r'.*name\s+is\s+(\w+).*am\s+(?P<age>\d{1,3})\s+years.*tel\s+is\s+(?P<tel>\d{11}).*', re.DOTALL)

string = '''
My name is Tom,
I am 16 years old,
My tel is 13972773480.
'''
# m = re.match(p, string)
# 或
m = p.match(string)

if m is None:
    print('Regex match fail.')
else:
    template_str = '''
    name: \g<1>
    age:  \g<2>
    tel:  \g<tel>
    '''
    print(m.expand(template_str))

輸出結果,跟上面是一樣的。

打印匹配對象中包含的數據

這里,我們主要是想通過對匹配對象各個方法和屬性的調用,讓大家更深刻的理解通過這些方法和屬性我們可以得到什么。

 p = re.compile(r'.*name\s+is\s+(\w+).*am\s+(?P<age>\d{1,3})\s+years.*tel\s+is\s+(?P<tel>\d{11}).*', re.DOTALL)

string = '''
My name is Tom,
I am 16 years old,
My tel is 13972773480.
'''

# m = re.match(p, string)
# 或
m = p.match(string)

if m is None:
    print('Regex Match Fail!')
else:
    print('match_obj.group(): ', '匹配到的所有內容: ', m.group())
    print('match_obj.group(0): ', '同上: ', m.group(0))
    print('match_obj.group(1): ', '第一個捕獲組匹配到的內容: ', m.group(1))
    print('match_obj.group(2): ', '第二個命名捕獲組匹配到的內容: ', m.group(2))
    print('match_obj.group("tel"): ', '第三個命名捕獲組匹配到的內容: ', m.group('tel'))
    print('match_obj.groups(): ', '所有捕獲組匹配到的內容組成的元組對象: ', m.groups())
    print('match_obj.groupdict(): ', '所有命名捕獲組匹配到的內容組成的字典對象: ', m.groupdict())
    print('match_obj: ', '直接打印匹配對象: ', m)
    print('match_obj.string: ', '傳遞給re.match函數的字符串參數: ', m.string)
    print('match_obj.re: ', '傳遞給re.match函數的正則表達式對象: ', m.re)
    print('match_obj.pos, match_obj.endpos: ', '傳遞個match方法的pos和endpos參數: ', m.pos, m.endpos)
    print('match_obj.pos, match_obj.start(1), match_obj.end(1): ', '第一個捕獲組匹配的內容在字符串中的切片位置: ', m.start(1), m.end(1))
    print('match_obj.pos, match_obj.span(1): ', '第一個捕獲組匹配的內容在字符串中的切片位置: ', m.span(1))
    print('match_obj.pos, match_obj.start("tel"), match_obj.end("tel"): ', '命名捕獲組tel匹配的內容在字符串中的切片位置: ', m.start('tel'), m.end('tel'))
    print('match_obj.pos, match_obj.span(tel): ', '命名捕獲組tel匹配的內容在字符串中的切片位置: ', m.span('tel'))

輸出結果:

match_obj.group():  匹配到的所有內容:  
    My name is Tom,
    I am 16 years old,
    My tel is 13972773480.
    
match_obj.group(0):  同上:  
    My name is Tom,
    I am 16 years old,
    My tel is 13972773480.
    
match_obj.group(1):  第一個捕獲組匹配到的內容:  Tom
match_obj.group(2):  第二個命名捕獲組匹配到的內容:  16
match_obj.group("tel"):  第三個命名捕獲組匹配到的內容:  13972773480
match_obj.groups():  所有捕獲組匹配到的內容組成的元組對象:  ('Tom', '16', '13972773480')
match_obj.groupdict():  所有命名捕獲組匹配到的內容組成的字典對象:  {'age': '16', 'tel': '13972773480'}
match_obj:  直接打印匹配對象:  <_sre.SRE_Match object; span=(0, 75), match='\n    My name is Tom,\n    I am 16 years old,\n  >
match_obj.string:  傳遞給re.match函數的字符串參數:  
    My name is Tom,
    I am 16 years old,
    My tel is 13972773480.
    
match_obj.re:  傳遞給re.match函數的正則表達式對象:  re.compile('.*name\\s+is\\s+(\\w+).*am\\s+(?P<age>\\d{1,3})\\s+years.*tel\\s+is\\s+(?P<tel>\\d{11}).*', re.DOTALL)
match_obj.pos, match_obj.endpos:  傳遞個match方法的pos和endpos參數:  0 75
match_obj.pos, match_obj.start(1), match_obj.end(1):  第一個捕獲組匹配的內容在字符串中的切片位置:  16 19
match_obj.pos, match_obj.span(1):  第一個捕獲組匹配的內容在字符串中的切片位置:  (16, 19)
match_obj.pos, match_obj.start("tel"), match_obj.end("tel"):  命名捕獲組tel匹配的內容在字符串中的切片位置:  58 69
match_obj.pos, match_obj.span(tel):  命名捕獲組tel匹配的內容在字符串中的切片位置:  (58, 69)

五、說說正則表達式字符串前的r前綴


前面我們介紹re模塊使用步驟時,第一步就是“編寫一個表示正則表達式規則的Python字符串”,那么正則表達式與Python字符串之間是什么關系呢?另外,我們通常都在一個正則表達式字符串前加上一個前綴r是何用意呢?

因為正則表達式并不是Python語言的核心部分(有些應用程序根本就不需要正則表達式,re模塊就像socket或zlib一樣,只是包含在Python中的一個簡單的C語言擴展模塊),也沒有創建專門的語法來表示它們,所以在Python中正則表達式是以Python字符串的形式來編寫并進行處理的。但是,我們應該知道的是字符串有 字符串的字面值(我們給一個字符串賦的值)字符串的實際值(這個字符串的打印值),且正則表達式引擎把一個字符串作為正則表達式處理時,所取的是這個字符串的實際值,而不是它的字面值。

問題1:

字符串的字面值 與 字符串的實際值 不一定是等價的,因為有些字符組合起來表示的是一個特殊的符號,此時會導致正則表達式錯誤而無法匹配到想要的結果。比如:\1在正則表達式中表示引用第一個捕獲組所匹配到的內容,而在字符串中卻表示一個特殊的字符,因此如果一個表示正則表達式的字符串中包含"\1"且沒有做任何處理的話,它是無法匹配到我們想要的結果的。如下圖所示:

如果想要匹配正確結果就需要對該字符串中表示特殊字符的字符組合進行處理,我們只需要在'\1'前加一個反斜線對'\1'中的反斜線進行轉義就可以了,即字符串"\1"的字面值才是\1。如下圖所示:

也就是說,一個正則表達式要想用一個Python字符串來表示時,可能需要對這個字符串的字面值做一些特殊的轉義處理。

問題2:

現在反過來考慮,\s在正則表達式中表示空白字符,如果我們想匹配一個字符串中'\s'這兩個字符,就需要在正則表達式中\s前也加一個反斜線進行轉義\\s。那么這時候表示正則表達式的字符串同樣不能直接寫'\\s',因為它是字符串字面值,其實際值是\s,因此此時也是不能匹配到我們想要的結果的。如下圖所示:

如果想要匹配正確結果就需要對該字符串進行一些處理,我們需要在'\\s'前加兩個反斜線對'\\s'中的兩個反斜線分別進行轉義,即字符串"\\\\s"的字面值才是\\s。如下圖所示:

字符串的r前綴

通過上面兩個問題我們可以得出以下結論:

  • 1)這些問題都是由于反斜線引起的,這種現象也被稱為"反斜線瘟疫";
  • 2)Python字符串與正則表達式都使用反斜線進行字符轉義,正是由于這種沖突才引起了這些問題;
  • 3)當一個正則表達式中又很多個反斜線要處理時,那無疑將是一種災難。

怎么辦呢?忘掉上面這所有的問題吧,你只需要在表示正則表達式的字符串前加上一個前綴r就可以把這個字符串當然正則表達式來寫了。 r是Raw,即“原始”的意思,帶有r前綴的字符串就叫做“Raw String”,即原始字符串的意思。也就是說當一個字符串帶有r前綴時,它的字面值就是其真實值,也就是正則表達式的值。

簡單來說,表示正則表達式匹配規則的字符串前面的r前綴是為了解決字符串中的反斜線與正則表達式中的反斜線引起的沖突問題。另外,我們根本無需關心哪些字符會引起這樣的沖突,只需要在每個表示正則表達式的字符串前加上一個r前綴就可以了。

六、re模塊綜合應用實例


0. 實例背景與準備工作

假設你正在寫一個撲克游戲,每張牌分別用一個字符來表示:

  • 'a' 代表老A,也叫“尖兒”
  • 'k' 代表老K
  • 'q' 代表Q,也叫“圈兒”
  • 'j' 代表J, 也叫“鉤兒”
  • 't' 代表10
  • '2'-'9' 分別代表2-9這幾個數字
    另外,游戲規則要求每個玩家手中要有5張牌,不能多也不能少。

首先,我們先來寫一個工具函數來幫助我們更優雅的展示匹配結果:

def display_match_obj(match_obj):
    if match_obj is None:
        return None
    return '<Match: %r, groups=%r>' % (match_obj.group(), match_obj.groups())

實例1. 首先匹配玩家手中的牌是否符合游戲規則

匹配規則分析

游戲規則有兩個:

  • 1)玩家手中的牌只能是'a', 'k', 'q', 'j', 't' 和 '2'-'9'中的字符
  • 2)玩家手中的牌只能有5張
所需知識點
  • 字符群組元字符:[]
  • 量詞元字符:{n}
  • 邊界匹配元字符:'^'和'$'
正則表達式

正確的正則表達式應該是這樣的:'^[akqjt2-9]{5}$'

匹配測試

下面來看下面幾個匹配測試

import re

p = re.compile(r"^[atjqk2-9]{5}$")

display_match_obj(p.match("akt5q"))
display_match_obj(p.match("akt5e"))
display_match_obj(p.match("akt"))
display_match_obj(p.match("727ak"))
display_match_obj(p.match("aaaak"))

輸出結果:

<Match: 'akt5q', groups=()>
Invalid
Invalid
<Match: '727ak', groups=()>
<Match: 'aaaak', groups=()>

分析:

  • 'akt5e'中的包含非法字符'e',因此匹配不成功
  • 'akt'中字符個數不是5,因此匹配不成功
  • groups為空元組表示沒有匹配到分組信息

實例2. 匹配玩家手中的牌是否包含對子(兩張一樣的牌)

匹配規則分析

這里,我們先不考慮上面的規則,僅僅考慮需要包含兩張一樣的牌。

所需知識點
  • 分組與向后引用:(...)和\n
正則表達式

正確的正則表達式應該是這樣的:'.*(.).*\1.*'

匹配測試

下面來看下面幾個匹配測試

import re

p = re.compile(r".*(.).*\1.*")

display_match_obj(p.match("akt5q"))
display_match_obj(p.match("akt5e"))
display_match_obj(p.match("akt"))
display_match_obj(p.match("727ak"))
display_match_obj(p.match("aaaak"))

輸出結果:

Invalid
Invalid
Invalid
<Match: '727ak', groups=('7',)>
<Match: 'aaaak', groups=('a',)>

分析:

  • 只有'727ak'和'aaaak'中的包含兩個相同的字符,其他的字符串都不匹配。
  • '727ak'和'aaaak'中被分組匹配到的字符分別是'7'和'a'
思考:如果想匹配包含3張或者4張相同牌,正則表達式該怎樣寫呢?

其實很簡單,只要重復相應次數的 '\1.*' 就可以了:

  • 匹配包含3張相同牌的正則表達式:'.*(.).*\1.*\1.*'
  • 匹配包含4張相同牌的正則表達式:'.*(.).*\1.*\1.*\1.*'

實例3:把上面兩個例子的匹配需求整合起來

匹配分析

這個要求的難點在于,這兩個匹配需求的匹配規則相對獨立無法用一個常規的正則表達式匹配模式來表達。要把多個相對獨立的正則表達式整合到一起,我們有兩種實現方式:

  • 1)匹配兩次:先用第一個例子中的正則表達式進行匹配,匹配通過再進行第二個匹配
  • 2)使用元字符特殊構造:(?=...)
第1種實現方式:匹配兩次
import re

def process_match(string):
    p1 = re.compile(r"^[akqjt2-9]{5}$")
    if p1.match(string):
        p2 = re.compile(r".*(.).*\1.*")
        display_match_obj(p2.match(string))
    else:
        display_match_obj(None)

if __name__ == "__main__":
    process_match("727a")
    process_match("727akk")
    process_match("72tak")
    process_match("727ak")

輸出結果:

Invalid
Invalid
Invalid
<Match: '727ak', groups=('7',)>

分析:

  • '727a'和'727akk'中的字符個數都不滿足第一個正則表達式,因此匹配失敗
  • '72tak'雖然滿足第一個正則表達式,但是不包含對子,因此也匹配失敗
  • 只有'727ak'既滿足第一個正則表達式,又包含對子(一對7),因此匹配成功
  • 可見,兩個正則表達式都生效了
第2種實現方式:使用元字符特殊構造 (?=...)
import re

p = re.compile(r"(?=^[akqjt2-9]{5}$)(?=.*(.).*\1.*)")

display_match_obj(p.match("727a"))
display_match_obj(p.match("727akk"))
display_match_obj(p.match("72tak"))
display_match_obj(p.match("727ak"))

輸出結果:

Invalid
Invalid
Invalid
<Match: '', groups=('7',)>

分析:

  • 由輸出結果可知,同樣也是最后一個匹配成功,因此這個正則表達式'(?=^[akqjt2-9]{5}$)(?=.*(.).*\1.*)'是滿足要求的;
  • 另外,發現Match的結果為空,這是因為特殊構造 (?=...) 在匹配過程中是不消費字符的,這也就說明這種特殊構造只適合做匹配測試,不能獲取匹配到的內容

七、參考文檔


問題交流群:666948590


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()