深入理解正則表達式環視的概念與用法
文章大綱:
深入理解正則表達式環視的概念與用法 一、環視的概念 (一)環視概念與匹配過程示例 示例一:簡單環視匹配過程 (二)什么是消耗正則的匹配字符? 示例二:一次匹配消耗匹配字符匹配過程 示例三:多次匹配消耗匹配字符匹配過程 二、環視的類型 (一)肯定和否定 (二)順序和逆序 · 兩種類型名稱組合 · 四種組合的用法 四種組合正則與環視的擺放位置 1、肯定順序:(?=exp) (1)常規用法 示例四:肯定順序環視常規用法 (2)變種用法 示例五:肯定順序環視變種用法 2、否定順序:(?!exp) 示例六:否定順序環視 3、肯定逆序:(?<=exp) 示例七:肯定逆序環視 4、否定逆序:(?<!exp) 示例八:否逆序環視 三、環視的應用 示例九:正則分塊組合法-必須包含字母、數字、特殊字符 示例十:正則逐步完善法-排除特定標簽p/a/img,匹配html標簽 示例十一:正則減除查錯法-匹配異常原因查找 總結
在《 深入講解正則表達式高級教程-環視 》中已經對環視做了簡單的介紹,但是,可能還有一些讀者比較迷惑,今天特意以專題的形式,深入探討一下正則表達式的環視的概念與用法。
一、環視的概念
環視,在不同的地方又稱之為零寬斷言,簡稱斷言。
環視強調的是它所在的位置,前面或者后面,必須滿足環視表達式中的匹配情況,才能匹配成功。
環視可以認為是虛擬加入到它所在位置的附加判斷條件,并不消耗正則的匹配字符。
(一)環視概念與匹配過程示例
示例一:簡單環視匹配過程
例如,對于源字符串 ABC ,正則 (?=A)[A-Z] 匹配的是:
- (?=A) 所在的位置,后面是 A
- 表達式 [A-Z] 匹配 A-Z 中任意一個字母
根據兩個的先后位置關系,組合在一起,那就是:
(?=A) 所在的位置,后面是 A ,而且是 A-Z 中任意一個字母,因此,上面正則表達式匹配一個大寫字母 A 。
從例子可以看出,從左到右,正則分別匹配了環視 (?=A) 和 [A-Z] ,由于環視不消耗正則的匹配字符,因此, [A-Z] 還能對 A 進行匹配,并得到結果。
(二)什么是消耗正則的匹配字符?
在《深入講解正則表達式高級教程》里我們已經講過,正則是按照單個字符來進行匹配的,一般情況下是從左到右,逐個匹配源字符串中的內容。
示例二:一次匹配消耗匹配字符匹配過程
例如,對于源字符串 ABCAD ,正則 A[A-Z] 匹配的過程是:
- 正則 A :因為沒有位置限定,因此是從源字符串開始位置開始,也就是正則里的 ^ ,這個 ^ 是虛擬字符,表示匹配字符串開始位置,也就是源字符串 ABCAD 里的 A 前面的位置,因為正則 A 能夠匹配源字符串 A ,匹配成功,匹配位置從源字符串 ^ 的位置后移一位,到達 A 后面,即此時源字符串 ABCAD 的 A 這個字符已經被消耗,接下來的正則匹配從 A 后面開始。
- 正則 [A-Z] :當前匹配位置為第一個 A 字母后面位置,正則 [A-Z] 對源字符串 ABCAD 里的 B 字母進行匹配,匹配成功,位置后移到 B 字母后面的位置。至此,由于正則已經匹配完成,因此,正則 A[A-Z] 匹配結果是 AB 。
我們知道,有些語言如js支持 g 模式修飾符,也就是全局匹配,那么,上面例子中,正則匹配1次成功之后,將會從匹配成功位置( B 字母后面位置)開始,再從頭進行匹配一次正則,直到源字符串全部消耗完為止。
示例三:多次匹配消耗匹配字符匹配過程
因此,全局匹配的過程補充如下:
- 正則 A :當前匹配位置為 B 字母后面位置,正則 A 去匹配源字符串中的 C ,匹配失敗,匹配位置后移一位,此時 C 被消耗了。
- 正則 A :當前匹配位置為 C 字母后面位置,正則 A 去匹配源字符串中的第二個 A 字母,匹配成功,匹配位置后移一位,此時 A 被消耗了。
- 正則 [A-Z] :當前匹配位置為第二個 A 字母后面位置,正則 [A-Z] 對源字符串 ABCAD 里的 D 字母進行匹配,匹配成功,位置后移到 D 字母后面的位置,此時 D 被消耗了。
- 由于正則里還有個源字符串結束位置,也就是正則里的 $ ,這個 $ 也是虛擬字符,因此,還要繼續進行匹配:
正則 A :當前匹配位置為 D 字母后面的位置,正則 A 去匹配源字符串的結束位置,匹配失敗,匹配結束。
最終匹配結果是 AB 和 AD 。
二、環視的類型
環視的類型有兩類:
(一)肯定和否定
1、肯定: (?=exp) 和 (?<=exp)
2、否定: (?!exp) 和 (?<!exp)
(二)順序和逆序
1、順序: (?=exp) 和 (?!exp)
2、逆序: (?<=exp) 和 (?<!exp)
· 兩種類型名稱組合
1、肯定順序: (?=exp)
2、否定順序: (?!exp)
3、肯定逆序: (?<=exp)
4、否定逆序: (?<!exp)
· 四種組合的用法
四種組合,根據正則與環視位置的不同,又可以組合出來8種不同的擺放方式。
一般來說,順序的環視,放在正則后面,認為是常規用法,而放在正則前面,對正則本身的匹配起到了限制,則認為是變種的用法。
而逆序的環視,常規用法是環視放在正則前面,變種用法是放在正則后面。
總結一句話就是:常規用法,環視不對正則本身做限制。
但是,無論常規和變種,都是非常常見的用法。
四種組合正則與環視的擺放位置
1、肯定順序常規: [a-z]+(?=;) 字母序列后面跟著; 2、肯定順序變種: (?=[a-z]+$).+$ 字母序列 3、肯定逆序常規: (?<=:)[0-9]+ :后面的數字 4、肯定逆序變種: \b[0-9]\b(?<=[13579]) 0-9中的奇數 5、否定順序常規: [a-z]+\b(?!;) 不以;結尾的字母序列 6、否定順序變種: (?!.*?[lo0])\b[a-z0-9]+\b 不包含l/o/0的字母數字系列 7、否定逆序常規: (?<!age)=([0-9]+) 參數名不為age的數據 8、否定逆序變種: \b[a-z]+(?<!z)\b 不以z結尾的單詞
下面示例,僅對肯定順序環視進行兩種用法的講解,其他組合都有類似用法,讀者參考上面列舉8種位置用法自行測試。
1、肯定順序: (?=exp)
(1)常規用法
所謂常規用法,主要指正則匹配部分位于肯定順序環視左側,如: test(?=\.php) ,用于匹配后綴是 .php 的test文件。
示例四:肯定順序環視常規用法
源字符串:
notexefile1.txt exefile1.exe exefile2.exe exefile3.exe notexefile2.php notexefile3.sh
需求:獲取 .exe 后綴文件不含后綴的文件名
正則:
.+(?=\.exe)
結果:
exefile1 exefile2 exefile3
示例中,因為要獲取 .exe 后綴不含后綴的文件名,因此,在不使用分組進行捕獲的時候,我們利用了肯定順序型環視的限定,達到了既限定為 .exe 后綴又不被捕獲進匹配結果的效果,充分展示了環視不占位的特性。
(2)變種用法
所謂變種用法,主要指正則匹配部分位于肯定順序環視右側,匹配內容收到環視條件的限定,如: ^(?=[a-z]+$).+ ,雖然后面用的是 .+ ( . 除了不能匹配換行,能匹配任意字符),但是,這個表達式只能匹配一個以上的 a-z 字母組合,因為它被前面的環視限制了匹配范圍。
示例五:肯定順序環視變種用法
需求:必須包含字母(不區分大小寫)、數字,6-16位密碼
正則:
^(?=.*?[a-zA-Z])(?=.*?[0-9])[a-zA-Z0-9]{6,16}$
測試用例:
量詞條件:
- 小于6
- 6-16(關注邊界值)
- 大于16
字符條件:
- 純數字
- 純英文
- 數字+英文
- 英文+數字
- 英文數字亂序混合
注:每類字符條件都要考慮量詞條件</pre>
示例中,使用 (?=.*?[a-zA-Z]) 限定后面的字符中至少有一個字母,使用 (?=.*?[0-9]) 限定后面的字符中至少有一個數字,最后通過實際匹配正則 [a-zA-Z0-9]{6,16} 限定量詞。此示例,同樣提現了環視不占位的特性,否則的話,第一個環視消耗完字符,會導致后面匹配失敗,而實際并沒有,因為環視不消耗匹配字符。
2、否定順序: (?!exp)
示例六:否定順序環視
源字符串:
notexefile1.txt exefile1.exe exefile2.exe exefile3.exe notexefile2.php notexefile3.sh需求:獲取不是 .exe 后綴文件不含后綴的文件名
正則:
(.+)(?!\.exe)\.[^.]+$
結果:
notexefile1 notexefile2首先,拿到這個需求,看過前面肯定順序環視例子的寫法,我們很可能一下子寫出來 .+(?!\.exe) ,但是測試之后卻發現, 錯了! 為什么?一萬個為什么飄過~~~
為什么匹配錯誤,這涉及到正則匹配的原理,匹配過程如下:
為了解釋方便,這里以多行模式進行講解。
正則 .+ :因為沒有指定位置,從每行字符串開始位置開始匹配, .+ 是貪婪模式,盡可能多匹配,而且是匹配除了換行外的任意字符,因此,以第一行為例, .+ 匹配到 notexefile1.txt ,匹配位置移動到字符串最后。
正則 (?!\.exe) :匹配字符串結束位置,不是 .exe ,成功,匹配結束。
匹配結果得到: notexefile1.txt
其他幾行匹配過程是類似的,我們發現每行它都匹配上了,這不是我們預期的結果。
為了得到預期的結果,我們需要在環視限定的條件下,把后綴部分消耗掉,同時利用否定順序環視限定其不能是 .exe 后綴,然后用分組獲取文件名,得到表達式: (.+)(?!\.exe)\.[^.]+$ 。這個表達式的匹配過程,跟上面其實是類似的,只不過因為表達式沒有匹配完成,導致了回溯,回溯讓出了后綴部分給 \.[^.]+ 去匹配了。
在寫這個正則的過程中,我們可以先寫出 (.+)\.[^.]+$ 這樣的正則,然后在再后綴位置插入環視限定,從而得到目標正則 (.+)(?!\.exe)\.[^.]+$ 。
由于回溯過程涉及步驟過多,這里就不做展開,后面有機會再寫一個關于正則回溯的文章,現在大家可以打開這個 否定順序匹配與回溯 演示頁,分別查看3個版本的debug情況。
選擇版本:在正則輸入框上面的下拉菜單里
查看debug:左側TOOLS區域的 Regex Debugger 菜單。
注:由于該站jquery引用自谷歌,因此需要FQ加載才可以打開
當然也可以用Regexbuddy的Debug功能,這個可以參考《 正則表達式工具RegexBuddy使用教程 》查看Debug用法。
三個版本的正則都是 (.+)(?!\.exe)\.[^.]+$
源字符串分別是:
- 測試示例六,使用示例六源字符串
測試匹配成功情況回溯,源字符串
notexefile1.txt
測試匹配失敗情況回溯,源字符串
exefile1.exe
3、肯定逆序: (?<=exp)
(1)肯定逆序環視和否定逆序環視在一些語言中是不支持的,如JavaScript就不支持,大家在使用過程中需要注意一下。
(2)很多語言不支持非確定長度的逆序環視。所謂非確定長度,是指逆序環視部分內容,不是固定長度的,如 (?<=.*;)abc ,這里用的 .* 就是不固定的長度。無論是分支情況還是什么,逆序環視部分需要固定長度。
(3)有些語言里,支持特定范圍的非確定長度,這個是指 (?<=.{0,100};)abc 這種,本來的 .* 使用 0-100 這樣的限定最大長度為100的范圍值。
因此,大家使用過程中可以根據自己使用語言的差異,測試使用。
示例七:肯定逆序環視
源字符串:
name=Zjmainstay age=26需求:獲取name參數的值
正則: (?<=name=).+
示例很直白,前面必須是 name= ,然后獲取其后面的數據,由于環視不占位,因此并沒有出現在匹配結果中。
4、否定逆序: (?!exp)
示例八:否逆序環視
源字符串:
name=Zjmainstay age=26需求:獲取不是name參數的值
正則: ^[^=]+=(?<!name=)(.+)
跟否定順序示例一樣,我們不能直接用 (?<!name=).+ 進行匹配,正則做法是先把參數部分匹配出來,再用否定逆序環視對它進行限定,限定它不能是 name= ,因此實現匹配。
講到這里,你們是否能想到前面否定順序示例六中,可以用否定逆序來做?
正則: (.+)\.[^.]+(?<!\.exe)$
因此,幾個環視組合,由于正則所擺放的位置不同,可以產生等價的效果。
三、環視的應用
環視一直是正則表達式使用過程中的難題,主要體現在它的不占位(不消耗匹配字符)但起限定作用、肯定和否定、順序和逆序區分、擺放位置不同如何理解等概念上。經過上面的講解,相信讀者已經對這幾個概念有了深刻的理解,但是,理解概念跟靈活運用是兩碼事。
接下來我們再舉幾個平時常用的例子,幫助大家理解并掌握,達到靈活運用的程度。
示例九:正則分塊組合法-必須包含字母、數字、特殊字符
正則: ^(?=.*?[a-z])(?=.*?\d)(?![a-z\d]+$).+$
解析:
(?=.*?[a-z]) 限制必須有字母
(?=.*?\d) 限制必須有數字
(?![a-z\d]+$) 限制從開頭到結尾不能全為數字和字母
.+ 在沒有限定的情況下可以是任意字符
^ 和 $ 限定字符串的開頭和結尾
組合起來就可以得到上面正則。
示例十:正則逐步完善法-排除特定標簽 p/a/img ,匹配html標簽
正則: </?(?!p|a|img)([^> /]+)[^>]*/?>
解析:
常見的標簽格式有:
<p>...</p> //無屬性值 <p class="t"....>...</p> //有屬性值 <img ..../> //有屬性值自閉合 <br/> //無屬性值自閉合首先,從簡單標簽入手,對于 </p> 和 <br/> ,寫出正則:
</?[^>]*/?>
由于 [^>] 通配符的匹配訪問太大,因此,實際上無論有沒有屬性值,都被上面表達式給匹配了,這個沒關系,我們通過進一步細化匹配通配符,縮小匹配范圍。
我們觀察可得,標簽名是這樣得到的:
無屬性值:<p> <([^>]+) 有屬性值:<p class <([^ ]+) 無屬性值自閉合:<br/> <([^/]+) 閉合標簽:</p> </([^>]+)>得到正則:
</?([^> /]+)用這部分代替前面通配正則的標簽名部分,得到:
</?([^> /]+)[^>]*/?>
最后,我們需要排除 p/a/img 標簽,用否定順序法,在標簽名前面加入否定環視:
</?(?!p|a|img)([^> /]+)[^>]*/?>
大功告成,這是我們要的結果!
此示例的正則逐步完善法是正則書寫過程中常用方法,倒推回去也是可行的,比如,假如我們拿到一段很長的正則,而它的匹配結果是錯誤的,我們該怎么做?
我們可以用逐步截斷的方法,一步步的減除掉右側的一部分,直到它恢復匹配,我們就知道剛剛被減除掉的部分正則是有問題的,觀察它為什么導致錯誤,修改正確,再逐步恢復后面減除的正則即可。
示例十一:正則減除查錯法-匹配異常原因查找
源字符串:
<ul> <li class="item">item1</li> <li class="item">item2</li> <li class="item bug">item3</li> <li class="item">item4</li> <li class="item">item5</li> </ul>正則: <li class="item">(.*?)</li>
減除排錯過程:
例子比較簡單,主要演示思路過程。
用上面的正則去匹配源字符串,我們發現,明明預期5個結果,但是卻得到了4個,因此,我們開始進行減除正則排錯。
- 減除右側 </li> ,此時正則 <li class="item">(.*?) 匹配4個
- 減除右側 (.*?) ,此時正則 <li class="item"> ,匹配4個
- 減除 "item"> ,此時正則 <li class= , 匹配5個
- 恢復 "item"> ,減除 > ,此時正則 <li class="item" ,匹配4個
- 減除 " ,此時正則 <li class="item , 匹配5個
至此,觀察發現item后面還有其他可能,補充兼容:- 修復得正則 <li class="item[^"]*"
- 逐步把前面減除的 " 后面部分補充回來,此時正則 <li class="item[^"]*">(.*?)</li> , 匹配5個
問題解決!總結
文章至此,已經完整講解了正則表達式環視的概念與用法,讀者從中能夠了解到正則的逐步匹配原理,消耗與不消耗匹配字符原理,環視的不占位概念,環視作為一個虛擬位置限定其前后匹配的概念,環視肯定和否定類型與順序和逆序類型的概念,以及各種概念原理的運用,最后還附帶了正則書寫過程中運用的分塊組合法、逐步完善法和減除查錯法,希望能夠幫助廣大讀者更加深刻地理解正則表達式,達到靈活運用的程度。
來自:http://www.cnblogs.com/Zjmainstay/p/regexp-lookaround.html