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

wvyb2788 8年前發布 | 10K 次閱讀 正則表達式

文章大綱:

深入理解正則表達式環視的概念與用法
        一、環視的概念
            (一)環視概念與匹配過程示例
                示例一:簡單環視匹配過程
            (二)什么是消耗正則的匹配字符?
                示例二:一次匹配消耗匹配字符匹配過程
                示例三:多次匹配消耗匹配字符匹配過程
        二、環視的類型
            (一)肯定和否定
            (二)順序和逆序
            · 兩種類型名稱組合
            · 四種組合的用法
                四種組合正則與環視的擺放位置
                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 后面,即此時源字符串 ABCAD 的 A 這個字符已經被消耗,接下來的正則匹配從 A 后面開始。
  2. 正則 [A-Z] :當前匹配位置為第一個 A 字母后面位置,正則 [A-Z] 對源字符串 ABCAD 里的 B 字母進行匹配,匹配成功,位置后移到 B 字母后面的位置。至此,由于正則已經匹配完成,因此,正則 A[A-Z] 匹配結果是 AB 。

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

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

因此,全局匹配的過程補充如下:

  1. 正則 A :當前匹配位置為 B 字母后面位置,正則 A 去匹配源字符串中的 C ,匹配失敗,匹配位置后移一位,此時 C 被消耗了。
  2. 正則 A :當前匹配位置為 C 字母后面位置,正則 A 去匹配源字符串中的第二個 A 字母,匹配成功,匹配位置后移一位,此時 A 被消耗了。
  3. 正則 [A-Z] :當前匹配位置為第二個 A 字母后面位置,正則 [A-Z] 對源字符串 ABCAD 里的 D 字母進行匹配,匹配成功,位置后移到 D 字母后面的位置,此時 D 被消耗了。
  4. 由于正則里還有個源字符串結束位置,也就是正則里的 $ ,這個 $ 也是虛擬字符,因此,還要繼續進行匹配:
    正則 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}$

測試用例:

量詞條件:

  1. 小于6
  2. 6-16(關注邊界值)
  3. 大于16

字符條件:

  1. 純數字
  2. 純英文
  3. 數字+英文
  4. 英文+數字
  5. 英文數字亂序混合

注:每類字符條件都要考慮量詞條件</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)\.[^.]+$
源字符串分別是:

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

總結

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

 

 

來自:http://www.cnblogs.com/Zjmainstay/p/regexp-lookaround.html

 

 本文由用戶 wvyb2788 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!