深入理解正則表達式環視的概念與用法


在《深入理解正則表達式高級教程-環視》中已經對環視做了簡單的介紹,但是,可能還有一些讀者比較迷惑,今天特意以專題的形式,深入探討一下正則表達式的環視的概念與用法。

 

 

 

一、環視的概念

環視,在不同的地方又稱之為零寬斷言,簡稱斷言。 
環視強調的是它所在的位置,前面或者后面,必須滿足環視表達式中的匹配情況,才能匹配成功。 
環視可以認為是虛擬加入到它所在位置的附加判斷條件,並不消耗正則的匹配字符。

 

(一)環視概念與匹配過程示例

 

示例一:簡單環視匹配過程

例如,對於源字符串ABC,正則(?=A)[A-Z]匹配的是: 
1. (?=A)所在的位置,后面是A 
2. 表達式[A-Z]匹配A-Z中任意一個字母 
根據兩個的先后位置關系,組合在一起,那就是: 
(?=A)所在的位置,后面是A,而且是A-Z中任意一個字母,因此,上面正則表達式匹配一個大寫字母A

從例子可以看出,從左到右,正則分別匹配了環視(?=A)[A-Z],由於環視不消耗正則的匹配字符,因此,[A-Z]還能對A進行匹配,並得到結果。

 

(二)什么是消耗正則的匹配字符?

在《深入理解正則表達式高級教程》里我們已經講過,正則是按照單個字符來進行匹配的,一般情況下是從左到右,逐個匹配源字符串中的內容。

 

示例二:一次匹配消耗匹配字符匹配過程

例如,對於源字符串ABCAD,正則A[A-Z]匹配的過程是: 
1. 正則A:因為沒有位置限定,因此是從源字符串開始位置開始,也就是正則里的^,這個^是虛擬字符,表示匹配字符串開始位置,也就是源字符串ABCAD里的A前面的位置,因為正則A能夠匹配源字符串A,匹配成功,匹配位置從源字符串^的位置后移一位,到達A后面,即此時源字符串ABCADA這個字符已經被消耗,接下來的正則匹配從A后面開始。 
2. 正則[A-Z]:當前匹配位置為第一個A字母后面位置,正則[A-Z]對源字符串ABCAD里的B字母進行匹配,匹配成功,位置后移到B字母后面的位置。至此,由於正則已經匹配完成,因此,正則A[A-Z]匹配結果是AB

我們知道,有些語言如js支持g模式修飾符,也就是全局匹配,那么,上面例子中,正則匹配1次成功之后,將會從匹配成功位置(B字母后面位置)開始,再從頭進行匹配一次正則,直到源字符串全部消耗完為止。

 

示例三:多次匹配消耗匹配字符匹配過程

因此,全局匹配的過程補充如下: 
3. 正則A:當前匹配位置為B字母后面位置,正則A去匹配源字符串中的C,匹配失敗,匹配位置后移一位,此時C被消耗了。 
4. 正則A:當前匹配位置為C字母后面位置,正則A去匹配源字符串中的第二個A字母,匹配成功,匹配位置后移一位,此時A被消耗了。 
5. 正則[A-Z]:當前匹配位置為第二個A字母后面位置,正則[A-Z]對源字符串ABCAD里的D字母進行匹配,匹配成功,位置后移到D字母后面的位置,此時D被消耗了。 
6. 由於正則里還有個源字符串結束位置,也就是正則里的$,這個$也是虛擬字符,因此,還要繼續進行匹配: 
正則A:當前匹配位置為D字母后面的位置,正則A去匹配源字符串的結束位置,匹配失敗,匹配結束。

最終匹配結果是ABAD

 

二、環視的類型

環視的類型有兩類:

 

(一)肯定和否定

1、肯定:(?=exp) 和 (?<=exp) 
2、否定:(?!exp) 和 (?<!exp)

 

(二)順序和逆序

1、順序:(?=exp) 和 (?!exp) 
2、逆序:(?<=exp) 和 (?<!exp)

 

· 兩種類型名稱組合

1、肯定順序:(?=exp) 
2、否定順序:(?!exp) 
3、肯定逆序:(?<=exp) 
4、否定逆序:(?<!exp)

 

· 四種組合的用法

四種組合,根據正則與環視位置的不同,又可以組合出來8種不同的擺放方式。 
一般來說,順序的環視,放在正則后面,認為是常規用法,而放在正則前面,對正則本身的匹配起到了限制,則認為是變種的用法。 
而逆序的環視,常規用法是環視放在正則前面,變種用法是放在正則后面。 
總結一句話就是:常規用法,環視不對正則本身做限制。 
但是,無論常規和變種,都是非常常見的用法。

 

四種組合正則與環視的擺放位置

 
  1. 1、肯定順序常規: [a-z]+(?=;) 字母序列后面跟着;
  2. 2、肯定順序變種: (?=[a-z]+$).+$ 字母序列
  3. 3、肯定逆序常規: (?<=:)[0-9]+ :后面的數字
  4. 4、肯定逆序變種: \b[0-9]\b(?<=[13579]) 0-9中的奇數
  5. 5、否定順序常規: [a-z]+\b(?!;) 不以;結尾的字母序列
  6. 6、否定順序變種: (?!.*?[lo0])\b[a-z0-9]+\b 不包含l/o/0的字母數字系列
  7. 7、否定逆序常規: (?<!age)=([0-9]+) 參數名不為age的數據
  8. 8、否定逆序變種: \b[a-z]+(?<!z)\b 不以z結尾的單詞

下面示例,僅對肯定順序環視進行兩種用法的講解,其他組合都有類似用法,讀者參考上面列舉8種位置用法自行測試。

 

1、肯定順序:(?=exp)

 
(1)常規用法

所謂常規用法,主要指正則匹配部分位於肯定順序環視左側,如:test(?=\.php),用於匹配后綴是.php的test文件。

 
示例四:肯定順序環視常規用法

源字符串:

 
  1. notexefile1.txt
  2. exefile1.exe
  3. exefile2.exe
  4. exefile3.exe
  5. notexefile2.php
  6. notexefile3.sh

需求:獲取.exe后綴文件不含后綴的文件名 
正則:.+(?=\.exe) 
結果:

 
  1. exefile1
  2. exefile2
  3. exefile3

示例中,因為要獲取.exe后綴不含后綴的文件名,因此,在不使用分組進行捕獲的時候,我們利用了肯定順序型環視的限定,達到了既限定為.exe后綴又不被捕獲進匹配結果的效果,充分展示了環視不占位的特性。

 
(2)變種用法

所謂變種用法,主要指正則匹配部分位於肯定順序環視右側,匹配內容收到環視條件的限定,如:^(?=[a-z]+$).+,雖然后面用的是.+.除了不能匹配換行,能匹配任意字符),但是,這個表達式只能匹配一個以上的a-z字母組合,因為它被前面的環視限制了匹配范圍。

 
示例五:肯定順序環視變種用法

需求:必須包含字母(不區分大小寫)、數字,6-16位密碼 
正則:^(?=.*?[a-zA-Z])(?=.*?[0-9])[a-zA-Z0-9]{6,16}$ 
測試用例:

 
  1. #量詞條件:
  2. 1. 小於6
  3. 2. 6-16(關注邊界值)
  4. 3. 大於16
  5. #字符條件:
  6. 1. 純數字
  7. 2. 純英文
  8. 3. 數字+英文
  9. 4. 英文+數字
  10. 5. 英文數字亂序混合
  11. 注:每類字符條件都要考慮量詞條件

示例中,使用(?=.*?[a-zA-Z])限定后面的字符中至少有一個字母,使用(?=.*?[0-9])限定后面的字符中至少有一個數字,最后通過實際匹配正則[a-zA-Z0-9]{6,16}限定量詞。此示例,同樣提現了環視不占位的特性,否則的話,第一個環視消耗完字符,會導致后面匹配失敗,而實際並沒有,因為環視不消耗匹配字符。

 

2、否定順序:(?!exp)

 
示例六:否定順序環視

源字符串:

 
  1. notexefile1.txt
  2. exefile1.exe
  3. exefile2.exe
  4. exefile3.exe
  5. notexefile2.php
  6. notexefile3.sh

需求:獲取不是.exe后綴文件不含后綴的文件名 
正則:(.+)(?!\.exe)\.[^.]+$ 
結果:

 
  1. notexefile1
  2. notexefile2

首先,拿到這個需求,看過前面肯定順序環視例子的寫法,我們很可能一下子寫出來.+(?!\.exe),但是測試之后卻發現,錯了!為什么?一萬個為什么飄過~~~ 
為什么匹配錯誤,這涉及到正則匹配的原理,匹配過程如下: 
為了解釋方便,這里以多行模式進行講解。 
正則.+:因為沒有指定位置,從每行字符串開始位置開始匹配,.+是貪婪模式,盡可能多匹配,而且是匹配除了換行外的任意字符,因此,以第一行為例,.+匹配到notexefile1.txt,匹配位置移動到字符串最后。 
正則(?!\.exe):匹配字符串結束位置,不是.exe,成功,匹配結束。 
匹配結果得到:notexefile1.txt 
其他幾行匹配過程是類似的,我們發現每行它都匹配上了,這不是我們預期的結果。

為了得到預期的結果,我們需要在環視限定的條件下,把后綴部分消耗掉,同時利用否定順序環視限定其不能是.exe后綴,然后用分組獲取文件名,得到表達式:(.+)(?!\.exe)\.[^.]+$。這個表達式的匹配過程,跟上面其實是類似的,只不過因為表達式沒有匹配完成,導致了回溯,回溯讓出了后綴部分給\.[^.]+去匹配了。

在寫這個正則的過程中,我們可以先寫出(.+)\.[^.]+$這樣的正則,然后在再后綴位置插入環視限定,從而得到目標正則(.+)(?!\.exe)\.[^.]+$

由於回溯過程涉及步驟過多,這里就不做展開,后面有機會再寫一個關於正則回溯的文章,現在大家可以打開這個否定順序匹配與回溯演示頁,分別查看3個版本的debug情況。 
選擇版本:在正則輸入框上面的下拉菜單里 
查看debug:左側TOOLS區域的Regex Debugger菜單。 
注:由於該站jquery引用自谷歌,因此需要翻牆加載才可以打開

當然也可以用Regexbuddy的Debug功能,這個可以參考《正則表達式工具RegexBuddy使用教程》查看Debug用法。

三個版本的正則都是(.+)(?!\.exe)\.[^.]+$ 
源字符串分別是: 
1. 測試示例六,使用示例六源字符串 
2. 測試匹配成功情況回溯,源字符串

notexefile1.txt

3. 測試匹配失敗情況回溯,源字符串

exefile1.exe
 

3、肯定逆序:(?<=exp)

(1)肯定逆序環視和否定逆序環視在一些語言中是不支持的,如JavaScript就不支持,大家在使用過程中需要注意一下。 
(2)很多語言不支持非確定長度的逆序環視。所謂非確定長度,是指逆序環視部分內容,不是固定長度的,如(?<=.*;)abc,這里用的.*就是不固定的長度。無論是分支情況還是什么,逆序環視部分需要固定長度。 
(3)有些語言里,支持特定范圍的非確定長度,這個是指(?<=.{0,100};)abc這種,本來的.*使用0-100這樣的限定最大長度為100的范圍值。 
因此,大家使用過程中可以根據自己使用語言的差異,測試使用。

 
示例七:肯定逆序環視

源字符串:

 
  1. name=Zjmainstay
  2. age=26

需求:獲取name參數的值 
正則:(?<=name=).+

示例很直白,前面必須是name=,然后獲取其后面的數據,由於環視不占位,因此並沒有出現在匹配結果中。

 

4、否定逆序:(?<!exp)

 
示例八:否逆序環視

源字符串:

 
  1. name=Zjmainstay
  2. 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)([^> /]+)[^>]*/?> 
解析: 
常見的標簽格式有:

 
  1. <p>...</p> //無屬性值
  2. <p class="t"....>...</p> //有屬性值
  3. <img ..../> //有屬性值自閉合
  4. <br/> //無屬性值自閉合

首先,從簡單標簽入手,對於</p><br/>,寫出正則: 
</?[^>]*/?> 
由於[^>]通配符的匹配訪問太大,因此,實際上無論有沒有屬性值,都被上面表達式給匹配了,這個沒關系,我們通過進一步細化匹配通配符,縮小匹配范圍。 
我們觀察可得,標簽名是這樣得到的:

 
  1. 無屬性值:<p> <([^>]+)
  2. 有屬性值:<p class <([^ ]+)
  3. 無屬性值自閉合:<br/> <([^/]+)
  4. 閉合標簽:</p> </([^>]+)>

得到正則:

 
  1. </?([^> /]+)

用這部分代替前面通配正則的標簽名部分,得到: 
</?([^> /]+)[^>]*/?> 
最后,我們需要排除p/a/img標簽,用否定順序法,在標簽名前面加入否定環視: 
</?(?!p|a|img)([^> /]+)[^>]*/?> 
大功告成,這是我們要的結果!

此示例的正則逐步完善法是正則書寫過程中常用方法,倒推回去也是可行的,比如,假如我們拿到一段很長的正則,而它的匹配結果是錯誤的,我們該怎么做? 
我們可以用逐步截斷的方法,一步步的減除掉右側的一部分,直到它恢復匹配,我們就知道剛剛被減除掉的部分正則是有問題的,觀察它為什么導致錯誤,修改正確,再逐步恢復后面減除的正則即可。

 

示例十一:正則減除查錯法-匹配異常原因查找

源字符串:

 
  1. <ul>
  2. <li class="item">item1</li>
  3. <li class="item">item2</li>
  4. <li class="item bug">item3</li>
  5. <li class="item">item4</li>
  6. <li class="item">item5</li>
  7. </ul>

正則:<li class="item">(.*?)</li> 
減除排錯過程: 
例子比較簡單,主要演示思路過程。 
用上面的正則去匹配源字符串,我們發現,明明預期5個結果,但是卻得到了4個,因此,我們開始進行減除正則排錯。 
1. 減除右側</li>,此時正則<li class="item">(.*?) 匹配4個 
2. 減除右側(.*?),此時正則<li class="item">,匹配4個 
3. 減除"item">,此時正則<li class=,匹配5個 
4. 恢復"item">,減除>,此時正則<li class="item",匹配4個 
5. 減除",此時正則<li class="item,匹配5個 
至此,觀察發現item后面還有其他可能,補充兼容: 
6. 修復得正則<li class="item[^"]*" 
7. 逐步把前面減除的"后面部分補充回來,此時正則<li class="item[^"]*">(.*?)</li>,匹配5個 
問題解決!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM