最近做Python課實驗發現正則表達式和它在py中的的標准庫re有很多能多琢磨一下的點,遂決定寫成一篇小記,以后想復習能再來看看。

名詞
因為不同文獻書籍對正則表達式的描述有差別,我在這里列出一下本文用到的部分名詞表述:
| 本小記中 | 其他說法 |
|---|---|
| 模式 | 表達式 / pattern |
| 子模式 | 子表達式 / 子組 / subpattern |
| 貪婪模式 | 貪心模式 / greedy mode |
| 非貪婪模式 | 非貪心模式 / 懶惰模式 / lazy mode |
| 非捕獲組 | non-capturing groups |
| 向前查找 | look-ahead |
| 向后查找 | look-behind |
| 字符組 | character class |
使用原生字符串
就我個人感覺,在Python寫正則表達式的時候最好養成使用原生字符串的習慣。
'\d{4}' # 字符串
r'\d{4}' # 原生字符串(raw)
為什么呢?在字符串中\是一個轉義符號,設想我們想用正則匹配反斜杠\:
re.findall('\\\\',string) # 前兩個\\先在語句中轉義,后兩個\\再在正則表達式中轉義為一個反斜杠
re.findall(r'\\',string) # 只需要在語句中轉義成一個反斜杠就行了
另外如果我們要匹配兩個連續相等的字符:
re.findall('(.)\1',st) # 無法匹配到兩個相同字符,因為在語句中轉義后到了正則表達式就沒有反斜杠了
re.findall('(.)\\1',st) # 這樣再轉義一次就可以了
re.findall(r'(.)\1',st) # 很明顯這樣寫更好
因此,為了防止漏寫反斜杠導致排查正則表達式時產生額外的困難,在Python里還是養成用原生字符串寫正則表達式的習慣為好。
子模式擴展語法
-
look-behind語法問題
展開閱讀
這一節主要圍繞
(?<=[pattern])和(?<![pattern])兩個子模式擴展語法展開。s = 'Dr.David Jone,Ophthalmology,x2441 \ Ms.Cindy Harriman,Registry,x6231 \ Mr.Chester Addams,Mortuary,x6231 \ Dr.Hawkeye Pierce,Surgery,x0986' pattern=re.compile(r'(?<=\s*)([A-Za-z]*)(?=,)')在這個例子中我原本是想尋找字符串中人名的姓氏的,但腦袋一熱寫了個
\s*,跑了一下當即給我返回了錯誤:re.error: look-behind requires fixed-width pattern我一會兒沒反應過來,國內搜索引擎也沒查到個大概。冷靜下來后咱注意到了 requires fixed-width pattern 這一句,意思是需要已知匹配長度的模式(表達式),再看一眼前面的look-behind,突然咱就恍然大悟了:
pattern=re.compile(r'(?<=\s)([A-Za-z]*)(?=,)')這樣寫就沒問題了,我們匹配到了所有的姓氏:
print(pattern.findall(s)) # ['Jone', 'Harriman', 'Addams', 'Pierce']所謂
look-behind其實就是(?<=[pattern])一類子模式擴展語法。-
注意分辨
(?<=[pattern])和(?=[pattern]),前者是放在待匹配正則表達式 之前的,后者是放在待匹配正則表達式 之后的。 -
這兩個子模式擴展語法的功能是 匹配[pattern]的內容,但在結果中並不會返回這個子模式。
-
我們通過表格來說明一下,功能是如果匹配到了即返回
[pattern2]匹配 的內容:正則寫法 正誤 (?<=[pattern1])[pattern2](?=[pattern3])√ (?=[pattern1])[pattern2](?<=[pattern3])× [pattern4](?<=[pattern1])[pattern2](?=[pattern3])√ [pattern4](?<=[pattern1])[pattern2](?=[pattern3])[pattern5]√ (?<=[pattern1])[pattern2]√ [pattern2](?=[pattern1])√
拿上面的模式(表達式)舉例:
(?<=\s)([A-Za-z]*)(?=,)從匹配內容上來說該模式(表達式)其實就是:
\s([A-Za-z]*),但 如果該模式(表達式)匹配到了內容,返回的 部分 是不包含
(?<=\s)和(?=,)的匹配內容的:[A-Za-z]*
咳咳,有點偏了,繼續講回來。要匹配的正則表達式在
(?<=[pattern])后面,所以匹配的時候是往后看的,所以(?<=[pattern])就叫look-behind。連起來看look-behind requires fixed-width pattern這個錯誤,意思就是
(?<=[pattern])中的待匹配子模式[pattern]的寬度一定要能確定!我們之前的寫法
(?<=[pattern]*)用了一個元字符*,這個元字符代表前面的[pattern]會重復匹配 0次或更多次 ,所以寬度是不確定的,由此導致了報錯。
(?<![pattern]*)也是look-behind子模式,所以也適用於上面的情況。-
同樣注意分辨
(?<![pattern])和(?![pattern]),前者是放在待匹配正則表達式 之前的,后者是放在待匹配正則表達式 之后的。 -
這兩個子模式擴展語法的功能是 如果沒出現[pattern]的內容就匹配,但在結果中並不會返回這個子模式。
一句話總結:綜上,在使用
(?<=[pattern]*)和(?<![pattern])時,在[pattern]里請不要使用?,*,+這些導致寬度不確定的元字符。元字符 功能 ? 匹配前面的子模式0次或1次,或者指定前面的子模式進行非貪婪匹配 * 匹配前面的子模式0次或多次 + 匹配前面的子模式1次或多次 
要好好記住哦~
-
-
非捕獲組和look-ahead,look-behind的區別
展開閱讀
在子模式擴展語法中非捕獲組(non-capturing group)寫作
(?:[pattern]),look-ahead是向前查找,look-behind是向后查找,我們列張表:中文術語 英文術語 模式 正向向后查找 positive look-behind (?<=)正向向前查找 positive look-ahead (?=)負向向后查找 negative look-behind (?<!)負向向前查找 negative look-ahead (?!)正向和負向指的分別是
出現則匹配和不出現則匹配。在上面一節里我們已經談了一下
look-ahead和look-behind,現在又出現個非捕獲組。非捕獲組
(?:[pattern])的功能是匹配[pattern],但不會記錄這個組,整個例子看看:import re s = 'Cake is better than potato' pattern = re.compile(r'(?:is\s)better(\sthan)') print(pattern.search(s).group(0)) # is better than print(pattern.search(s).group(1)) # thanMatch對象的group(num/name)方法返回的是對應組的內容,子模式序號從1開始。group(0)返回的是整個模式的匹配內容(is better than),而group(1)返回的是第1個子模式的內容(than)。這里可以發現第1個子模式對應的是
(\sthan)而不是(?:is\s),也就是說(?:is\s)這個組未被捕獲(沒有被記錄)問題來了,positive look-ahead(正向向前查找)
(?=[pattern])和 positive look-behind(正向向后查找)(?<=[pattern])是 出現[pattern]則匹配,但並不返回該子模式匹配的內容,它們和(?:[pattern])有什么區別呢?拿下面這段代碼的執行結果來列表:
import re s = 'Cake is better than potato' pattern = re.compile(r'(?:is\s)better(\sthan)') pattern2 = re.compile(r'(?<=is\s)better(\sthan)')子模式擴展語法 pattern.group(0) pattern.group(1) (?:[subpattern]) is better than 空格than (?<=[subpattern]) better than 空格than 
根據上面的結果總結一下:
-
(?<=[pattern])和(?=[pattern])是匹配到了[pattern]不會返回、亦不會記錄(捕獲)[pattern]子模式,所以在上面例子中整個模式的匹配結果中沒有is空格。 -
(?:[pattern])是匹配到了[pattern]會返回,但不會記錄(捕獲)[pattern]子模式,所以在上面例子中整個的匹配結果中有is空格。 -
(?:[pattern]),(?<=[pattern]),(?=[pattern])的共同點是 都不會記錄[pattern]子模式(子組),所以上面例子中group(1)找到的第1個組的內容是(\sthan)匹配到的空格than。
-
基本語法相關
-
非貪婪模式
展開閱讀
要實現找出字符串中人名姓氏和對應的電話分機碼,我會這樣寫:
import re s = 'Dr.David Jone,Ophthalmology,x2441 \ Ms.Cindy Harriman,Registry,x6231 \ Mr.Chester Addams,Mortuary,x6231 \ Dr.Hawkeye Pierce,Surgery,x0986' pattern = re.compile(r'(?<=\s)([A-Za-z]*)(?=,).*?(?<=x)(\d{4})') print(pattern.findall(s)) # [('Jone', '2441'), ('Harriman', '6231'), ('Addams', '6231'), ('Pierce', '0986')]主要思路是前面的模式根據空格和逗號先匹配到姓,后面的模式通過x開頭和
\d{4}匹配到四位電話分機碼。前面和后面的模式之間我最開始寫的是
.*,*元字符會將.的匹配重復0次或多次,然后我們就得到了這樣的匹配結果:[('Jone', '0986')](直接一步到位了喂!(#`O′)元字符表我好歹還是看了幾次的,能制止這種貪婪匹配的符號就是
?了,但因為我記得?非貪婪的表現是匹配盡可能短的字符串,再想了一下*元字符重復匹配次數最少不是0次嘛!那這問號可不能加在.*后面了!然后我就試了下面幾種:
(?<=\s)([A-Za-z]*)(?=,).*(?<=x)(\d{4})? (?<=\s)([A-Za-z]*)(?=,).*(?<=x)?(\d{4})? (?<=\s)([A-Za-z]*)(?=,).*(?<=x)?(\d{4}) (?<=\s)([A-Za-z]*)(?=,).*(?<=x)(\d{4})\s (?<=\s)([A-Za-z]*)(?=,).*(?<=x)(\d{4})?\s當然這些模式匹配的結果都沒能如我願,實在忍不住了,我還是把中間部分改成了
.*?,然后就成了!
(?<=\s)([A-Za-z]*)(?=,).*?(?<=x)(\d{4})想了一下,原來所謂的 匹配盡可能短的字符串 並不是從元字符的功能角度上去說的。
就
2between1and3這個字符串來說:-
如果我單獨寫一個
.*?進行匹配,就會匹配個寂寞, -
但如果我在兩邊加上限定:
\d+.*?\d+(.*?匹配的內容必須在數字包夾之中), -
若為
.*貪婪模式,匹配結果會是between1and,但正因為是.*?非貪婪模式,匹配的是 結果字符串寬度更小 的部分between。
綜上,非貪婪指的是在 符合當前模式的情況下 使得最終匹配結果 盡可能地短。
在使用非貪婪模式
?符號時要考慮 語境 ,結合上下文去設計功能。 -
-
中括號中的元字符
展開閱讀
寫這一節是因為Python課老師說中括號[]里的元字符都只是被當作普通字符來看待了,然鵝,在做實驗的時候我發現並不是這樣。(・ε・`)
看看這個匹配單個Python標識符的正則表達式:
^\D[\w]* # Python標識符開頭不能是數字這個模式能順利匹配
hello_world2,_hey_there這一類字符串。等等,這樣的話不就代表\w這種元字符可以在[]中用了嘛!我們再試試這些:
^\D[z\wza]* # 仍然可以匹配標識符,\w真的起了作用 ^\D[z\dza]* # 可以匹配 hz2333a,\d也起了作用 ^\D[z\nza]* # 可以匹配到帶換行符的 hz\naaa,\n也起了作用很容易能發現
\w,\s,\n,\v,\t,\r一類元字符其實都是可以在中括號[]中正常發揮 元字符的作用 的,其他還有\b等元字符。在中括號中使用他們無非是 有沒有意義 的問題,Python並不會報錯。
那么再試試這些吧:
^\D[\w+]* # 能匹配到 hello+world ^\D[\w+*]* # 能匹配到 hello+world*2 ^\D[\w+*?]* # 能匹配到 hello+wo?rld*2 ^\D[(\w+*)]* # 能匹配到 hello+(world)*2 ^\D[(\w{1,3}+*)]* # 能匹配到 hello+(world)*2,{1,3} ^\D[\w$]* # 能匹配到 hello$world ^\D[\(\w\*\?\\)\$]* # 能匹配到hello$wor\ld*?到了這里,我發現老師說的在
[]中被當作普通字符的元字符只是一部分罷了,主要是*,?,+,{},(),$這些元字符。從上面的例子可以看出來,中括號里這些元字符相當於:
\*,\?,\+,\{\},\(\),\$適用於中括號
[]的元字符主要有兩個:^逆向符,-范圍指定符,比如:[^a-z]匹配的就是a-z小寫字母集之外的隨意一個字符。
總結一下:
-
\w,\s,\n,\v,\t,\r,... 一類元字符與其相反意義(例如\w對\W)的元字符是完全可以使用在[]中的,無非是有沒有意義的問題。 -
*,?,+,{},(),$,... 一類其他符號元字符也可以使用在[]中,全被當作 普通字符 對待。 -
中括號里用上述的元字符Python都不會報錯,請放心~₍₍٩( ᐛ )۶₎₎
-
-
子模式引用方法\num
展開閱讀
教材上列子模式功能時提了一下
\num這個用法,但真的只是提了一下:此處的num是指一個表示子模式序號的正整數。例如,"(.)\1"匹配兩個連續的相同字符

剛開始我是真沒懂這是啥意思,以為是重復引用前面的子模式:
(\d)[A-Za-z_]+\1我試過用這個模式去對
12hello3這個字符串進行匹配,然后返回了個寂寞...什么gui,這里的
\1難道不是重復(\d)再匹配個數字嗎?隨后我改了一下待匹配字符串,就有結果了:
待匹配Str 匹配結果 12hello3 None 12hello1 12hello1 12hello2 2hello2 好家伙,原來
\num引用的 不是子模式本身,而是 已知子模式的匹配結果上面的例子中
(\d)是第1個子模式,匹配結果如果是 2,那么后面\1的地方也一定要是 2 才會進行匹配,我們再來幾個例子:(\d)(\d)[A-Za-z_]+\2\1 # 能匹配到 34hello43 (\d)(\d)[A-Za-z_]+\1world\2 # 能匹配到 34hello3world4 (\d)(\d)[A-Za-z_]+\1*world\2 # 能匹配到 34hello33333world4簡單總結:
-
\num引用的是對應的子模式匹配的結果,注意這里只能是子模式的序號。 -
子模式的序號 從1開始。
-
如果你需要引用已命名子模式的匹配結果,可以用子模式擴展語法
(?<子模式名>)和(?=子模式名),例如:import re s = '34hello33333world4' pattern = re.compile(r'(?P<f>\d)(\d)[A-Za-z_]+(?P=f)*world\2') print(pattern.match(s).group(0)) # 能匹配到 34hello33333world4值得注意的是
(?=子模式名)匹配到的內容和(?<子模式名>)匹配到的內容是一致的。s = '34hello33533world4' pattern = re.compile(r'(?P<f>\d)(\d)[A-Za-z_]+(?P=f)*world\2') pattern.match(s) # 返回None,因為(?P=f)*匹配的是和(?P<f>\d)所匹配的一樣的字符串。(?P<f>\d)匹配到的是3,而(?P=f)*尋找的部分33533中多出來了一個5,由此不滿足匹配要求。 -
在中括號
[]中\num是沒有效果的(和上一節來一波聯動)。 -
為什么寫了
\num卻沒有效果,考慮一下是不是沒用原生字符串的問題。
-
re模塊修飾符
-
如何同時使用多個flags
展開閱讀
像
re.compile,re.search,re.match,re.findall這幾個函數都允許修飾符flags作為參數,我們拿re.compile舉例:import re s='''Hello line1 hello line2 hello line3 ''' pattern=re.compile('^hElLo',re.I) print(pattern.findall(s))這不得勁啊!我想進行
多行匹配又想保證忽略大小寫怎么辦?( ̄▽ ̄)"彳亍,那就這樣寫!
pattern=re.compile('^hElLo',re.I | re.M)這里的
|可以稱作一個管道符(似乎是Shell里的叫法)。名字啥的倒無所謂了,使用了這個符號我們就能使用多個標志啦!(雖然通常情況下不會使用超過兩個)我口味刁鑽,我偏不用
|符,哼!(¬︿¬)好啊,沒問題啊!那我們先去子模式買點擴展語法!

在Python里還有個子模式擴展語法可以給整個模塊應用多個修飾符(flags),它就是
(?修飾符們):pattern=re.compile('(?im)^hElLo') # i->忽略大小寫,m->多行匹配 pattern=re.compile('(?sm)^hElLo') # s->換行符識別,m->多行匹配值得注意的是這個子模式擴展語法請最好放在 整個模式的最前面,不然Python會報“不建議”警告:
DeprecationWarning: Flags not at the start of the expression. -
常用的幾個修飾符
展開閱讀
修飾符 功能 re.S 讓元字符 .支持換行符\nre.M 對多行進行匹配,對元字符 ^和$有影響re.I 匹配時忽略大小寫 re.X 允許模式中有空格和多行,方便閱讀 注:Python3里面沒有re.U。
在舉例之前先來個記憶方法:
-
re.S和元字符.有關,可以背.S,擴寫成單詞背成DOT SEARCH,代表這個匹配和點元字符有關。 -
re.I是忽略大小寫,直接字面意思背成IGNORE CASE即可。 -
re.M是多行匹配,也可以直接字面意思背成MULTILINE。 -
re.X嘛...想不到了,就死背吧(ノへ ̄、)
先從
re.I開始,這一個其實就是讓模式忽略大小寫去進行匹配:import re s='''Hello line1 hello line2 hello line3 ''' pattern=re.compile('hElLo') print(pattern.findall(s)) # [] pattern2=re.compile('hElLo',re.I) print(pattern2.findall(s)) # ['Hello', 'hello', 'hello']
re.M的話主要影響了兩個元字符的匹配:^開頭匹配和$尾部匹配普通情況下,
^匹配整個字符串的開頭,而$匹配的是 單行字符串的末尾 或者 多行字符串中最后一行的結尾。但使用了
re.M后,對於多行字符串來說,^不僅匹配了字符串的開頭,還 匹配了每一行的開頭;而$也匹配了 每一行的結尾和字符串的結尾,接下來舉幾個例子:
import re s='''Hello line1 hello line2 hello line3 ''' print( re.findall('^hElLo\slINe\d',s,re.I) ) # ['Hello line1'] print( re.findall('hElLo\slINe\d$',s,re.I) ) # ['hello line3'] print( re.findall('^hElLo\slINe\d$',s,re.I) ) # [] print( re.findall('^hElLo\slINe\d',s,re.I | re.M) ) # ['Hello line1', 'hello line2', 'hello line3'] print( re.findall('hElLo\slINe\d$',s,re.I | re.M) ) # ['Hello line1', 'hello line2', 'hello line3'] print( re.findall('^hElLo\slINe\d$',s,re.I | re.M) ) # ['Hello line1', 'hello line2', 'hello line3']
默認情況下元字符
.只能匹配除換行符\n以外的任意字符。而
re.S讓元字符.能匹配包括換行符\n在內的 所有字符!例子:
import re s='''Hello line1 hello line2 hello line3 ''' print( re.findall('line(.*)hello',s) ) # [] print( re.findall('line(.*)hello',s,re.S) ) # ['1\nhello line2\n'] print( re.findall('line(.*?)hello',s,re.S) ) # ['1\n', '2\n']
re.X是一個能增加正則表達式可讀性的修飾符,讓寫正則變得更優雅~ ヽ(✿゚▽゚)ノ
我們先直接上例子:
import re s = 'Dr.David Jone,Ophthalmology,x2441 \ Ms.Cindy Harriman,Registry,x6231 \ Mr.Chester Addams,Mortuary,x6231 \ Dr.Hawkeye Pierce,Surgery,x0986' pattern = re.compile(r'(?<=\s)([A-Za-z]*)(?=,).*?(?<=x)(\d{4})') print(pattern.findall(s))正則越復雜,在單行里的可讀性就越差,這不彳亍,我們要優雅!( ̄_, ̄ ),於是可以這樣寫:
pattern = re.compile(r''' (?<=\s) # 根據空格匹配姓氏大概位置 ([A-Za-z]*) # 姓氏是由英文字母組成的 (?=,) # 姓氏后面有個逗號 .*? # 匹配姓氏和電話分機號之間的內容 (?<=x) # 找到電話分機號共同前綴x (\d{4}) # 電話分機號一律是4位 ''', re.X)就差一個紅酒杯🍷了有木有,優雅多了!可讀性大幅增加o(*≧▽≦)ツ
由上面的例子可以看出,
re.X忽略了多行模式中的空白、換行和#等字符。這里放一段官方文檔對於
re.X的描述:Whitespace within the pattern is ignored, except when in a character class, or when preceded by an unescaped backslash, or within tokens like *?, (?: or (?P<...>. When a line contains a # that is not in a character class and is not preceded by an unescaped backslash, all characters from the leftmost such # through the end of the line are ignored.
也就是說空格的忽略也有例外:
-
當空格在字符組(character class),也就是中括號
[]里的時候,不會被忽略。import re s = '''Dr.David Jone,Ophthalmology,x2441 Ms.Cindy Harriman,Registry,x6231 Mr.Chester Addams,Mortuary,x6231 Dr.Hawkeye Pierce,Surgery,x0986''' # 我們用 不會忽略中括號內的空格 這個特性來匹配上面字符串中的人名,如Dr.David Jone print(re.findall(r''' ^[a-zA-Z.]*? [\w]* # 中括號里沒有空格 (?=,) ''', s, re.X | re.M)) # 一個都匹配不上 print(re.findall(r''' ^[a-zA-Z.]*? [ \w]* # 中括號里有空格 (?=,) ''', s, re.X | re.M)) # 能夠匹配上:['Dr.David Jone', 'Ms.Cindy Harriman', 'Mr.Chester Addams', 'Dr.Hawkeye Pierce'] -
當模式中的空格前面有轉義斜杠
\,這個空格不會被忽略。import re s = '''Dr.David Jone,Ophthalmology,x2441 Ms.Cindy Harriman,Registry,x6231 Mr.Chester Addams,Mortuary,x6231 Dr.Hawkeye Pierce,Surgery,x0986''' # 我們用 不會忽略中括號內的空格 這個特性來匹配上面字符串中的人名,如Dr.David Jone print(re.findall(r''' ^[a-zA-Z.]*? # 這兒只有個普通的空格 [\w]* (?=,) ''', s, re.X | re.M)) # 一個都匹配不上 print(re.findall(r''' ^[a-zA-Z.]*? \ # 這兒有個被轉義的空格 [\w]* (?=,) ''', s, re.X | re.M)) # 匹配上了:['Dr.David Jone', 'Ms.Cindy Harriman', 'Mr.Chester Addams', 'Dr.Hawkeye Pierce'] -
當空格在
*?,(?:,(?P<...>這種語法里時,不會被忽略。經過測試,我覺得這一條和上一條轉義不會被忽略其實是一個道理(官方文檔也沒寫的很詳細)。測試中,這樣寫不會被忽略:\ *? (?:\ ) (?P<...>\ )很明顯能發現實際上還是 空格轉義,當然也有可能是我理解錯了。
不管怎樣,這樣匹配空格的方法在實際操作中肯定是 非常少用 的,別人讀這樣的正則表達式時一眼望去還真難發現哪個角落有沒有個空格 (#`O′)
對於
#注釋符而言情況就要簡單多了,在模式中只有兩種情況#不會被忽略:-
#存在於字符組(character class),也就是中括號[]里的時候。 -
#被反斜杠\轉義。
-
After All
正則表達式並不是什么時候都用得上的,尤其是很多時候正則的效率相對於字符串處理還真不算好。
但是,在字符串處理寫起來非常繁瑣的情況下,正則的確也幫我們節省了不少時間,提升了工作效率。
在咱看來,正則表達式和SQL語句有一個共性,就是其在其所處的體系中是通式般的存在:正則表達式幾乎可以在所有編程語言中進行使用,而SQL語句也可以在標准化關系數據庫管理系統中進行使用。

本人文筆不佳,寫的可能有點粗糙。希望這篇小記對大家掌握正則表達式有一定的幫助,感謝各位的耐心閱讀。( ゚∀゚) ノ♡
如果后面我在學習Python正則表達式的時候有了新的可記錄的點,我會繼續更新在這篇文章中。
To be continued...
本文在Github撰寫:https://github.com/cat-note/bottleofcat/blob/main/Python/TipsOfRegex.md
