正則表達式
正則表達式(Regular Expression
,在代碼中常簡寫為regex
、 regexp
、RE
或re
)是預先定義好的一個“規則字符率”,通過這個“規則字符串”可以匹配、查找和替換那些符合“規則”的文本。
雖然文本的查找和替換功能可通過字符串提供的方法實現,但是實現起來極為困難,而且運算效率也很低。而使用正則表達式實現這些功能會比較簡單,而且效率很高,唯一的困難之處在於編寫合適的正則表達式。
Python 中正則表達式應用非常廣泛,如數據挖掘、數據分析、網絡爬蟲、輸入有效性驗證等,Python 也提供了利用正則表達式實現文本的匹配、查找和替換等操作的 re
模塊。
1.1 正則表達式字符串
正則表達式是一種字符串,正則表達式字符串是由普通字符和元字符組成的。
1)普通字符
普通字符是按照字符字面意義表示的字符。
2)元字符
元字符是預先定義好的一些特定字符,比如\w
、\.
都屬於元字符。
1.1.1 元字符
元字符(Metacharacters)是用來描述其他字符的特殊字符,它由基本元字符和普通字符構成。基本元字符是構成元字符的組成要素。基本元字符主要有14個,具體如下圖所示。
字符 | 說明 |
---|---|
\ | 轉義符,表示轉義 |
. | 表示任意一個字符 |
+ | 表示重復1次或多次 |
* | 表示重復0次或多次 |
? | 表示重復0次或1次 |
` | ` |
{} | 定義量詞 |
[] | 定義字符類 |
() | 定義分組 |
^ | 可以表示取反,或匹配一行的開始 |
$ | 匹配一行的結束 |
上面表格中\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 定義字符類
定義一個普通的字符類需要使用[
和]
元字符類。例如想在輸入字符串中匹配Java
或java
,可以使用正則表達式[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模塊
re
是Python
內置的正則表達式模塊,前面雖然使用過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)
上述代碼第①行輸入字符串開頭不是email
,search()
函數可以匹配成功,而match()
函數卻匹配失敗。代碼第②行輸入字符串開頭就是email格式的郵箱,所以search()
和match()
函數都可以匹配成功
search
和match()
函數如果匹配成功都返回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()
函數中參數maxsplit
和flags
是默認的,分割的次數沒有限制,分割結果是['AB', 'CD', 'EF']
列表
代碼第②行split()
函數指定maxsplit
為1,分割結果是['AB', 'CD34EF']
列表,列表元素的個數是maxsplit+1
。
代碼第③行split()
函數指定maxsplit
為2,2是最大可能的分割次數,因此maxsplit≥2
與maxsplit=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()
函數中參數count
和flags
都是默認的,替換的最大數量沒有限制,替換結果是AB CD EF
代碼第②行sub()
函數指定count
為1,替換結果是AB CD34EF
代碼第③行sub()
函數指定count
為2,2是最大可能的替換次數,因此count≥2
與count=0
是一樣的。
1.6 編譯正則表達式
上面所有介紹的正則表達式內容足以開發實際項目了,但是為了提高效率,還可以對Python正則表達式進行編譯。編譯的正則表達式可以重復使用,這樣能減少正則表達式的解析和驗證,提高效率
在re
模塊中的compile()
函數可以編譯正則表達式,compile()
函數語法如下:
re.compile(pattern[, flags=0])
其中參數pattern
是正則表達式,參數flags
是編譯標志。compile()
函數返回一個編譯的正則表達式對象regex
1.6.1 已編譯正則表達式對象
正則表達式方法需要一個已編譯的正則表達式對象才能調用,這些方法與re模塊函數功能類似。
實際例子如下:
import re
p = r'\w+@jkc\.com'
regex = re.compile(p)
text = "Tony's email is tony_g588@jkc.com"
m = regex.search(text)
print(m) # <re.Match object; span=(16, 33), match='tony_g588@jkc.com'>
m = regex.match(text)
print(m) # None
p = r'[Jj]ava'
regex = re.compile(p)
text = "I like Java and java"
match_list = regex.findall(text)
print(match_list) # ['Java', 'java']
match_iter = regex.finditer(text)
for m in match_iter:
print(m.group()) # Java java
p = r'\d+'
regex = re.compile(p)
text = "AB12CD34EF"
clist = regex.split(text)
print(clist) # ['AB', 'CD', 'EF']
repace_text = regex.sub(' ', text)
print(repace_text) # AB CD EF
1.6.2 編譯標志
compile()函數編譯正則表達式對象時,還可以設置編譯標志。編譯標志可以改變正則表達式引擎行為
ASCII和Unicode
之前介紹過預定義字符類\w
和\W
,其中\w
匹配單詞字符,在Python2
中是ASCII編碼,在Python3
中則是Unicode
編碼,所以包含任何語言的單詞字符。可以通過編譯標志re.ASCII(或re.A)
設置采用ASCII
編碼,通過編譯標志re.UNICODE(re.U)
設置采用Unicode
編碼
示例如下:
text = '你好hello'
p = r'\w+'
regex = re.compile(p, re.U)
m = regex.search(text)
print(m) # <re.Match object; span=(0, 7), match='你好hello'>
regex = re.compile(p, re.A)
m = regex.search(text)
print(m) # <re.Match object; span=(2, 7), match='hello'>
re.U
可以匹配中文字符,而re.A
只能匹配ASCII編碼
忽略大小寫
默認情況下正則表達式引擎對大小寫是敏感的,但有時在匹配過程中需要忽略大小寫,可以通過編譯標志re.IGNORECASE(或re.I)
實現
p = r'(java).*(python)'
regex = re.compile(p, re.I)
m = regex.search('I like Java and Python')
print(m)
m = regex.search('I like JAVA and PYTHON')
print(m)
m = regex.search('I like java and python')
print(m)