python進階(20) 正規表示式的超詳細使用

Silent丿丶黑羽發表於2021-08-31

正規表示式

  正規表示式(Regular Expression,在程式碼中常簡寫為regexregexpREre)是預先定義好的一個“規則字元率”,通過這個“規則字串”可以匹配、查詢和替換那些符合“規則”的文字。
  雖然文字的查詢和替換功能可通過字串提供的方法實現,但是實現起來極為困難,而且運算效率也很低。而使用正規表示式實現這些功能會比較簡單,而且效率很高,唯一的困難之處在於編寫合適的正規表示式。
  Python 中正規表示式應用非常廣泛,如資料探勘、資料分析、網路爬蟲、輸入有效性驗證等,Python 也提供了利用正規表示式實現文字的匹配、查詢和替換等操作的 re 模組。
 

1.1 正規表示式字串

正規表示式是一種字串,正規表示式字串是由普通字元和元字元組成的。
1)普通字元
普通字元是按照字元字面意義表示的字元。
2)元字元
元字元是預先定義好的一些特定字元,比如\w\.都屬於元字元。
 

1.1.1 元字元

元字元(Metacharacters)是用來描述其他字元的特殊字元,它由基本元字元和普通字元構成。基本元字元是構成元字元的組成要素。基本元字元主要有14個,具體如下圖所示。

字元 說明
\ 轉義符,表示轉義
. 表示任意一個字元
+ 表示重複1次或多次
* 表示重複0次或多次
? 表示重複0次或1次
| 選擇符號,表示“或關係”,例如:A | B 表示匹配A或B
{} 定義量詞
[] 定義字元類
() 定義分組
^ 可以表示取反,或匹配一行的開始
$ 匹配一行的結束

上面表格中\w+ 是元字元,它由兩個基本元字元(\+)和一個普通字元 w構成。另外,還有.元字元,它由兩個基本元字元\,構成。
學習正規表示式某種意義上講就是在學習元字元的使用,元字元是正規表示式的重點也是難點。下面會分門別類地介紹元字元的具體使用。
 

1.1.2 字元轉義

在正規表示式中有時也需要字元轉義,比如 w字元不表示英文字母 w,而是表示任何語言的單詞字元(如英文字母、亞洲文字等)、數字和下畫線等內容時,需要在w 字母前加上反斜槓\。反斜槓\也是基本元字元,與 Python 語言中的字元轉義是類似的。不僅可以對普通字元進行轉義,還可以對基本元字元進行轉義。如上面的表格,其中點.字元是希望按照點.的字面意義使用,作為.com域名的一部分,而不是作為.基本元字元使用,所以需要加反斜槓\進行轉義,即\.才是表示點.的字面意義。
 

1.1.3 開始與結束字元

本節通過一個示例介紹在 Python中如何使用正規表示式。
在1.1.1 節介紹基本元字元時介紹了^$,它們可以用於匹配一行字串的開始和結束。當以^開始時,要求一行字串的開始位置匹配:當以$結束時,要求一行字串的結位置匹配。所以正規表示式\w+@jiakecong.com^w+@jiakecong.com$是不同的。
示例程式碼如下:

import re


p1 = r'\w+@jiakecong\.com'
p2 = r'^\w+@jiakecong\.com$'
text = "Tony 's email is tony_guan111@jiakecong.com"
m = re.search(p1, text)
print(m)

m = re.search(p2, text)
print(m)

email = "tony_guan111@jiakecong.com"
m = re.search(p2, email)
print(m)

輸出結果如下:

<re.Match object; span=(17, 43), match='tony_guan111@jiakecong.com'>
None
<re.Match object; span=(0, 26), match='tony_guan111@jiakecong.com'>

 

1..2 字元類

在正規表示式中可以使用字元類,一個字元類定義一組字元,其中的任一一個字元出現在輸入字串中即匹配成功。注意每次匹配只能匹配字元類中的一個字元。
 

1.2.1 定義字元類

定義一個普通的字元類需要使用[]元字元類。例如想在輸入字串中匹配Javajava,可以使用正規表示式[Jj]ava,示例程式碼如下:

p = r'[Jj]ava'
m = re.search(p, 'I like Java and Python')
print(m)

m = re.search(p, 'I like JAVA and Python')
print(m)

m = re.search(p, 'I like java and python')
print(m)

輸出結果如下:

<re.Match object; span=(7, 11), match='Java'>
None
<re.Match object; span=(7, 11), match='java'>

上述程式碼中除了JAVA不匹配正規表示式[Jj]ava,其他都匹配
 

1.2.2 字串取反

在正規表示式中指定不想出現的字元,可以在字元類前加^符號。示例程式碼如下:

import re

p = r'[^0123456789]'

m = re.search(p, '1000')
print(m)

m = re.search(p, 'python')
print(m)

上述程式碼定義的正規表示式[^0123456789],它表示輸入字串中出現非0-9數字即匹配,即出現在[0123456789]以外的任意一字元即匹配
 

1.2.3 區間

上面示例中的[^0123456789]正規表示式,事實上有些麻煩,這種連續的數字可以使用區間表示。區間是用連字元-表示的,例如[0123456789]採用區間表示為[0-9][^0123456789]採用區間表示為[^0-9]。區間還可以表示連續的英文字母字元類,例如[a-z]表示所有小寫字母字元類,[A-Z]表示所有大寫字母字元類。
另外,也可以表示多個不同區間,[A-Za-z0-9]表示所有字母和數字字元類,[0-25-7]表示0、1、2、5、6、7幾個字元組成的字元類。
示例程式碼如下:

import re

m = re.search('[A-Za-z0-9]', 'A10.3')
print(m)

m = re.search(r'[0-25-7]', 'A3489C')
print(m)

輸出結果如下:

<re.Match object; span=(0, 1), match='A'>
None

 

1.2.4 預定義字元類

有些字元類很常用,例如[0-9]等。為了書寫方便,正規表示式提供了預定義的字元類,例如預定義字元類\d等價於[0-9]字元類。預定義字元類如下圖所示

字元 說明
. 匹配任意一個字元
\ 匹配反斜槓\字元
\n 匹配換行
\r 匹配回車
\f 匹配一個換頁符
\t 匹配一個水平製表符
\v 匹配一個垂直製表符
\s 匹配一個空格符,等價於[\t\n\r\f\v]
\S 匹配一個非空格符,等價於[^\s]
\d 匹配一個數字字元,等價於[0-9]
\D 匹配一個非數字字元,等價[^0-9]
\w 匹配任意語言的單詞字元、數字和下劃線'_'等字元,如果正規表示式標誌設定為ASCII,則只匹配[a-zA-Z0-9]
\W 等價於[^\w]

示例程式碼如下:

import re

p = r'\D'
m = re.search(p, 'assss')
print(m)

m = re.search(p, '1000')
print(m)

text = '你們好hello'
m = re.search(r'\w', text)
print(m)

輸出結果如下:

<re.Match object; span=(0, 1), match='a'>
None
<re.Match object; span=(0, 1), match='你'>

上述程式碼正規表示式\D就等於[^0123456789]。另一個正規表示式\w表示任意字元,會在text字串中查詢匹配字元,找到的結果是字元。
 

1.3 量詞

之前學習的正規表示式元字元只能匹配顯示一次字元或字串,如果想匹配顯示多次字元或字串可以使用量詞
 

1.3.1 量詞的使用

量詞表示字元或字串重複的次數,正規表示式中的量詞如下表:

字元 說明
? 出現0或1次
* 出現0或多次
+ 出現1或多次
{n} 出現n次
{n,m} 至少出現n次,但不超過m次
{n,} 至少出現n次

量詞的使用示例程式碼如下:

import re

m = re.search(r'\d?', '87654321')
print(m)

m = re.search(r'\d?', 'ABC')
print(m)

m = re.search(r'\d*', '87654321')
print(m)

m = re.search(r'\d*', 'ABC')
print(m)

m = re.search(r'\d+', '87654321')
print(m)

m = re.search(r'\d+', 'ABC')
print(m)

m = re.search(r'\d{8}', '87654321')
print(m)

m = re.search(r'\d{8}', 'ABC')
print(m)

m = re.search(r'\d{7,8}', '87654321')
print(m)

m = re.search(r'\d{9, }', '87654321')
print(m)

輸出結果如下:

<re.Match object; span=(0, 1), match='8'>
<re.Match object; span=(0, 0), match=''>
<re.Match object; span=(0, 8), match='87654321'>
<re.Match object; span=(0, 0), match=''>
<re.Match object; span=(0, 8), match='87654321'>
None
<re.Match object; span=(0, 8), match='87654321'>
None
<re.Match object; span=(0, 8), match='87654321'>
None

 

1.3.2 貪婪量詞和懶惰量詞

量詞還可以細分為貪婪量詞和懶惰量詞,貪婪量詞會盡可能多地匹配字元,懶惰量詞會盡可能少地匹配字元。大多數計算機語言的正規表示式量詞預設是貪婪的,要想使用懶惰量詞在量詞後面加?即可
示例程式碼如下:

import re

m = re.search(r'\d{5,8}', '87654321')
print(m)

m = re.search(r'\d{5,8}?', '87654321')
print(m)

輸出結果如下:

<re.Match object; span=(0, 8), match='87654321'>
<re.Match object; span=(0, 5), match='87654'>

上述程式碼使用了貪婪量詞{5,8},輸入字串87654321是長度8位的數字字串,儘可能多地匹配字元結果是87654321。程式碼使用懶惰量詞{5,8}?,輸入字串87654321是長度8位的數字字串,儘可能少的匹配字元結果是87654
 

1.4 分組

在此之前學習的量詞只能重複顯示一個字元,如果想讓一個字串作為整體使用量詞,可將整個字串放到一對小括號中,這就是分組(也稱子表示式)
 

1.4.1 分組的使用

對正規表示式進行分組不經可以對一個字串整體使用量詞,還可以在正規表示式中引用已經存在的分組。示例程式碼如下:

import re

p = r'(121){2}'
m = re.search(p, '121121abcabc')
print(m)
print(m.group())  # 返回匹配的字串
print(m.group(1))  # 返回第一組內容

p = r'(\d{3,4})-(\d{7,8})'
m = re.search(p, '010-87654321')
print(m)
print(m.group())  # 返回匹配字串
print(m.groups())  # 獲得所有組內容

輸出結果如下:

<re.Match object; span=(0, 6), match='121121'>
121121
121
<re.Match object; span=(0, 12), match='010-87654321'>
010-87654321
('010', '87654321')

上述程式碼定義的正規表示式(121)是將121字串分為一組,(121){2}表示對121重複兩次,即121121。程式碼呼叫match物件的group()方法返回匹配的字串,group()方法語法如下:

match.group([group1, ...])

其中引數group1是組編號,在正規表示式中組編號是從1開始的,所以程式碼正規表示式m.group(1)表示返回第一組內容
程式碼 r'(\d{3,4})-(\d{7,8})'正規表示式可以用來驗證固定電話號碼,在-之前是3-4位的區號,-之後是7-8位的電話號碼。在該正規表示式中有兩個分組。程式碼m.groups()方法是返回所有分組,返回值是一個元組
 

1.4.2 分組命名

在Python程式中訪問分組時,除了可以通過組編號進行訪問,還可以通過組名進行訪問,前提是要在正規表示式中為組命名。組命名通過在組開頭新增?P<分組名>實現。
示例程式碼如下:

import re

p = r'(?P<area_code>\d{3,4})-(?P<phone_code>\d{7,8})'
m = re.search(p, '010-87654321')
print(m)
print(m.group())  # 返回匹配字串
print(m.groups())  # 獲得所有組內容

# 通過組編號返回組內容
print(m.group(1))
print(m.group(2))

# 通過組名返回組內容
print(m.group('area_code'))
print(m.group('phone_code'))

輸出結果如下:

<re.Match object; span=(0, 12), match='010-87654321'>
010-87654321
('010', '87654321')
010
87654321
010
87654321

上述程式碼其實和1.4.1的程式碼是一樣的,只是給正規表示式命名了,以後就可以通過組編號或組名字來訪問
 

1.4.3 反向引用分組

除了可以在程式diamante中訪問正規表示式匹配之後的分組內容,還可以再正規表示式內部引用之前的分組。
下面通過示例熟悉以下反向引用分組。假設由於工作需要想解析一段XML程式碼,需要找到某一個開始標籤和結束標籤,示例程式碼如下:

import re

p = r'<([\w]+)>.*</([\w]+)>'
m = re.search(p, '<a>abc</a>')
print(m)

p = r'<([\w]+)>.*</([\w]+)>'
m = re.search(p, '<a>abc</b>')
print(m)

輸出結果如下:

<re.Match object; span=(0, 10), match='<a>abc</a>'>
<re.Match object; span=(0, 10), match='<a>abc</b>'>

上述程式碼的正規表示式分成了兩組,兩組內容完全一樣。但是測試結果發現他們都是匹配的,但是<a>abc</b>明顯不是有效的XML程式碼,因為開始標籤和結束標籤應該是一致的。可見程式碼r'<([\w]+)>.*</([\w]+)>'並不能保證開始標籤和結束標籤是一致的。為了解決此問題,可以引用反向引用,即讓第二組反向引用第一組。在正規表示式中反向引用語法是\組編號,組編號是從1開始的。示例程式碼如下:

import re

p = r'<([\w]+)>.*</\1>'  # 使用了反向引用  ①
m = re.search(p, '<a>abc</a>')
print(m)  # 匹配

m = re.search(p, '<a>abc</b>')
print(m)  # 不匹配

輸出結果如下:

<re.Match object; span=(0, 10), match='<a>abc</a>'>
None

上述程式碼第①行時定義正規表示式,其中\1是反向引用第一個組,從執行結果可見字串<a>abc</a>是匹配的,而<a>abc</b>字串不匹配
 

1.4.4 非捕獲分組

前面介紹的分組稱為捕獲分組。捕獲分組的匹配子表示式結果被暫時儲存到記憶體中,以備表示式或其他程式引用,這個過程稱為"捕獲",捕獲結果可以通過組編號或組名進行引用。但是有時並不想引用子表示式的匹配結果,不想捕獲匹配結果,只是將小括號作為一個整體進行匹配,此時可以使用非捕獲分組,在組開頭使用?,可以實現非捕獲分組
示例程式碼如下:

import re

s = 'img1.jpg,img2.jpg,img3.bmp'

# 捕獲分組
p = r'\w+(\.jpg)'
mlist = re.findall(p, s)       ①
print(mlist)

# 非捕獲分組
p = r'\w+(?:\.jpg)'
mlist = re.findall(p, s)       ②
print(mlist)

輸出結果如下:

['.jpg', '.jpg']
['img1.jpg', 'img2.jpg']

上述程式碼實現了從字串中查詢.jpg結尾的文字,其中程式碼第①行和第②行的正規表示式區別在於前者是捕獲分組,後者是非捕獲分組。捕獲分組將括號中的內容作為子表示式進行捕獲匹配,將匹配的子表示式(即組的內容)返回,結果是['.jpg','.jpg']。而非捕獲分組將括號中的內容作為普通的正規表示式字串進行整體匹配,即找到.jpg結尾的文字,所以最後結果是['img1.jpg', 'img2.jpg']
 

1.5 re模組

rePython內建的正規表示式模組,前面雖然使用過re模組一些函式,但還有很多重要函式沒有詳細介紹,這一節將詳細介紹這些函式
 

1.5.1 search()和match()函式

search()match()函式非常相似,它們的區別如下所示

  • search():在輸入字串中查詢,返回第一個匹配內容,如果找到一個則match物件,如果沒有找到返回None
  • match():在輸入字串開始處查詢匹配內容,如果找到一個則match物件,如果沒有找到返回None

示例程式碼如下:

import re

p = r'\w+@jiakecong\.com'

text = "Tony 's email is tony_guan111@jiakecong.com"  ①
m = re.search(p, text)
print(m)

m = re.match(p, text)
print(m)

email = 'tony_guan111@jiakecong.com'        ②
m = re.search(p, email)
print(m)

m = re.match(p, email)
print(m)

# match物件幾個方法
print('match物件幾個方法:')                      ③
print(m.group())
print(m.start())
print(m.end())
print(m.span())

輸出結果如下:

<re.Match object; span=(17, 43), match='tony_guan111@jiakecong.com'>
None
<re.Match object; span=(0, 26), match='tony_guan111@jiakecong.com'>
<re.Match object; span=(0, 26), match='tony_guan111@jiakecong.com'>
match物件幾個方法:
tony_guan111@jiakecong.com
0
26
(0, 26)

上述程式碼第①行輸入字串開頭不是emailsearch()函式可以匹配成功,而match()函式卻匹配失敗。程式碼第②行輸入字串開頭就是email格式的郵箱,所以search()match()函式都可以匹配成功
searchmatch()函式如果匹配成功都返回match物件。match物件有一些常用方法,見程式碼第③行。其中group()方法返回匹配的子字串;start()方法返回子字串的開始索引;end()方法返回子字串的結束索引;span方法返回子字串的跨度,它是一個二元素的元組。
 

1.5.2 findall()和finditer()函式

findall()finditer()函式非常相似,它們的區別如下所示

  • findall():在輸入字串中查詢所有匹配內容,如果匹配成功,則返回match列表物件,如果匹配失敗則返回None
  • finditer():在輸入字串中查詢所有匹配內容,如果匹配成功,則返回容納match的可迭代物件,通過迭代物件每次可以返回一個match物件,如果匹配失敗則返回None

示例程式碼如下:

import re

p = r'[Jj]ava'
text = 'I like Java and java'

match_list = re.findall(p, text)       ①
print(match_list)

match_iter = re.finditer(p, text)     ②
for m in match_iter:                    ③
    print(m.group())

輸出結果如下:

['Java', 'java']
Java
java

上述程式碼第①行的findall()函式返回match列表物件。程式碼第②行的finditer()函式返回可迭代物件。程式碼第③行通過for迴圈遍歷可迭代物件
 

1.5.3 字串分割

字串分割使用split函式,該函式按照匹配的子字串進行字串分割,返回字串列表物件

re.split(pattern, string, maxsplit=0, flags=0)

其中引數pattern是正規表示式;引數string是要分割的字串;引數maxsplit是最大分割次數,maxsplit預設值為零,表示分割次數沒有限制;引數flags是編譯標誌
示例程式碼如下:

import re

p = r'\d+'
text = 'AB12CD34EF'

clist = re.split(p, text)   ①
print(clist)

clist = re.split(p, text, maxsplit=1)    ②
print(clist)

clist = re.split(p, text, maxsplit=2)    ③
print(clist)

輸出結果如下:

['AB', 'CD', 'EF']
['AB', 'CD34EF']
['AB', 'CD', 'EF']

上述程式碼呼叫split()函式通過數字對AB12CD34EF字串進行分割,\d+正規表示式匹配一到多個數字。程式碼第①行split()函式中引數maxsplitflags是預設的,分割的次數沒有限制,分割結果是['AB', 'CD', 'EF']列表
程式碼第②行split()函式指定maxsplit為1,分割結果是['AB', 'CD34EF']列表,列表元素的個數是maxsplit+1
程式碼第③行split()函式指定maxsplit為2,2是最大可能的分割次數,因此maxsplit≥2maxsplit=0是一樣的。
 

1.5.4 字串替換

字串替換使用sub()函式,該函式用於替換匹配的子字串,返回值是替換之後的字串。

re.sub(pattern, rep1, string, count=0, flags=0)

其中引數pattern是正規表示式;引數rep1是替換字串;引數string是要提供的字串;引數count是要替換的最大數量,預設值為零,表示替換數量沒有限制;引數flags是編譯標誌
示例程式碼如下:

import re

p = r'\d+'
text = 'AB12CD34EF'

replace_text = re.sub(p, ' ', text)      ①
print(replace_text)

replace_text = re.sub(p, ' ', text, count=1)     ②
print(replace_text)

replace_text = re.sub(p, ' ', text, count=2)     ③
print(replace_text)

輸出結果如下:

AB CD EF
AB CD34EF
AB CD EF

上述程式碼呼叫sub()函式替換AB12CD34EF字串中的數字。程式碼第①行sub()函式中引數countflags都是預設的,替換的最大數量沒有限制,替換結果是AB CD EF
程式碼第②行sub()函式指定count為1,替換結果是AB CD34EF
程式碼第③行sub()函式指定count為2,2是最大可能的替換次數,因此count≥2count=0是一樣的。

相關文章