『正則表達式』《正則指引》學習筆記

1. 字符組

1.1 普通字符組

在正則表達式中,字符組表示在同一個位置可能出現的各種字符,其寫法是在一對方括號[]之間列出所有可能出現的字符

>>> import re
>>> regex = r"[ab]"
>>> re.search(regex, "a")
<re.Match object; span=(0, 1), match='a'>
>>> re.search(regex, "a") != None
True
>>> re.search(regex, "b") != None
True
>>> re.search(regex, "c") != None
False

上面寫法太感人,字符數量一多就非常麻煩,所以正則表達式提供了範圍表示法

所謂範圍表示法,就是用[x-y]的形式表示xy整個範圍內的字符

>>> import re
>>> regex = r"[0-9]"
>>> re.search(regex, "5") != None
True
>>> re.search(regex, "a") != None
False

範圍表示法本質上是按照碼值來確定的,所以x的碼值必須小於y的碼值,如果大小顛倒,編譯器會報錯

另外,不要圖省事用[0-z],雖然這麼寫可以,但是會有多餘的字符也被匹配了

在不少語言中,還可以用\xhex來表示一個字符,它可以表示一些難輸入或者難以顯示的字符,比如所有ASCII字符對應的字符組就是[\x00-\x7F](碼值0~127)

>>> import re
>>> regex = r"[\x00-\x7f]"
>>> re.search(regex,"c") != None
True
>>> re.search(regex,"I") != None
True
>>> re.search(regex,"0") != None
True
>>> re.search(regex,"<") != None
True

1.2 元字符與轉義

上面的例子中,字符組中的橫線-並不能匹配橫線字符,而是用來表示範圍,這類字符叫做元字符(meta-character)。字符組的開方括號[、閉方括號]以及^$都算元字符。

在匹配中,它們有着特殊的意義。但是,有時候並不需要表示這些特殊意義,只需要普通字符,此時就必須做特殊處理,也就是轉義

>>> import re
>>> regex1 = r"[-09]"
>>> regex2 = r"[0-9]"
>>> regex3 = r"[-0-9]"
>>> regex4 = r"[0\-9]"
>>> re.search(regex1, "-") != None
True
>>> re.search(regex2, "-") != None
False
>>> re.search(regex3, "-") != None
True
>>> re.search(regex4, "-") != None
True
>>> re.search(regex4, "5") != None
False
  • 由於Python支持原生字符串,故直接用反斜槓就行,但是有些語言不支持原生字符串,如Java,應該寫成regex = "[0\\-9]"
  • 有些字符成對出現時纔是元字符,這種情況只要對開始的元字符轉義即可

1.3 排除型字符組

非常類似普通字符組[……],只是在開方括號後緊跟一個脫字符^,寫作[^……],表示“在當前位置,匹配一個沒有列出的字符”。

>>> import re
>>> regex = r"[^0-9][0-9]"
>>> re.search(regex, "A8") != None
True
>>> re.search(regex, "x6") != None
True
>>> re.search(regex, "88") != None
False
>>> re.search(regex, "8") != None
False

注意,^只有緊跟在[之後纔是排除型字符組的元字符

>>> regex = r"[0-9^]"
>>> re.search(regex, "^") != None
True

1.4 字符組簡記法

字符組簡記
等價字符組
含義
\d
[0-9]
數字(digit)
\w
[0-9a-zA-Z_]
單詞字符(word)
\s
[ \t\r\n\v\f](第一個字符是空格)
空白字符(space)

相對於上面三個普通字符組簡記法,正則表達式也提供了對應排除型字符組的簡記法:\D\W\S——字母一樣,只是變成大寫

所以,要表示任意字符,可以這麼寫:[\s\S][\d\D][\w\W]

1.5 匹配範圍

前面的例子沒有對字符串的匹配範圍做限定,只要子串有匹配的就可以,但有時候我們需要判斷字符串整體是否滿足一定要求,這就需要對正則表達式做一定限定,一般形如^[……]$,表示必須整體匹配

>>> import re
>>> regex1 = r"^[0-9][A-Z]$"
>>> regex2 = r"[0-9][A-Z]"
>>> re.search(regex1, "Q3Q") != None
False
>>> re.search(regex2, "Q3Q") != None
True

2. 量詞

之前匹配的都是單個字符,現在用量詞來實現多個字符的匹配

2.1 一般形式

量詞
說明
{n}
之前的元素必須出現n次
{m,n}
之前的元素至少出現m次,至多出現n次
{m,}
之前的元素至少出現m次,出現次數無上限
{0,n}
之前的元素可以不出現,也可以出現,最多出現n次
>>> import re
>>> regex = r"^\d{4,6}$"
>>> re.search(regex, "123") != None
False
>>> re.search(regex, "1234") != None
True
>>> re.search(regex, "123456") != None
True
>>> re.search(regex, "1234567") != None
False

2.2 常用量詞

常用量詞
{m,n}的等價形式
說明
*
{0,}
可以出現,也可以不出現,出現次數無上限
+
{1,}
至少出現一次,出現次數無上限
?
{0,1}
至多出現一次,也可能不出現

2.2.1 量詞?的應用

>>> import re
>>> re.search("travell?er", "traveler") != None
True
>>> re.search("travell?er", "traveller") != None
True

2.2.2 量詞+的應用

>>> import re
>>> re.search(r"<[^>]+>", "<bold>") != None
True
>>> re.search(r"<[^>]+>", "</table>") != None
True
>>> re.search(r"<[^>]+>", "<>") != None
False

2.2.3 量詞*的應用

>>> import re
>>> re.search("(dis)*like", "I like you") != None
True
>>> re.search("(dis)*like", "I dislike you") != None
True
>>> re.search("(dis)*like", "I hate you") != None
False

2.3 數據提取

re.search()如果匹配成功,返回一個MatchObject對象,可以通過MatchObjectgroup()方法返回匹配的文本。

這裏再介紹一個方法:re.findall(pattern, string),這個方法能找出給定字符串的所有匹配正則表達式的子串,注意,正則表達式不要用^$

這個方法會返回一個list,其中的元素是在string中一次尋找的pattern能匹配的文本。

>>> import re
>>> re.findall(r"\d{6}", "zipcode1: 201203, zipcode2: 100859")
['201203', '100859']
>>> # 或者也可以這樣輸出
>>> for zipcode in re.findall(r"\d{6}", "zipcode1: 201203, zipcode2: 100859"):
	print(zipcode)

	
201203
100859

2.4 點號

點號是個特殊的元字符,可以匹配換行符\n之外的所有字符

所以要匹配任意字符不能用點號,而應該用[\s\S]或[\d\D][\w\W]

2.5 匹配優先量詞和忽略優先量詞

2.5.1 匹配優先量詞

顧名思義,就是在拿不準是否要匹配的時候優先嚐試匹配,並且記下這個狀態,以備將來反悔。

例如:給定一個正則表達式regex = r"\d.*\d",簡單說就是要匹配以數字開頭結尾的字符串

>>> import re
>>> regex = r"\d.*\d"
>>> re.search(regex,"1abcd3").group()
'1abcd3'
>>> re.search(regex, "1abcd3efg5").group()
'1abcd3efg5'
>>> re.search(regex, "1abcd3efg").group()
'1abcd3'

這裏以"1abcd3efg"當爲例,匹配到 “3” 時,它既符合點號的匹配,也適合\d的匹配,因爲優先匹配,所以會把 “3” 和點號匹配,就這樣,直到點號不能匹配或字符串結束爲止,最後在進行\d的匹配,從點號不能匹配點號的位置開始匹配\d,如果不滿足則往前回溯一個字符,直到能匹配\d爲止,如果回溯到第一個都不滿足,則返回None,不存在匹配的子串。

2.5.2 忽略優先量詞

如果不確定是否需要匹配,忽略優先量詞會選擇“不匹配”的狀態,它會先嚐試匹配該部分之後的表達式是否匹配,如果不匹配,再回溯回來判斷是否匹配。

忽略優先量詞的語法很簡單,在一般的量詞之後加上“?”即可

>>> import re
>>> regex = r"\d.*?\d"
>>> re.search(regex, "1abcd3").group()
'1abcd3'
>>> re.search(regex, "1abcd3efg5").group()
'1abcd3'
>>> re.search(regex, "1abcd3efg").group()
'1abcd3'

2.6 轉義

量詞
轉義形式
*
\*
+
\+</center>
?
\?
{n}
\{n}
{m,n}
\{m,n}
{m,}
\{m,}
*?
\*\?
+?
\+\?
??</center>
\?\?
.
\.</center>

3. 括號

3.1 分組

以匹配身份證號碼爲例,按照之前說過的內容可以這樣寫:

要求
正則表達式
首位是數字,不能爲0 [1-9]
出去首位和末位,剩下13位或16位,切都是數字 \d{13,16}
末尾可能是數字,也可能是x [\dx]

所以整個表達式是[1-9]\d{13,16}[\dx]

>>> import re
>>> # 最好限定整體匹配
>>> id_card_regex = r"^[1-9]\d{13,16}[\dx]$"
>>> re.search(id_card_regex, "110101198001017032") != None
True
>>> re.search(id_card_regex, "1101018001017016") != None
True
>>> re.search(id_card_regex, "11010119800101701x") != None
True

但是,這個表達式還有一些問題它會把不是身份證號碼格式的字符串也匹配,就是16位和17位數字的字符串

>>> # 16位
>>> re.search(id_card_regex, "1101011980010171") != None
True
>>> # 17位
>>> re.search(id_card_regex, "1101011980010171x") != None
True

所以我們不能用量詞{13,16},像這種可能出現或不出現的多個字符不能簡單用量詞限定,要先把這些組合成一個組,對它們整體限定,所謂分組,簡單來說,就是把看成整體的字符組,在兩端加圓括號就行了。

>>> import re
>>> id_card_regex = r"^[1-9]\d{14}(\d{2}[\dx])?$"
>>> # 16位
>>> re.search(id_card_regex, "1101011980010171") != None
False
>>> # 17位
>>> re.search(id_card_regex, "1101011980010171x") != None
False

3.2 多選結構

多選結構的形式是(...|...),在括號內以豎線”|“分隔開多個子表達式,這些子表達式也叫多選分支。在一個多選結構內,多選分支的數目沒有限制。在匹配時,整個多選結構被視爲單個元素,只要其中某個子表達式能夠匹配,整個多選結構的匹配就成功;如果所有子表達式都不能匹配,則整個多選結構匹配失敗。

>>> import re
>>> regex = r"(ab|cd)"
>>> re.search(regex, "ac") != None
False
>>> re.search(regex, "ab") != None
True
>>> re.search(regex, "bc") != None
False
>>> re.search(regex, "cd") != None
True
  • 多選結構最好加括號,因爲豎線“|”的優先級很低,如果不加,那麼可能會出現這種情況
  >>> regex = r"^ab|cd$"
  >>> re.search(regex, "ab") != None
False
  >>> re.search(regex, "^ab") != None
True
  • 多選分支不等於字符組,雖然可以替代,但是會很麻煩;
    排除型字符組可以表示”無法由某幾個字符匹配的字符“,多選結構沒有對應的結構表示“無法由某幾個表達式匹配的字符串
  • 多選分支的排列是有講究的,一般多選分支匹配都是從左往右,所以如果出現這種情況:湖南|湖南省,你要看實際需求,哪個在前哪個在後

3.3 引用分組

括號不僅僅能把有聯繫的元素歸攏起來並分組,還有其他作用——使用括號之後,正則表達式會保存每個分組真正匹配的文本,等到匹配完成後,通過group(num)之類的方法“引用”分組在匹配時捕獲的內容。其中,num表示對應括號的編號,括號分組的編號原則是從左向右計數,從1開始,因爲捕獲了文本,所以這種功能叫做捕獲分組。對應的這種括號叫做捕獲型括號。

舉個例子,我們經常遇到諸如 2010-12-22、 2019-08-03 這類表示日期的字符串,希望從中提取出年、月、日之類的信息,就可藉助分組來實現。正則表達式中,每個捕獲分組都有一個編號,具體如下所示:

字  符  串:  2010  -  12  -  22
字  符  串:  2019  -  08  -  03
表  達  式:  (\d{4})-(\d{2})-(\d{2})
分組編號:       1         2        3

一般來說,正則表達式匹配完成之後,都會得到一個表示“匹配結果”的對象,對它調用獲取分組的方法,傳入分組編號num,就可以得到對應分組匹配的文本。re.search()返回的的是一個MatchObject對象,想要知道是否匹配,判斷它是否爲None即可,但如果想獲得匹配結果的詳細信息,可以使用MatchObject.group(num)就可以獲得編號爲num的分組匹配的文本。

>>> import re
>>> regex = r"(\d{4})-(\d{2})-(\d{2})"
>>> obj = re.search(regex, "2019-08-03")
>>> obj.group(1)
'2019'
>>> obj.group(2)
'08'
>>> obj.group(3)
'03'

注意,也有編號爲0的分組,它默認存在,表示整個表達式匹配的文本

>>> obj.group()
'2019-08-03'

有些表達式會包含嵌套括號,者不影響括號的編號,括號的編號是以左開括號爲準的,從左往右,從1開始一次編號計數

新手容易弄錯的分組的結構

>>> import re
>>> re.search(r"(\d{4})-(\d{2})-(\d{2})", "2010-12-22").group(1)
'2010'
>>> re.search(r"(\d){4}-(\d){2}-(\d){2}", "2010-12-22").group(1)
'0'

注意第二個表達式,它的編號爲1的分組爲(\d),但是因爲後面有量詞{4},所以要重複出現4次,而且編號都是1,於是每出現一次就更新一次結果,所以值依次爲2,0,1,0,所以最終一號編組值是0

正則表達式的替換

在Python語言中進行正則表達式替換的方法是re.sub(pattern, replacement, string),其中pattern是用來匹配被替換文本的表達式,replacement是要替換成的文本,string是要進行替換操作的字符串。

>>> import re
>>> re.sub(r"[a-z]", "*", "1a2b3c")
'1*2*3*'

replacement中也可以引用分組,形式是\num,其中的num是對應分組的編號,不過replacement並不是一個正則表達式,而是一個普通字符串,但是\1\2這些並不是合法轉義,故replacement也必須是原生字符串。

>>> import re
>>> regex = r"(\d{4})-(\d{2})-(\d{2})"
>>> re.sub(regex, r"\2/\3/\1", "2019-08-25")
'08/25/2019'
>>> re.sub(regex, r"\1年\2月\3日", "2019-08-25")
'2019年08月25日'

值得注意的是,如果想在replacement中引用整個表達式匹配的文本,不能使用\0,即使是原生字符串也不行,因爲\0xx表示八進制數字的轉義,會有歧義衝突,而且\0本身也表示編碼爲0的字符,如果一定要用整個表達式,可以給正則表達式整體加括號,用\1引用

3.4 反向引用

前面我們看到了引用分組,能引用某個分組內的子表達式匹配的文本,但引用都是在匹配完成後進行的,能不能在正則表達式中引用呢?

答案是可以的,這種功能被稱作反向引用,它允許在正則表達式內部引用之前的捕獲分組匹配的文本(也就是說,只要你定義好一個捕獲分組,後面可直接用編號來用),其形式也是\num,其中num表示所引用分組的編號,編號規則和之前介紹的相同。

>>> import re
>>> regex = r"([a-z])\1"
>>> re.search(regex, "ab") != None
False
>>> re.search(regex, "aa") != None
True

關於反向引用,還有一點需要強調:

        反向引用重複的是對應捕獲分組匹配的文本,而不是之前的表達式;也就是說,反向引用的是由之前表達式決定的具體文本,而不是符合某種規則的未知文本

對分組的引用可能出現在三種場合:

  • 在匹配完成後,用group(num)之類的方法提取數據
  • 在進行正則表達式替換時,用\num引用
  • 在正則表達式內部,用\num引用

不過,這是Python中的規定,具體細微差別看具體語言

反向引用的二義性

\10這樣的正則表達式,你不清楚他是想要\10還是\10,默認是查找\10,如果不存在則報錯

爲了消除二義性,Python提供了\g<num>表示法,如果不是想用第10組,那麼應該這樣寫:\g<1>0

3.5 命名分組

爲了杜絕上面的二義性,以及便於更加直觀的引用捕獲分組,而不是用不夠直觀的數字編號,一些語言提供了命名分組,可以將它看做另一種捕獲分組,但是標識是容易記憶和辨別的名字,而不是數字編號。

在Python中,使用(?P<name>...)來分組,其中name是賦予這個分組名字,P必須大寫舉個例子:

  • 字符串:2019-08-28
  • 字符串:2020-01-01
  • 表達式:(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})
  • 分組名:      year              month            day
>>> import re
>>> regex = r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})"
>>> result = re.search(regex, "2020-12-12")
>>> result.group("year")
'2020'
>>> result.group("month")
'12'
>>> result.group("day")
'12'

雖然分組有了名字,但是數字計數的名字仍然可以使用

如果使用了命名分組,在表達式中反向引用時,必須使用(?P=name)的寫法,而要進行正則表達式的替換,則需要寫作\g<name>,其中的name是分組的名字。

>>> import re
>>> re.search(r"^(?P<char>[a-z])(?P=char)$", "aa") != None
True
>>> re.sub(r"(?P<digit>\d)", r"\g<digit>0", "123")
'102030'

3.6 非捕獲分組

目前爲止,介紹了括號的三種用途:

  • 分組:將相關元素歸攏到一起,構成單個元素
  • 多選構造:規定可能出現的多個子表達式
  • 引用分組:將子表達式匹配的文本存儲起來,供之後引用

這些功能是耦合的,有時候不需要引用,括號也會進行引用分組,浪費了性能,所以正則表達式提供了非捕獲分組

它和普通的捕獲分組很類似,只是在開括號後緊跟一個問號和冒號(?:...),這樣的括號叫做非捕獲型括號。這樣引用分組會按照從左到右計數編號,不過,會略過這種非捕獲型括號

>>> import re
>>> re.search(r"(\d{4})-(\d{2})-(\d{2})", "2010-12-22").group(2)
'12'
>>> re.search(r"(?:\d{4})-(\d{2})-(\d{2})", "2010-12-22").group(2)
'22'

一般只使用多選結構的功能時,最好使用非捕獲型括號

3.7 括號的轉義

這與前面的元字符不同,與括號有關的三個元字符()|都必須轉義

4. 斷言

正則表達式中的大多數結構匹配的文本會出現在最終的匹配結果中(一般用group(0)可以得到),但是也有些結構並不真正匹配文本,而只負責判斷在某個位置左/右側的文本是否符合要求,這種結構被稱爲斷言。常見的斷言有三類:單詞邊界、行起始/結束位置、環視。

4.1 單詞邊界

符號是\b,顧名思義,它匹配的是“單詞邊界”的位置,而不是字符。也就是說,\b能匹配這樣的位置:一邊是單詞字符,另一邊不是單詞字符。

下面的表格更詳細的說明了\b的用法:

字符串
\brow\b
\brow
row\b
tomorrow
brown
row
rowdy
表達式
說明
只能是單詞row \b的右側是單詞字符,所以左側不能是單詞字符 \b的左側是單詞字符,所以右側不能是單詞字符

①單詞字符只能出現在一側,具體左邊還是右邊無所謂

②規則要求一邊是單詞字符,另一邊不是單詞字符,故另一邊可以是非單詞字符,也可以什麼都沒有

③一般而言,單詞字符\w只能匹配[0-9a-zA-Z_],所以用\b\w+\b就能準確匹配英文單詞了

>>> import re
>>> re.findall(r"\b\w+\b", "a sentence\tcontains\na lot of words")
['a', 'sentence', 'contains', 'a', 'lot', 'of', 'words']

與單詞邊界\b對應的還有非單詞邊界\B,兩者的關係類似\s\S\w\W\d\D。在同一種語言中,不論\b是如何規定的,\b能匹配的位置,\B就不能匹配;\B能匹配的位置,\b也不能匹配。

4.2 行起始/結束位置

單詞邊界匹配的是某個位置而不是文本,在正則表達式中,這類匹配位置的元素叫做錨點,它用來定位到某個位置。常用的錨點有^$,它們分別匹配字符串的開始位置和結束位置,所以可以用來判斷“整個字符串能否由表達式匹配”。

4.2.1 行終止符

在編輯本文中,敲下回車鍵就代表換行,那麼在字符串中什麼代表一行終止呢?

不同平臺下的行終止符
平臺 行終止符
UNIX/Linux \n
Windows \r\n
Mac OS \n

每一行的起始位置就是行終止符之後的那個位置,後面用*NL*表示行終止符

4.2.2 多行模式

設定多行模式最簡單的方法就是在正則表達式之前加上(?m),這裏雖然出現了括號,但因爲是專用於指定品牌模式,所以不會作爲捕獲分組。

>>> import re
>>> regex = r"(?m)^\w+"
>>> string = "first line\nsecond line\nlast line"
>>> re.findall(regex, string)
['first', 'second', 'last']

4.2.3 多行匹配

4.2.3.1 ^$

^能匹配字符串開頭,以及行終止符的下一個位置

\$能匹配行終止符之前的位置,以及字符串最後一個字符之後的位置

行終止符之前之後的位置只的是字符之間的空間位置,不是字符的索引位置

>>> import re
>>> regex1 = r"(?m)^\w+"
>>> regex2 = r"(?m)\w+$"
>>> string = "first line\nsecond line\n\rlast line\r\n"
>>> re.findall(regex1, string)
['first', 'second']
>>> re.findall(regex2, string)
['line', 'line']

這裏注意,\r\n是分開算2個行終止符,不是一個!!

4.2.3.2 \A\Z

多行模式下,用來匹配字符串的開頭和結尾的錨點,

>>> import re
>>> string = "first line\nsecond line\rlast line"
>>> regex1 = r"(?m)\A\w+"
>>> regex2 = r"(?m)\w+\Z"
>>> re.findall(regex1, string)
['first']
>>> re.findall(regex2, string)
['line']

4.3 環視

什麼是環視?簡單而言,在某個位置向左或向右看必須出現或不能出現某類字符

環視的分類
名字 記法 判斷方向 結構內表達式匹配成功的返回值
肯定順序環視 (?=...) 向右 True
否定順序環視 (?!...) 向右 False
肯定逆序環視 (?<=...) 向左 True
否定逆序環視 (?<!...) 向左 False

True表示出現要求的字符組才匹配,False表示不是給定的字符組才匹配

>>> import re
>>> regex = r"^<(?!/)[^>]+(?<!/)>$"
>>> re.search(regex, "</>") != None
False
>>> re.search(regex, "<u>") != None
True
>>> re.search(regex, "<br/>") != None
False
>>> re.search(regex, "<font color=blue>") != None
True

環視有一個很重要的用途,就是避免編寫正則表達式時“牽一髮而動全身”的尷尬——既可以集中關注某個部分,添加全局性的限制,又不會干擾其他部分的分配。

環視的另一點價值在於,提取數據時杜絕匹配錯誤,比如提取郵政編碼,手機號的子串也滿足,這時候用環視限定一下就可以杜絕這種錯誤。

4.4 環視的組合

環視因爲不改變位置,要對同一位置有多個要求時,可以連着寫幾個環視,相當於and操作,如果想做相當於or操作,則相當於表達式多選結構的語法

4.5 斷言和反向引用

斷言不匹配任何字符,只匹配位置;而反向引用只引用之前的捕獲分組匹配的文本,之前捕獲分組中錨點表示的位置信息,在反向引用時不會保留下來。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章