1. 基礎字符
1.1 元字符
元字符是一些特殊的metacharactes, 並且不匹配自己。相反,他們表示應該匹配一些與衆不同的東西,或者通過重複他們或改變他們的含義來影響正則的其他部分。元字符如下:
. ^ $ * + ? { } [ ] \ | ( )
- 在元字符"[ ]“中,可以單獨列出字符,也可以通過給出兩個字符並用”-"標記將它們分開來表示一系列字符。例如,[abc]將匹配任何字符a、b或c(匹配單個字符),這與[a-c]相同,它使用一個範圍來表示同一組字符。
- 字符類中的元字符不生效。例如,[akm’中的任意字符; '$'通常是一個元字符,但在一個字符類中它被剝奪了特殊性。
1.2 預定義字符集
\d
匹配任何十進制數字;這等價於[0-9]
\D
匹配任何非數字字符;這等價於[^0-9]
\s
匹配任何空白字符;這等價於[ \t\n\r\f\v]
\S
匹配任何非空白字符;這等價於[^ \t\n\r\f\v]
\w
匹配任何字母與數字字符;這相當於類[a-zA-Z0-9]
\W
匹配任何非字母與數字字符;這相當於類[^a-zA-Z0-9]
1.3 常用操作符
操作符 | 說明 | 實例 |
---|---|---|
. | 表示任意單個字符 | 對於大小寫字母、各種符號都能匹配 |
[ ] | 字符集,對單個字符給出取值範圍 | [abc]表示a、b、c,[a-z]表示a到z的單個字符 |
[^ ] | 非字符集,對單個字符給出排除範圍 | [^abc ]表示非a或b或c的單個字符 |
* | 前一個字符0次或無限次擴展 | abc*表示ab、abc、abcc、abccc等 |
+ | 前一個字符1次或無限次擴展 | abc+表示abc、abcc、abccc等 |
? | 前一個字符0次或1次擴展 | abc?表示ab、abc |
| | 左右表達式任意一個 | abc|def表示abc、def |
{m} | 擴展前一個字符m次 | ab{2}c表示abbc |
{m, n} | 擴展前一個字符m至n次(包 含n) | ab{1, 2}表示ab、abb |
^ | 匹配字符串開頭 | ^abc表示abc且在一個字符串的開頭 |
$ | 匹配字符串結尾 | abc$表示abc且在一個字符串的結尾 |
( ) | 分組標記,內部只能使用 | 操作符 | (abc)表示abc,(abc | def)表示abc、def |
\d | 表示十進制數字 | |
\w | 匹配任何字母與數字字符 | 相當於[a-zA-Z0-9] |
tips:
- ?字符可以看作是一個可選字符,如home-?grew,其表示home-grew或者homegrew,即當作 ? 的前一個字符是可選的的;
- 對於{m, n}來說,如果不限定m和n的數值,即{ , },則認爲下限m的值爲0,而上限n的值則爲無窮,因此:
- *也就等價於{0, }
- +也就等價於{1, }
- ?也就等價於{0, 1}
- 但在實際中,還是建議使用*、+、?的形式,這樣更短更容易閱讀
2. 使用正則表達式
有了前面的基礎字符知識,我們已經可以使用正則表達式了,在python中如何使用它們呢?re模塊提供了正則表達式引擎的接口,允許你將正則編譯爲對象,然後用它們進行匹配。
2.1 編譯正則表達式
正則表達式首先要編譯成模式對象,然後才能進行各種操作。
import re
re_pattern = re.compile('ab*')
print("re_pattern : ",re_pattern)
print("type :", type(re_pattern))
[output]
re_pattern : re.compile('ab*')
type : <class 're.Pattern'>
需要明確的是,正則是作爲字符串傳遞給re.compile()的,正則被處理爲字符串,因爲正則表達式不是核心python語言的一部分,並且沒有創建用於表達它們的特殊語法。
可以理解的是,在正則被re模塊編譯之前,它就是一個普通的字符串,這與python語言所理解的字符串並無二致,如前所述,正則表達式使用反斜槓字符 \ 來表示特殊形式或允許使用特殊字符而不調用它們的特殊含義,這就會與python字符串中的反斜槓用途發生衝突,導致反斜槓災難。
例如我們要編寫一個與 \expression相匹配的正則,那麼編譯之後得到的目標模式就應該是 \ \expression,在編譯之前,正則就是一個普通的字符串,對於每一個 \,在字符串中表示就是 \ \,也就是說,編譯前的字符串就應該是 \ \ \ \ expression,可見,對目標字符串中如果有一個反斜槓,那麼傳給編譯器的正則表達式中就會有4個反斜槓,十分繁瑣。
字符 階段 \expression 被匹配的字符串 \ \ expression 爲 re.compile()轉義的反斜槓 “\\\\expression” 爲字符串字面轉義的反斜槓 解決方案是使用python的原始字符串表示法來表示正則表達式;反斜槓不以任何特殊的方式處理前綴爲 ‘r’的字符串字面,因此 r’\n’ 是一個包含 ''和 'n’的雙字符字符串,而 '\n’是一個包含換行符的單字符字符串。正則表達式通常使用這種原始字符串表示法用python代碼編寫。
常規字符串 原始字符串 “ab*” r"ab*" “\\\\expression” r"\\expression" “\\w+\\s+\\l” r"\w+\s+\l" tips:
# 帶反斜槓的特殊字符可以用\d或者\\d來匹配 pattern = re.compile("\\d") result = pattern.findall("space hh99") print(result) [output] : ['9', '9'] # 匹配一個反斜槓 pattern = re.compile("\\\\") result = pattern.findall("n\\b\\a") print(result) [output] : ['\\', '\\']
3. 應用匹配
3.1 re庫的常用函數
函數 | 說明 |
---|---|
search() | 掃描字符串,查找此正則匹配的任何位置,返回match對象 |
match() | 從一個字符串的開始位置其匹配正則表達式,返回match對象 |
findall() | 搜索字符串,以列表類型返回全部能匹配的子串 |
split() | 將一個字符串按照正則表達式匹配結果進行分割,返回列表類型 |
finditer() | 搜索字符串,返回一個匹配結果的迭代類型,每個迭代元素是match對象 |
sub() | 在一個字符串中替換所有匹配正則表達式的子串,返回替換後的字符串 |
3.2 匹配對象
方法/屬性 | 目的 |
---|---|
group() | 返回正則匹配的字符串 |
start() | 返回匹配的開始位置 |
end() | 返回匹配的結束位置 |
span() | 返回包含匹配(start, end)位置的元組 |
group()返回正則匹配的子字符串。start()和end()返回匹配的其實和結束索引。span()在單個元組中返回開始和結束索引。由於match()方法只檢查正則是否在字符串的開頭匹配,所以start()將始終爲0。
注意:
只有match對象纔有上述屬性,也就是說,對一個模式只有使用search()、match()和finditer()後所返回的match對象纔有上述屬性,而對於使用findall()等方法,由於其返回對象是一個list,所以沒有上述屬性。
import re
result1 = pattern.match("little boy found his favorite toy")
result2 = pattern.match("littleboyfoundhisfavoritetoy")
print(result1)
print(result2)
[output]
>>> <re.Match object; span=(0, 6), match='little'>
>>> <re.Match object; span=(0, 28), match='littleboyfoundhisfavoritetoy'>
result2.group()
[output]
>>> 'littleboyfoundhisfavoritetoy'
result2.start()
[output]
>>> 0
result2.end()
[output]
>>> 28
result2.span()
[output]
>>> (0, 28)
但是,模式的search()方法會掃描字符串,因此在這種情況下匹配可能不會從0開始。
result3 = pattern.search("****little boy`s here")
result3.group()
[output]
>>> 'little'
result3.start()
[output]
>>> 4
result3.end()
[output]
>>> 10
result3.span()
[output]
>>> (4, 10)
4. 更多模式能力
4.1 分組
通常,你需要獲取更多信息,而不僅僅是正則是否匹配。正則表達式通常用於通過將正則分成幾個子組來解析字符串,這些子組匹配不同的感興趣組件。
例如,現有如下信息:
From: [email protected]
User-Agent: Thunderbird
MIME-Version: 1.0
To: editor@example
我們想要提取每一個 :後面的信息,因此可以使用分組來進行匹配,
# e.g. 4.1.1
info = "From: [email protected]\nUser-Agent: Thunderbird\nMIME-Version: 1.0\nTo: editor@example"
print(info)
[output]
>>>
From: author@example.com
User-Agent: Thunderbird
MIME-Version: 1.0
To: editor@example
pattern1 = re.compile(r"From:\s+(\w+.\w+.\w+)\s+User-Agent:\s+(\w+)\s+MIME-Version:\s+(\d+.\d+)\s+To:\s+(\w+.\w+)", re.IGNORECASE)
result = pattern1.findall(info)
print(result)
[output]
>>>
[('[email protected]', 'Thunderbird', '1.0', 'editor@example')]
result1 = pattern1.finditer(info)
for result in result1:
result.group(1)
result.group(2)
result.group(3)
result.group(4)
Out[108]: '[email protected]'
Out[108]: 'Thunderbird'
Out[108]: '1.0'
Out[108]: 'editor@example'
再上述4.1.1的例子中,我們在模式中寫了四個分組,分別匹配From、User-Agent、MIME-Version和To後面的內容,這四個分組被自動地編號,分別爲1、2、3、4,同時被捕獲到了相應的內存中,這就引入了分組的後向引用,例如,我要進行一個雙詞的匹配,請看下例:
#e.g. 4.1.2
raw = "loving loving can hurt"
'''
在匹配之前,(\w+)這個分組可以匹配任何單詞內容,一旦當這個分組匹配完畢,在這裏,一開始就匹配了loving這個單詞,因此,就把loving捕獲,放在分組1的內存單元,再經過一個空格後,把分組1中的捕獲內容取出,與當前單詞比較,看是否匹配,如果匹配,則結束,否則,重新匹配這個分組。
'''
pattern = re.compile(r"\b(\w+)\b\s+\1")
print(pattern.findall(raw))
[output]
>>>
['loving']
4.2 忽略某個分組
有時候給正則的某個子表達式加括號並不是爲了分組,而僅僅是爲了看起來更清晰,因此我們並不需要捕獲該分組,那麼,可以使用(?:expression)來忽略該分組。
# e.g. 4.2.1
raw = "age:13, name:Tom"
pattern = re.compile(r"age.(\d+).\s*name.(\w+)")
print(pattern.findall(raw))
[out]: [('13', 'Tom')]
print(pattern.search(raw).group(1))
[out]: 13
print(pattern.search(raw).group(2))
[out]: Tom
pattern1 = re.compile(r"age.(\d+).\s*name.(?:\w+)")
print(pattern1.findall(raw))
[out]: ['13']
print(pattern1.search(raw).group(1))
[out]: 13
# 由於忽略了第二個分組,因此無法檢索該分組
print(pattern1.search(raw).group(2))
[out]: IndexError: no such group
需要注意的是,除了你無法檢索組匹配內容的事實外,非捕獲組的行爲與捕獲組完全相同;你可以在裏面放任何東西,用重複元字符重複它,比如*,燃火把它嵌入其他組(捕獲或不捕獲)。(?:…)在修改現有模式的時候特別有用,因爲你可以添加新組而不更改所有其他組的編號方式。紅色部分不懂什麼意思。值得一提的是,捕獲和非捕獲組之間的搜索沒有性能差異,兩種形式沒有哪一種更快。
4.3 命名分組
命名組的語法是python特定的擴展之一:(?P…)。name是該分組的名稱。命名組的行爲與捕獲組完全相同,並且還將名稱與組關聯。處理捕獲組的的匹配對象方法都接受 1. 按編號引用該分組;2. 按組名字符串引用該分組。
raw = "sheeran sheeran is good"
pattern = re.compile(r"(?P<name>\b\w+\b)\s+(?P=name)")
print(pattern.findall(raw))
[out]: ['sheeran']
pattern = re.compile(r"(?P<name>\b\w+\b)\s+\1")
print(pattern.findall(raw))
[out]: ['sheeran']
4.4 前向斷言
另一個零寬度斷言是前向斷言。前向斷言以正面和負面形式提供,如下所示:
-
正前向斷言 – (?=…)
如果包含的正則表達式,由
...
表示,在當前位置成功匹配,則成功,否則失敗。但是,一旦嘗試了包含的表達式,匹配的引擎就不會前進,模式其餘的部分會在斷言開始的地方嘗試。# 選出得分大於30分的球員 raw = ["James:36.33", "Bryant:33.2", "ONeal:24.33"] pattern = re.compile(r".+(?=[:][3][0-9][.][0-9]+)") for it in raw: print(pattern.findall(it)) [out]: ['James'] ['Bryant'] []
-
負前向斷言 – (?!..)
如果包含的表達式在字符串中的當前位置不匹配,則成功。
''' 現有一些文件如下: ["sample.txt", "ss.batch", "text.bat", "computer.sss", "name.is.wiki", "haha.bat.ten", "start.end.bat"] 我們需要從中選取出後綴不是bat的文件,因此可以使用負前向斷言來匹配後綴不是bat的文件 ''' raw = ["sample.txt", "ss.batch", "text.bat", "computer.sss", "name.is.wiki", "haha.bat.ten", "start.end.bat"] pattern = re.compile(r".*[.](?!bat$)[^.]*") for it in raw: print(pattern.findall(it)) [out]: ['sample.txt'] ['ss.batch'] [] ['computer.sss'] ['name.is.wiki'] ['haha.bat.ten'] []
在上述代碼中,模式是
r".*[.](?!bat$)[^.]*"
,意思是,從結尾匹配bat,如果匹配,則該模式失敗,如果結尾不匹配bat,那麼開始匹配模式[^.]*$
。也就是說,如果結尾不匹配bat,我們使用[^.]*$
就可以把不匹配的後綴部分輸出。如果我們使用的模式是r".*[.](?!bat$)
,那麼就相當於把後綴不是bat的.*[.]
輸出,即只是輸出後綴前面的部分:raw = ["sample.txt", "ss.batch", "text.bat", "computer.sss", "name.is.wiki", "haha.bat.ten"] pattern = re.compile(r".*[.](?!bat$)") for it in raw: print(pattern.findall(it)) [out]: ['sample.'] ['ss.'] [] ['computer.'] ['name.is.'] ['haha.bat.']
tips:
在平常使用中,更感覺前向斷言像是一個篩選條件,我們可以匹配一個字符或字符串,在匹配對象的後面或者前面我們設定一些條件,可以用前向斷言來實現。
5. 修改字符串
到目前爲止,我們只是針對靜態字符串執行搜索。正則表達式通常也用於以各種方式修改字符串,使用以下模式方法:
方法/屬性 | 目的 |
---|---|
split() | 將字符串拆分成一個列表,在正則匹配的任何地方將其拆分 |
sub() | 找到正則匹配的所有子字符串,並用不同的字符串替換它們 |
subn() | 與sub()相同,但返回新字符串和替換次數 |
5.1 分割字符串
模式的split()方法在正則匹配的任何地方拆分字符串,返回一個片段列表。它有兩種使用方式:
pattern.split(string [, maxsplit = 0])
re.split(pattern, string [, maxsplit = 0])
通過正則表達式的匹配拆分字符串,如果在正則中使用捕獲括號,則它們的內容也將作爲結果列表的一部分返回。如果maxsplit非零,則最多執行maxsplit次拆分。
# 分隔符是任意非字母數字字符序列
p = re.compile(r"\W+")
# 不設定maxsplit
result1 = p.split("this is a test, short and sweet, of split()")
print(result1)
[out]: ['this', 'is', 'a', 'test', 'short', 'and', 'sweet', 'of', 'split', '']
# 設定maxsplit = 3,則最多在字符串上分隔3次
result2 = p.split("this is a test, short and sweet, of split()", maxsplit = 3)
print(result2)
[out]: ['this', 'is', 'a', 'test, short and sweet, of split()']
如果我們不單單想要根據分隔符分隔出的文本,而且還需要知道分隔符是什麼,因此,如果在正則中使用捕獲括號,則分隔符的內容也會作爲列表的一部分返回。
# 在這一句中,分隔符是“, ”
re.split(r"([\W]+)", "words, words, words")
5.2 搜索和替換
另一個常見任務是找到模式的所有匹配項,並用不同的字符串替換它們。sub()方法接受一個替換值,可以是字符串或函數,也可以是要處理的字符串。
pattern.sub(replacement, string[, count = 0])
- 使用replacement來替換string中的匹配對象,如果沒有找到模式,則string將保持不變。
- 可選參數count是要替換的模式最大出現次數,count必須是非負整數。默認爲0表示替換所有。
raw = "sheeran is a good singer, and I think sheeran is awesome!"
pattern = re.compile(r"sheeran")
pattern.sub("jay", raw)
[out]:
'jay is a good singer, and I think jay is awesome!'
pattern.sub("Jay", raw, count = 1)
[out]:
'Jay is a good singer, and I think sheeran is awesome!'
# subn的用法和sub一致,但返回一個包含新字符串值和一致性的替換次數的2元組。
pattern.subn("Jay", raw)
[out]:
('Jay is a good singer, and I think Jay is awesome!', 2)
需要注意的是,如果replacement是一個字符串,則處理其中的任何反斜槓轉義。也就是說,\n
被轉換爲單個換行符,\r
被轉換爲回車符,依此類推。
還有一種語法用於引用由(?P<name>...)
語法定義的命名組。\g<name>
將使用名稱爲name
的組匹配的子字符串,\g<number>
使用相應的組號(後向引用),因此\g<2>
等同於\2
,但在諸如\g<2>0
之類的替換字符串中並不模糊。而\20
則被解釋爲對組20捕獲的內容驚醒引用,而不是對組2的引用,因此不建議使用\20
這種用法,以下幾種用法都是等效的:
player = "James{Forward} Rondo{Guard} Cousins{Center}"
p = re.compile(r"\w+{(?P<position>[^}]*)}")
# 把所有匹配正則的子字符串替換成 player(\g<position>)
p.sub(r"player(\g<position>)", player)
[out]:
'player(Forward) player(Guard) player(Center)'
p.sub(r"player(\g<1>)",player)
[out]:
'player(Forward) player(Guard) player(Center)'
p.sub(r"player(\1)", player)
[out]:
'player(Forward) player(Guard) player(Center)'
此外,replacement也可以是一個函數,它可以爲你提供更多控制。如果replacement是一個函數,則爲patter的每次非重疊出現將調用該函數。在每次調用時,函數都會傳遞一個匹配的匹配對象參數,並可以使用此信息計算所需的替換字符串並將其返回。
# 實現將句中的正數轉換爲浮點數
def transfer(match):
value = int(match.group())
after_transfer = float(value)
# 字符類型轉換
return str(after_transfer)
raw = "James scored 33 points and Davis scored 29 points"
p = re.compile(r"\d+")
'''
需要注意的時,sub(replacement, string),replacement一般都是一個字符型對象。
'''
p.sub(transfer, raw)