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


文章大綱:

    深入理解正則表達式環視的概念與用法
        一、環視的概念
            (一)環視概念與匹配過程示例
                示例一:簡單環視匹配過程
            (二)什么是消耗正則的匹配字符?
                示例二:一次匹配消耗匹配字符匹配過程
                示例三:多次匹配消耗匹配字符匹配過程
        二、環視的類型
            (一)肯定和否定
            (二)順序和逆序
            · 兩種類型名稱組合
            · 四種組合的用法
                四種組合正則與環視的擺放位置
                1、肯定順序:(?=exp)
                    (1)常規用法
                        示例四:肯定順序環視常規用法
                    (2)變種用法
                        示例五:肯定順序環視變種用法
                2、否定順序:(?!exp)
                        示例六:否定順序環視
                3、肯定逆序:(?<=exp)
                    示例七:肯定逆序環視
                4、否定逆序:(?<!exp)
                    示例八:否逆序環視
        三、環視的應用
            示例九:正則分塊組合法-必須包含字母、數字、特殊字符
            示例十:正則逐步完善法-排除特定標簽p/a/img,匹配html標簽
            示例十一:正則減除查錯法-匹配異常原因查找
        總結

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

一、環視的概念

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

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

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

例如,對於源字符串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、肯定順序常規: [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}$
測試用例:

#量詞條件:
1. 小於6
2. 6-16(關注邊界值)
3. 大於16

#字符條件:
1. 純數字
2. 純英文
3. 數字+英文
4. 英文+數字
5. 英文數字亂序混合

注:每類字符條件都要考慮量詞條件

示例中,使用(?=.*?[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引用自谷歌,因此需要翻牆加載才可以打開

當然也可以用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的范圍值。
因此,大家使用過程中可以根據自己使用語言的差異,測試使用。

示例七:肯定逆序環視

源字符串:

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個,因此,我們開始進行減除正則排錯。

  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個
    問題解決!

總結

文章至此,已經完整講解了正則表達式環視的概念與用法,讀者從中能夠了解到正則的逐步匹配原理,消耗與不消耗匹配字符原理,環視的不占位概念,環視作為一個虛擬位置限定其前后匹配的概念,環視肯定和否定類型與順序和逆序類型的概念,以及各種概念原理的運用,最后還附帶了正則書寫過程中運用的分塊組合法、逐步完善法和減除查錯法,希望能夠幫助廣大讀者更加深刻地理解正則表達式,達到靈活運用的程度。


更多關於正則表達式入門的內容,請參考本站博客《我眼里的正則表達式入門教程
更多關於正則表達式高級的內容,請參考本站博客《深入講解正則表達式高級教程
Windows正則表達式測試工具請從《正則表達式測試工具RegexBuddy-4.1.0》下載
Mac正則表達式測試工具請從《Mac正則表達式測試工具》下載

文章首發自Zjmainstay學習筆記《深入理解正則表達式環視的概念與用法


免責聲明!

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



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