前言
正則表達式使用單個字符串來描述、匹配一系列匹配某個句法規則的字符串。在很多文本編輯器里,正則表達式通常被用來檢索、替換那些匹配某個模式的文本。簡單說就是一個特殊的字符串去匹配一個字符串。定義了一些規則,用於匹配滿足這些規則的字符串。
對於正則表達式應該很多人第一感覺就是很難,完全沒有規律,看不懂。
我覺得可能有以下幾個原因:
1、讀不懂。
各種不同的字符組合一起,難以理解。確實,對於熟悉正則表達式的人來說,一個稍微長點的正則表達式想要理解起來,可能也要花費一定的功夫。可讀性差是正則表達式的一個很大的缺點。
如:[\w!#$%&'*+/=?^_`{|}~-]+(?:\.[\w!#$%&'*+/=?^_`{|}~-]+)*@(?:[\w](?:[\w-]*[\w])?\.)+[\w](?:[\w-]*[\w])?
2、寫不出來
各種標點符合,完全不知道什么意思,沒法寫。
3、很多工具,很多編程語言都有正則表達式,而往往這些正則表達式存在細微的差別。
4、寫出來的正則表達式有問題,匹配了非期望的字符串,甚至把期望的字符串給遺漏了。
另外通常情況下正則的速度確實相對慢一些。但是很多情況下正則表達式太慢是由於寫的正則表達式有問題,沒有優化。我並不推薦大量使用正則表達式,有些情況下又非用不可。(想檢測一個字符串中是否含有the, \bthe\b)
關於匹配
本教程所說的匹配成功,並不是指正則表達式完全匹配目標字符串,指正則表達式能匹配目標字符串的部分。無特殊說明,僅匹配一次
推薦的書籍和網站:
精通正則表達式(第三版)
http://rubular.com/ 測試正則表達式
基本概念
如果想找出當前目錄所有的.txt文件。用過windows或者linux命令行的應該知道使用“dir *.txt 、ls*.txt”去查找。為什么這個字符串能找到所有的txt文件呢?因為*這個字符有特殊的含義,表示任意文本。那么.txt有什么特殊含義呢?沒有什么含義,就是表示他們自身。所有*.txt表示任意文本開通,但是以.txt結尾就行。
正則表達式有兩種字符組成(是不是很簡單),如上面例子中的*,屬於特殊字符,也稱之為元字符,表示的不是字面上的含義;還有一種就是上面說的.txt,表示普通文本字符。
正則表達式之所以強大,靠的就是元字符提供了強大的描述能力。下面介紹一些常用的元字符。
1、行的起始^和結束$(字符串的起始和結束)
^表示一行的起始,$表示一行的結束。這是最簡單的兩個元字符了。
例如cat可以匹配所有cat單詞,以及包含cat字符的單詞。^cat只能找到行首的cat,同樣cat$表示行尾的cat。簡單來說^和$匹配的是一個位置,並不匹配任何字符。元字符不僅可以匹配字符,還可以匹配特定位置。
關於正則表達式的理解
1、^cat匹配以cat開頭的行
2、^cat匹配的是以c作為一行的第一個字符,緊接着一個a,接着一個t的文本
這兩種結果並沒有什么差異,但是第二種更符合正則表達式的邏輯,對后面分析會有幫助。
^cat$ 匹配的是行開頭,然后是一個c,接着一個a,接着一個t,然后是行尾。
^$ 行開頭,然后行結尾(空行)
^ 行開頭 (沒有特殊意義,每行都有開頭,當然可以用來統計行數)
注意,對於一個字符串來說,如java或者python中^和$表示字符串的起始和結束,如^cat$不能匹配"cat\ncat"
2、字符組[]
比如我們搜索單詞grey(灰色),但是也有可能寫作gray。我們怎么用正則表達式同時能匹配上grey和gray呢?
這時可以考慮使用字符組[],用中括號將某處期望出現的字符括起來。[ea]表示匹配e或者a,gr[ea]p的意思是:先找到g,跟着一個r,然后是一個a或者e,最后是一個y。
可以看出在字符組外,普通字符都是(接下來是)的意思,首先匹配g,接下來是r等等。但是在字符組內是完全不一樣的,表示的意思是或,就是指必須匹配字符組中的一個字符,僅只能匹配其中一個。如這個表達式並不能匹配greay
<H[123456]>可以用來匹配<H1>、<H2>等一直到<H6>,注意<>並不是元字符。
字符組元字符
連字符-
上面的<H[123456]>可以寫成<H[1-6]>,是等效的。-連字符表示一個范圍,表示字符組內的字符是1到6,包括1和6(和通常編程里面的范圍不太一樣)。就是指這個字符在字符組內才是元字符,否則就是普通字符或者其他意義的元字符。同樣的,反過來,字符組外的元字符在字符組內就不是元字符了,或者是不同意義的元字符。
[0-9A-Za-z] 支持多重范圍,表示可以匹配0-9,A-Z,a-z中的任意一個字符
[0-9a-zA-Z] 順序也可以顛倒(0-9不能顛倒)
[0-9a-zA-Z_!.?] 可以和普通文本結合起來。
注意:-連號只有出現在字符組中字符中間才算連字符,出現在開頭就不算了。(出現在結尾呢?當然也不是字符組元字符了)
排除型字符 [^...]
[^...],字符組中的^如果出現在第一個位置,也是一個字符組元字符。匹配的是所有沒有被字符組列出來的任何字符。簡單說就是表示排除的意思,不希望匹配的字符。
例如:例如英文單詞中,q后面通常跟的是字母是u,很奇怪,我想找出q后面不是u的單詞怎么辦? q[^u].表示的就是匹配的是一個,q然后一個非u的字符。我們發現q和Iraq沒有被匹配上。
注意:匹配一個沒有列出的字符,而不是不要匹配列出的字符。看起來差不多,但是有些細微差別。重點在於q后面需要匹配一個字符,排除型字符組也需要匹配一個字符。
3、用點號匹配任意字符
例如:對於這三個日期,2017-07-12、2017.07.12、2017/07/12,利用上面學過的元字符,可以表示為:2017[-./]07[-./]12。注意:雖然可以匹配上面的三個字符串,但也會匹配一些非法的日期如2017/07.12等。
注意:編寫正則表達式一定要和實際應用環境結合起來。比如只是想相對精確的找出2017-07-12這個日期,那么用這個表達式就可以了。如果你的應用中只接受上面三種格式,其他格式都是錯誤的,你需要精確的匹配日期。那么上面的正則表達式就不能滿足要求了。第一種,只是相對精確的匹配目標字符串,只要目標字符串使我們想要的,那么一定匹配到,可以容許少量的非法字符串。第二種就是精確匹配期望的字符串,不要匹配不期望的字符串。理想情況下正則表達式都是第二種,但是這種正則表達式會更難寫一點,有時候也不一定能寫出來。需要在復雜性和完整性之前取得平衡。
再稍微不精確一點,可以使用2017.07.12來進行匹配。這里的.點號就不是字面上的意思了,它是一個元字符,表示匹配任意單個字符(通常不匹配換行符\n)
4、多選結構 |
多選結構是用來匹配任意子表達式,用這個符號“|”表示,是或的意思。它可以把不同的子表達式組合成一個總的表達式,而這個總的表達式可以匹配任意的子表達式。如上面的日期的例子:如果想精確匹配,正則表達式可以這么寫2017-07-12|2017\.07\.12|2017/07/12。它是把三個子表達式組合起來了,可以匹配任意一個表達式。注意.之前有反斜杠,因為.號是元字符,反斜杠可以把元字符變成原始的含義。
這個和字符組是類似的,都是表示或的意思,字符組只能選擇單個字符,多選結構可以選擇一個字符串。gr[ea]y也可以用多選結構來寫,grey|gray。還可以簡寫成gr(e|a)y。可以用括號來限定多選的范圍,括號也是元字符。
注意:多選結構與其他元字符一起使用,一定要注意他的使用范圍。必要時用括號括起來。
^From|Subject|Date:,我們可能期望的是行起始跟着From或者Subject或者Date,然后后面接着一個:。^(From|Subject|Date):才能完全匹配我們的期望字符串。
5、單詞分界符 \b
b本身不是元字符,但是通過反斜杠轉義后變成了元字符。表示的是一個單詞的分界符。它匹配的也是一個位置。\bcat\b就能匹配It is a cat.中cat單詞了,cats之列的就匹配不上。\ba通常用來表示a的前面不是字母和數字。
6、可選項元素 ?
對於colour(顏色, 英式),同樣存在color(美式)。我們需要匹配這兩種格式。我們使用了colo(r|ur),但是我們還可以更簡單點。
colou?r可以解決這個問題。?表示的意思就是可選項,它加在一個字符后面,表示他允許出現這個字符,當然也可以不出現。簡單來說就是匹配0個或者1個字符,優先匹配1個字符。如果它不是跟着字符后面,可能表示其他含義。
它只作用於緊鄰的字符。上述正則表達式的意思是匹配一個 c,然后是o,然后是l,然后是o,然后是u?,然后是r。其中u?總是可以匹配成功的,如果出現u,就匹配u,如果沒有,就什么都不匹配。也就是說,不管u存不存在,u?都是匹配成功的。例如對於這個例子,semicolon,首先是一個c,接着一個o,接着一個l,接着一個u?,雖然單詞這里沒有u,u?也是匹配成功的。到目前為止colou?已經匹配了colo這四個字符了,接着后面跟着一個r,但是r不能匹配n,所以最終匹配失敗了。
字符串July 4th和July 4可以用這個正則July 4(th)?來匹配,括號可以限定?的作用范圍,其實括號的一個主要作用就是限制所以范圍。
7、其他量詞* + 區間{min,max}
加好+ ,星號 * 和?很類似。並不單獨使用,而是作用在其他字符后面,限定個數使用。
* 出現0次或更多次。下限0次,無上限。含義是匹配盡可能多的次數,如果實在無法匹配,那也沒有關系。
+ 出現1次或更多次。下限1次,無上限。匹配盡可能多的次數,但如果一次都匹配不了,那么久匹配失敗。
例如:a* a? a+分別匹配下面這三個字符串
aaaab:a*匹配了aaaa;a?匹配了a;a+匹配了aaaa。(a?只匹配第一個a,因為我們這里討論的是匹配一次就結束匹配。)
ab: a*匹配了a;a?匹配了a;a+匹配了a。
b:a*選擇匹配0個字符串,匹配成功了。a?同樣選擇匹配0個字符,也匹配成功了;a+只要需要匹配一個a,這里沒有a,所以匹配失敗了。
注意 :*和?一樣是永遠不會匹配失敗的,只是匹配的內容不一樣。
? * +可以統稱為量詞,他們的作用是限定所所用元素的匹配次數。量詞是貪婪的,他會匹配盡可能多的字符,直到無法匹配。
規定出現的次數的范圍:區間{min,max}
{min,max},至少出現min次,最多出現max次。
{min, } max可以省略,表示最少出現minci,無上限。
{num} 正好出現num次。
例如: a{1,5},a至少出現1次,最多出現5次。
注意:量詞和區間范圍都是匹配優先的。
8、環視(又稱 零寬斷言)
(?=...) 順序環視,表示的含義是,從左到右順序查看文本,如果能夠匹配就返回匹配成功。
如a(?=def),表示的含義,首先有一個a,接着一個d,接着一個e,接着一個f,如果成功,則返回成功。匹配adef就可能成功。
順序環視匹配的也是一個位置,並不占用字符
(?=Jeffrey) 去匹配Jeffrey Fried。匹配的就是J前面的位置
(?=Jeffrey)Jeff可以獲得更精確的結果。
同樣還有逆序環視
(?<=...),表示從右到左環視字符串。(?<=Jeffrey)匹配的是y后面的位置。
上面又稱肯定環視
將等號換成!就表示否定環視
如(?!...) 順序否定環視
(?<!...)逆序否定環視
re.split(r'(?<=a)b(?=c)', '123b321abc123') ['123b321a', 'c123']
re.split(r'(?<!a)b(?!c)', '123b321abc123') ['123', '321abc123']
注意:環視的限制,環視中可以出現什么表達式通常有限制,一般順序環視沒有限制,逆序環視往往限制匹配的長度。(?<=books?)往往是不合法的正則表達式,因為匹配文本長度是不確定的。
括號的作用
1、限制作用域
如前面限制| ? {min, max} + *等作用域
2、捕獲期望字符串。可以進行分組和反向引用。可以有多個括號,捕獲的順序是(出現的次序。括號能夠記住自己匹配上的內容,這樣可以進場分組和反向引用。
2.1、分組,大部分語言都提供分組的功能。如python中:
import re #2017.07.12 re.match(r'([0-9]{4}).([0-9]{2}).[0-9]{2}', '2017-07-12').group(1)
2.2、反向引用,反斜杠加上數字,表示當前位置匹配第幾個分組中的內容。從1開始
例如:
([a-z])([1-9])\2\1,其中\1匹配的是[a-z]匹配的內容,\2匹配的是[1-9]匹配的內容。能夠匹配a11a等對稱的字符串。
(([a-z])([1-9]))\3\2去匹配a11a,第一個括號捕獲的是a11a,第二個括號匹配的是a,第三個是1
還是之前日期的例子,正則表達式2017([-./])07(\1)12,可以精確匹配這三個'2017-07-12', '2017.07.12', '2017/07/12'中的一個,不會匹配其他字符串。注意\1加了括號,因為1后面還有數字,避免 混淆。同時加了括號,那么又會多出一個捕獲分組。
3、組合成其他元字符
例如環視
4、命名捕獲
(P?<Name>...)
python中:
print re.match(r'(?P<word>\w+)(?P=word)', 'pythonpython').group('word')
java中:
Pattern pattern = Pattern.compile("(?<word>\\w+)\\k<word>"); Matcher matcher = pattern.matcher("javajava"); if(matcher.matches()){ System.out.println(matcher.group("word")); }
注意:一個字符是否是元字符,取決於所屬環境,如在字符組內.號就不是字符組。部分元字符加上反斜杠轉義成普通字符,部分普通字符加上反斜杠變成元字符。
其他常用元字符
\t制表符,tap
\n換行符
\s 任何空白字符(包括制表符,換行,空格等)常用
\S除了\s以外的字符
\w [a-zA-Z0-9],通常用\w+匹配單詞,注意的是部分工具中\w可以匹配unicode中的“字母”如漢字,notepad++
\d [0-9]
\D 除了\d意外的字符,[^0-9]
大多數正則表達式都提供對unicode的支持。
\u4e2d匹配中文的中字。
[\u4e00-\u9fa5]可以用這個來表示中文字符。
忽略優先量詞 *? +? {m,n}?,前面提到的匹配優先量詞加上?,就變成忽略優先量詞,優先匹配下限次數。如a+?去匹配aaaab,只會匹配第一個a。
占有優先量詞 *+ ?+ ++ {m,n}+,匹配優先量詞加上+,不會交還已匹配的字符。目前主要是java.util.regex這個包提供這個功能。\d+0 匹配123450是可以匹配成功的。但是\d++0是無法匹配成功的。
匹配原理
正則匹配引擎主要分為兩類,DFA和NFA,發展的過程也產生了一些變體
DFA引擎 主要有awk MySQL
傳統型NFA引擎 主要有python java PHP Ruby sed .NET
其他引擎等
DFA不支持忽略優先量詞,基本上就是傳統型NFA。本教程只關注傳統型NFA引擎的正則表達式。
有兩條普遍的規則,適用於大多數NFA和DFA引擎。
規則一 優先選擇最左端的匹配結果
起始位置最靠左的匹配結果總是優先其他可能的結果。匹配先從需要查找的字符串的起始位置嘗試匹配。嘗試匹配的意思是,在當前位置測試整個表達式能匹配的文本。如果當前位置測試了所有可能之后不能找到匹配結果,那么就會從第二個字符之前的位置開始重新嘗試匹配。在找到匹配前會一直重復這個過程,如果嘗試了所有位置,都找不到匹配結果的情況下,才會報告匹配失敗。
例如:使用ORA來匹配FLORAL,首先第一輪從F前面開始匹配,用O去嘗試匹配,嘗試會失敗;然后從F后面L前面開始匹配,仍然用O去嘗試匹配,失敗;第三次嘗試成功。
規則二 標准量詞是匹配優先的* ? + {m,n}
標准量詞並非是所有可能中最長的,但它們匹配盡可能多的字符,直到上限。\b\w+s\b來匹配結尾是s的單詞。比如匹配regexes,\w+是可以匹配整個單詞的,但是如果\w+匹配了整個單詞,s\b就無法匹配了,所有\w+只能匹配regexe,最后的字符讓s\b去匹配
^Subject:(.*),如果^Subject:部分匹配成功了,那么整個正則表達式都會匹配成功。.*的目的是*是匹配優先的,會把剩下的所有字符都匹配上。這是我們期望的。
過度的優先匹配
^.*\d\d能夠匹配一行中最后的兩個數字。對於這個字符串,about 24 characters long,首先.*匹配了整個字符串,這時候第一個\d需要匹配,為了避免匹配失敗,.*需要交出最后一個字符g,顯然\d無法匹配g,.*繼續交出下一個字符n,這樣循環15次,最終交出4時候,\d終於可以匹配上了,但是第二個\d仍然無法匹配,所以.*必須再交出一個字符2。這樣.*匹配了“about ”,\d\d匹配了24。
NFA引擎:表達式主導
例如:to(nite|knight|night) 去匹配'...tonight...'。正則表達式第一個需要匹配的是t,它去字符串中尋找t,從第一個位置開始尋找,找到后停下,接下來是o,檢查o能否匹配下一個字符,如果能匹配,繼續檢查下面的元素。下面的元素指的是(nite|knight|night)它們是或的關系。引擎會嘗試這三種可能。在嘗試nite的過程和之前一樣,嘗試匹配n,接着是i,最后是e。
NFA具有表達式主導的特性,引擎的匹配原理就很重要,如果我們改變表達式,可以節省很多時間。
to(ni(ght|te)|knight)
tonite|toknight|tonight
to(k?night|nite)
回溯
NFA最重要的性質是,它會一次處理各個子表達式或組成的元素,遇到需要在兩個可能的成功的可能中進行選擇的時候,它會選擇其一,同時記住另一個,以備稍后可能的需要。
需要作出選擇的情形主要包括量詞和多選結構。無論選擇哪一個途徑,如果匹配成功了,其余下的也匹配成功了,那么匹配就結束了,如果余下的匹配失敗了,引擎會回溯到之前做出選擇的地方,選擇其他的備用分支繼續嘗試。這樣,引擎會嘗試表達式的所有可能途徑,直到匹配成功,或者嘗試完所有路徑並失敗。
例如:對於to(nite|knight|night)去匹配hot tonic tonight
首先用正則表達式的t,從字符串的起始位置開始匹配,首先t無法匹配h,第一輪匹配失敗,第二輪,t匹配o,同樣失敗,第三輪,t匹配成功,但是接下來o不匹配空格,導致本輪失敗。進行第四輪匹配,to匹配成功,進入3個多選分支。三個分支都有可能。假設選擇的是nite進行匹配,首先是n匹配成功,接着是i匹配成功,但是到c的時候匹配失敗。這個失敗並不會導致這一輪失敗,也就是這個位置的匹配失敗,因為還有其他分支沒有嘗試。假設接下來嘗試knight,這時候回溯到to和nic之間的位置,顯然第一次匹配,k與n匹配失敗,那么只剩下night繼續匹配了,顯然這一次也失敗了,這導致這個位置的匹配全部失敗。這樣會進入下一個位置,重復上面的操作。最終到tonight的前的位置時,選擇night這個分支匹配成功了。
回溯的兩個要點。
在匹配時,當出現多個選擇時,應該首先選擇哪個?
1、對於匹配優先量詞,會優先進行匹配。
2、對於忽略優先量詞,會選擇跳過嘗試。
3、對於多選結構,會選擇按順序進行嘗試。
當需要回溯時,應該選擇哪個備用狀態。
選擇當前最近存儲的選項。如果前面是死路,你只需要沿路返回,找到上一次做出選擇的地方。
關於備用狀態
ab?c 匹配 abc 首先a匹配a,成功,接着b?去匹配b,這時候有可能失敗,所以必須記下備用狀態ab? · c和a · bc,b?匹配b成功,接着c匹配c成功。最終匹配成功,備用狀態也丟棄了。
ab?c 匹配 ac 首先a匹配a,成功,接着b?去匹配c,這時候有可能失敗,所以必須記下備用狀態ab? · c和a · c,b?匹配c失敗,選用回溯到備用狀態,接着c匹配c成功。最終匹配成功,備用狀態也丟棄了。
ab?c 匹配 abx 首先a匹配a,成功,接着b?去匹配b,這時候有可能失敗,所以必須記下備用狀態ab?·c和a·bx,b?匹配b成功,接着c匹配x失敗,選用回溯到備用狀態,接着c匹配b失敗。但是整個匹配還沒有結束。還會繼續匹配下去。
*和+的回溯
\d+ 去匹配 a 1234 num,在用\d去匹配空格的時候,失敗了,那么它會回溯。
a 1`234 num
a 12`34 num
a 123`4 num
a 1234` num
\d+`去匹配空格,但是此時表達式已經匹配完成,並且已經匹配到數據了,那么整個匹配結束。
a `1234 num這個位置並不在備用選項中,\d*來匹配整個字符串,會有這個備用選項嗎?
一些問題
.*的問題 ".*"去匹配雙引號文本
<input name="id" value="1">,結果發現匹配的是"id" value="1"這個文本,"[^"]*"就可以了。
<B>name</B>and<B>age</B> 使用<B>.*</B>去匹配,同樣出現上面的問題,但是我們不能使用<B>[^</B>]*</B>。
<B>.*?</B> 使用忽略優先量詞
占有優先量詞,java中提供了占有優先量詞,它不會交回已匹配的字符,可以避免備用狀態。
\d++元 去匹配金額,1231231美元。
\d++\d永遠無法匹配任何字符。
多選結構的陷阱
Jan 1到 Jan 31,需要匹配合法的日期
Jan [0-3][0-9] 無法匹配Jan 1 卻能匹配Jan 00
Jan (0?[1-9]|[12][0-9]|3[01])看起來沒有任何問題。可以匹配Jan 2,Jan 01,但是匹配Jan 21的時候匹配的是Jan 2。
Jan ([12][0-9]|3[01]|0?[1-9])
正則表達式的實用技巧
理想情況中,好的正則表達式必須在下面這些方面取得平衡。
1、只匹配期望的文本,排除不期望的文本
2、易於控制和理解
3、必須保證效率(如果能夠匹配,必須很快返回結果,如果不能匹配,應該盡可能快的報告匹配失敗)
現實情況,好的正則表達式與具體問題結合。在完整性和復雜性之間取得平衡。
避免優化過度
為提高效率而改動正則表達式最需要考慮的問題是,改動是否會影響准確性。
必要時才進行優化,優化需要考慮使用的環境和語言,優化后需要測試
正則表達式的匹配過程
正則表達式編譯,檢測語法,編譯成內部形式
開始匹配,定位到字符串的起始位置
元素檢測 依次測試正則表達式的各個元素。
相連元素會依次嘗試 Subject
量詞修飾的元素,控制權會在量詞和被限定的勻速直接輪換
控制權在捕獲型括號內外切換回帶來一些開銷。
尋找匹配結果 如果遭到匹配結果,就會報告匹配成功。
繼續匹配 如果沒有找到匹配,就會從下一個字符繼續匹配。
匹配徹底失敗,所有嘗試都失敗了,就會報告匹配失敗
簡單優化措施
1、消除不必要的括號,必須使用括號時,請盡量使用非捕獲型括號
例如:.* 和 (?:.)*
2、消除不必要的字符組
例如:[.] 和 \.
3、使用.*開頭的表達式應該在最前面加^
例如:.*abc vs ^.*abc
4、在多選結構中提取開頭的必需元素
例如:th(?:is|at) 替代 (?:this|that)
5、多選結構的代價
例如:u|v|w|x|y|z VS [uvwxyz],盡量避免多選結構
6、改變多選結構的順序
例如:去匹配引號中的字符,並且能夠匹配轉義的引號。"(\\.|[^"\\])*"去匹配"2\"x3\" likeness" 。可以優化"([^"\\]|\\.)*"