正則(高級)(轉)
深入理解正則表達式高級教程 Featured
作者: Zjmainstay本文是一篇正則表達式高級教程,主要通過對正則表達式幾個概念的介紹,深入探討正則表達式高級功能,以期達到通俗化解釋正則表達式高深概念的目的。
- 深入理解正則表達式應用
-
- 概念一:按單字符匹配
- 概念二:匹配優先和不匹配優先
- 概念三:貪婪模式與非貪婪模式
- 概念四:環視(斷言/零寬斷言)
- 概念五:平衡組
- 概念六:模式修飾符
- 七:正則三段論應用
- 正則三段論:定錨點,去噪點,取數據
-
前面已經寫過一篇文章《我眼裏的正則表達式(入門)》介紹過正則表達式的基礎和基本套路正則三段論:定錨點,去噪點,取數據了,接下來這篇文章,補充一點相對高級的概念:
1. 概念一:按單字符匹配
2. 概念二:匹配優先和不匹配優先
3. 概念三:貪婪模式與非貪婪模式
4. 概念四:環視(斷言)
5. 概念五:平衡組
6. 概念六:模式修飾符
7. 附:正則三段論應用
概念一:按單字符匹配
正則裏面的數據都是按照單個字符來進行匹配的,這個通過數值區間的例子最容易體現出來,比如,示例一:
我要匹配0-15
的數值區間,用正則來寫的話,便是[0-9]|1[0-5]
,這裏,便是把0-9這種單字符的情況,和10-15這種多字符的情況拆分開了,使用分支|
來區分開,表示要麽是0-9,要麽是10-15。
上面是兩位數值的情況,現在延伸至1-65535,我個人的處理思想是從大到小,一塊塊分解:
1. 65530-65535 ==> 6553[0-5] 末位區間0-5
2. 65500-65529 ==> 655[0-2][0-9] 第四位區間0-2,末位區間0-9
3. 65000-65499 ==> 65[0-4][0-9]{2} 第三位區間0-4,後兩位0-9
4. 60000-64999 ==> 6[0-4][0-9]{3} 第二位區間0-4,後三位0-9
5. 10000-59999 ==> [1-5][0-9]{4} 第一位區間1-5,後四位0-9
6. 1-9999 ==> [1-9][0-9]{0,3} 第一位只能是1-9,後三位可有可無
最後組合起來: (6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3})
便得到1-65535匹配正則。
根據數據處理需求,可以在正則前後加上^
和$
,以匹配整個數據串,或者前後加入\b
,把它當做單詞邊界處理。沒有限定字符的邊界往往是js正則判斷中常見的錯誤之一。
概念二:匹配優先和不匹配優先
匹配優先和不匹配優先從字面理解也是比較容易的,所謂匹配優先,就是,能匹配我就先匹配;不匹配優先就是,能不匹配我就先不匹配,這段匹配先跳過,先看看後面的匹配能不能通過。
概念三:貪婪模式與非貪婪模式
正則的貪婪模式和非貪婪模式是一個比較容易混淆的概念,不過,我們可以這麽理解,一個人很貪婪,所以他會能拿多少拿多少,換過來,那就是貪婪模式下的正則表達式,能匹配多少就匹配多少,盡可能最多。而非貪婪模式,則是能不匹配就不匹配,盡可能最少。
下面舉個例子,示例二:
需求:匹配1後面跟任意個0
源串:10001
使用貪婪模式:10* 結果:1000 和 1
使用非貪婪模式:10*? 結果:1 和 1
首先,*
是匹配0個或多個的意思。
貪婪模式下,它表示,首先匹配一個1,然後匹配1後面的0,最多可以匹配3個0,因此得到1000,然後第二次又匹配到一個1,但是後面沒有0,因此得到1;
非貪婪模式下,它表示,首先匹配一個1,然後1後面的0,能不匹配就不匹配了,所以,它每次都只是匹配了一個1。
看到這裏,也許,有些人覺得,哎呀,我懂了!
那麽,下來我們改改,看看你是不是真懂了。
示例三:
需求:匹配1後面跟任意個0,再跟一個1
源串:10001
使用貪婪模式:10*1 結果:10001
使用非貪婪模式:10*?1 結果:10001
為什麽這兩次的結果一樣了?
因為,正則表達式要判斷完這整個正則才算成功,這種情況下,
貪婪模式,首先匹配一個1,然後匹配1後面盡可能多的0,發現3個,再匹配0後面的一個1,正則表達式匹配完,完成匹配,得到10001;
非貪婪模式,首先匹配一個1,然後,0*?是非貪婪模式,它不想匹配了(非貪婪模式不匹配優先),看看後面是不是1了?然後發現哎媽呀,後面是個0啊,然後它回頭,不能再偷懶了,用0*?匹配一個0吧,然後匹配到10,又不想匹配了,看看後面有沒有1了,還是沒有,又回去用0*?匹配掉一個0,得到100,繼續偷懶,但是發現後面還不是1,然後又用0*?匹配得到1000,最後,發現真不容易啊,終於看到1了,匹配得到10001,正則表達式匹配完,完成匹配。
看到這裏,是不是懂了?
那究竟哪個模式好呢?
什麽時候使用貪婪模式,什麽時候使用非貪婪模式,哪個性能好,哪個性能不好,不能一概而論,要根據情況分析。
下面我舉個例子:
源碼:
<a href="http://www.zjmainstay.cn/my-regexp" target="_blank" title="我眼裏的正則表達式(入門)">我眼裏的正則表達式(入門)</a>
<a title="我眼裏的正則表達式(入門)" href="http://www.zjmainstay.cn/my-regexp" target="_blank">我眼裏的正則表達式(入門)</a>
正則1:<a [^>]*?href="([^"]*?)"[^>]*?>([^<]*?)</a>
(238次)
正則2:<a [^>]*?href="([^"]*)"[^>]*>([^<]*)</a>
(65次)
正則3:<a [^>]*href="([^"]*)"[^>]*>([^<]*)</a>
(136次)
附:執行次數的獲取請下載正則表達式測試工具:RegexBuddy 4.1.0-正則測試工具.rar,使用裏面的Debug功能。
正則1是通用寫法,正則2是在確定字符不會溢出的情況下消除非貪婪模式,正則3是證明並不是全部消除非貪婪模式就是最優。
因此,關於貪婪模式好還是非貪婪模式好的討論,只能說根據需求而定,不過,在平時的時候用,一般使用非貪婪模式較多,因為貪婪模式經常會由於元字符範圍限制不嚴謹
而導致匹配越界,得到非預期結果。
在確定的數據結構裏,可以嘗試使用[^>]*>
這樣的排除字符貪婪模式替換非貪婪模式,提升匹配的效率。註意,貪婪部分([^>]*
)的匹配,最好不要越過其後面的字符(>
),否則會導致貪婪模式下的回溯,如正則3,[^>]*
的匹配越過了href,一直匹配到>
為止,而這時候再匹配href,會匹配不到而導致多次回溯處理,直到回溯到href前的位置,後面才繼續了下去。
另外,需要註意一點,無論使用貪婪模式還是非貪婪模式,在不同語言需要註意回溯次數和嵌套次數的限制,比如在PHP中,pcre.backtrack_limit=100000
,pcre.recursion_limit=100000
。
概念四:環視(斷言/零寬斷言)
環視,在不同的地方又稱之為零寬斷言,簡稱斷言。
用一句通俗的話解釋:
環視,就是先從全局環顧一遍正則,(然後斷定結果,)再做進一步匹配處理。
斷言,就是先從全局環顧一遍正則,然後斷定結果,再做進一步匹配處理。
兩個雖然字面不一樣,意思卻是同一個,都是做全局觀望,再做進一步處理。
環視的作用相當於對其所在位置加了一個附加條件,只有滿足這個條件,環視子表達式才能匹配成功。
環視主要有以下4個用法: (?<=exp)
匹配前面是exp的數據 (?<!exp)
匹配前面不是exp的數據 (?=exp)
匹配後面是exp的數據 (?!exp)
匹配後面不是exp的數據
示例四: (?<=B)AAA
匹配前面是B的數據,即BAAA匹配,而CAAA不匹配 (?<!B)AAA
匹配前面不是B的數據,即CAAA匹配,而BAAA不匹配 AAA(?=B)
匹配後面是B的數據,即AAAB匹配,而AAAC不匹配 AAA(?!B)
匹配後面不是B的數據,即AAAC能匹配,而AAAB不能匹配
另外,還會看到(?!B)[A-Z]
這種寫法,其實它是[A-Z]範圍
裏,排除B
的意思,前置的(?!B)只是對後面數據的一個限定,從而達到過濾匹配的效果。
因此,環視做排除處理是比較實用的,比如,示例五:
需求:字母、數字組合,不區分大小寫,不能純數字或者純字母,6-16個字符。
通用正則:^[a-z0-9]{6,16}$ 字母數字組合,6-16個字符
排除純字母:(?!^[a-z]+$)
排除純數字:(?!^[0-9]+$)
組合起來:(?!^[a-z]+$)(?!^[0-9]+$)^[a-z0-9]{6,16}$
註意,環視部分是不占寬度的,所以有零寬斷言的叫法。
所謂不占寬度,可以分成兩部分理解:
1、環視的匹配結果不納入數據結果
2、環視它匹配過的地方,下次還能用它繼續匹配。
如果不是環視,則匹配過的地方,不能再匹配第二次了。
上面示例四體現了:環視的匹配結果不納入數據結果,它的結果:
(?<=B)AAA 源串:BAAA 結果:AAA
(?<!B)AAA 源串:CAAA 結果:AAA
AAA(?=B) 源串:AAAB 結果:AAA
AAA(?!B) 源串:AAAC 結果:AAA
而示例五體現了:環視它匹配過的地方,下次還能用它繼續匹配
因為,整個匹配過程中,正則表達式一共走了3次字符串匹配,第一次匹配不全部是字母,第二次匹配不全部是數字,第三次匹配全部是字母數字組合,6-16個字符。
擴展部分:
`[A-Z](?<=B)` [A-Z]範圍等於B
`[A-Z](?<!B)` [A-Z]範圍排除B
`(?!B)[A-Z]` [A-Z]範圍排除B
附: js不支持(?<=exp)
和 (?<!exp)
語法
概念五:平衡組
平衡組並不是所有程序語言都支持,而我本人使用的PHP語言就不支持,所以平時接觸也是比較少的。
平衡組主要用到下面四個語法:
(?‘group‘) 把捕獲的內容命名為group,並壓入堆棧(Stack)
(?‘-group‘) 從堆棧上彈出最後壓入堆棧的名為group的捕獲內容,如果堆棧本來為空,則本分組的匹配失敗
(?(group)yes|no) 如果堆棧上存在以名為group的捕獲內容的話,繼續匹配yes部分的表達式,否則繼續匹配no部分
(?!) 零寬負向先行斷言,由於沒有後綴表達式,如沒有(?!B)的B,試圖匹配總是失敗
在PHP中是支持(?(group)yes|no)語法的,這裏的group是分組編號,即子模式編號,如(A)?(?(1)yes|no) ,匹配Ayes 和 no
下面這裏引用《正則表達式30分鐘入門教程#平衡組》關於<>配對匹配的例子,展示平衡組用法,
< #最外層的左括號
[^<>]* #最外層的左括號後面的不是括號的內容
(
(
(?‘Open‘<) #碰到了左括號,在黑板上寫一個"Open"
[^<>]* #匹配左括號後面的不是括號的內容
)+
(
(?‘-Open‘>) #碰到了右括號,擦掉一個"Open"
[^<>]* #匹配右括號後面不是括號的內容
)+
)*
(?(Open)(?!)) #在遇到最外層的右括號時,判斷黑板上還有沒有沒擦掉的"Open";如果還有,則匹配失敗
> #最外層的右括號
平衡組的一個最常見的應用就是匹配HTML,下面這個例子可以匹配嵌套的<div>標簽:
<div[^>]*>[^<>]*(((?‘Open‘<div[^>]*>)[^<>]*)+((?‘-Open‘</div>)[^<>]*)+)*(?(Open)(?!))</div>
概念六:模式修飾符
模式修飾符在許多程序語言中都支持的,比如最常見的是i
,不區分大小寫,如javascript裏的/[a-z0-9]/i,表示匹配字母數字,不區分大小寫。
本人在寫php正則時常用的模式修飾符主要有i
和s
,如: $pattern = ‘#[a-z0-9]+#is‘;
模式修飾符s的作用主要是的.
能夠匹配換行,在處理換行數據時,通常會用到。
在PHP中,模式修飾符有兩種用法,一種是上面的,在分隔符後面的模式修飾符,它的作用範圍是全局;另一種是在正則表達式中間的。
例如:
正則:/((?i)[A-Z]+)c/
測試字符:abcABC
匹配:abc
說明:局部(ab)的大小寫被忽略了,(?i)的作用範圍在分組1內
如果把正則改成:/([A-Z]+)c/i
,則匹配結果將是:abcABC
示例地址:PHP正則表達式中間的模式修飾符
關於PHP模式修飾符的講解,請查看PHP手冊中的《PHP模式修飾符》。
七:正則三段論應用
《我眼裏的正則表達式(入門)》 和 本文《深入正則表達式應用》幾乎傾盡本人正則學習全部思想,但是很多讀者反饋,看暈了!看到如此點評,實屬無奈,因此,有必要追加本節,來個整體統籌運用,希望能讓大家猶如撥雲見月,洞悉其中的精義。
正則三段論:定錨點,去噪點,取數據
兩篇文章中,最重要的部分當屬正則三段論:定錨點,去噪點,取數據,它是整個正則處理過程中的靈魂,它貫穿整個正則撰寫過程。
下面舉例說明它的思想,示例六:
源數據:標題:深入正則表達式應用,作者:Zjmainstay
需求:匹配作者名字
我要從源數據取到Zjmainstay這個作者名,那麽,在這裏,作者:就是我們所說的錨點,因為在上面這段數據中它能夠唯一定位到我們的數據Zjmainstay(就在它後面),因此,我們得到
(1) 定錨點:作者:
而在這裏,我們不需要關心標題什麽的,因此,標題:深入正則表達式應用,就是我們的噪點,因此,我們得到
(2) 去噪點
最後,我們確定作者:後面就是我們的數據,這個數據可以是任意字符,因此,我們得到正則: 作者:(.*)
而噪點部分,因為不會對數據取值造成幹擾,直接去掉,不需要引入正則中。
下面再舉一個噪點幹擾的例子,示例七:
源數據: <a href="http://www.zjmainstay.cn/my-regexp" class="demo8" title="正則三段論應用舉例">正則表達式入門教程</a>
需求:提取鏈接和標題,還有a標簽的文字
看到這個源數據和需求,我們必須定位好錨點,主要有:
1. <a
//必須是a標簽
2. href=" 和 "
//href=""的內容得到鏈接
3. title=" 和 "
//title=""的內容得到標題
4. > 和 </a>
//>和</a>的內容得到標簽文字
然後,其他的都是噪點,使用.*?替代,需要提取的數據部分使用括號獲取子模式,得到分組數據,因此得到正則: <a href="(.*?)".*?title="(.*?)">(.*?)</a>
看到這裏,也許有朋友覺得,我還是不會寫,那麽,再來一個更簡單的構建方法,細化步驟,從源串逐步得到正則,示例八:
1. 直接拷貝源串,特殊字符處理轉義(本例沒特殊字符)
<a href="http://www.zjmainstay.cn/my-regexp" class="demo8" title="正則三段論應用舉例">正則表達式入門教程</a>
2. 從左到右,一段段轉化
2.1 <a href="(.*?)" class="demo8" title="正則三段論應用舉例">正則表達式入門教程</a>
2.2 <a href="(.*?)".*?title="正則三段論應用舉例">正則表達式入門教程</a>
2.3 <a href="(.*?)".*?title="(.*?)">正則表達式入門教程</a>
2.4 <a href="(.*?)".*?title="(.*?)">(.*?)</a>
3. 得到最終的正則
<a href="(.*?)".*?title="(.*?)">(.*?)</a>
至此,正則三段論的基本思想已經展示完畢,大家還有什麽不解請評論留言,本人看到會第一時間給予回復。
熟悉正則三段論處理思想,剩下的便是基本語義的熟練程度了,這個通過多用多練可以達到熟能生巧的境界。
最後留下一句至尊提醒:.
是萬能字符,大家看著用,遇到換行使用[\s\S]
替換.
即可。
正則(高級)(轉)