[Python]正則表達式-不完全使用

1. 基礎字符

1.1 元字符

元字符是一些特殊的metacharactes, 並且不匹配自己。相反,他們表示應該匹配一些與衆不同的東西,或者通過重複他們或改變他們的含義來影響正則的其他部分。元字符如下:

. ^ $ * + ? { } [ ] \ | ( )

  1. 在元字符"[ ]“中,可以單獨列出字符,也可以通過給出兩個字符並用”-"標記將它們分開來表示一系列字符。例如,[abc]將匹配任何字符a、b或c(匹配單個字符),這與[a-c]相同,它使用一個範圍來表示同一組字符。
  2. 字符類中的元字符不生效。例如,[akm]a,k,m,]將匹配'a', 'k', 'm', '’中的任意字符; '$'通常是一個元字符,但在一個字符類中它被剝奪了特殊性。

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:

  1. ?字符可以看作是一個可選字符,如home-?grew,其表示home-grew或者homegrew,即當作 ? 的前一個字符是可選的的;
  2. 對於{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 前向斷言

另一個零寬度斷言是前向斷言。前向斷言以正面和負面形式提供,如下所示:

  1. 正前向斷言 – (?=…)

    如果包含的正則表達式,由...表示,在當前位置成功匹配,則成功,否則失敗。但是,一旦嘗試了包含的表達式,匹配的引擎就不會前進,模式其餘的部分會在斷言開始的地方嘗試。

    # 選出得分大於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']
    	   []
    
  2. 負前向斷言 – (?!..)

    如果包含的表達式在字符串中的當前位置不匹配,則成功。

    '''
    現有一些文件如下:
    ["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])
  1. 使用replacement來替換string中的匹配對象,如果沒有找到模式,則string將保持不變。
  2. 可選參數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)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章