正则表达式的环视深度剖析
文章目錄
- 一、環視基礎
- 二、順序環視匹配過程
- (一)順序肯定環視匹配過程
- (二)順序否定環視匹配過程
- 三、逆序環視匹配過程
- (一)逆序環視基礎
- (二)逆序肯定環視匹配過程
- 1. 逆序表達式的長度固定,如何匹配
- 2. 逆序表達式的長度不固定,如何匹配
- (1)匹配開始位置不確定,匹配結束位置確定
- (2)匹配開始位置確定,匹配結束位置不確定
- (三)逆序否定環視匹配過程
- 1. 逆序表達式的長度固定,如何匹配
- (1)匹配起始位置不確定,匹配結束位置確定
- (2)匹配起始位置確定,匹配結束位置不確定
- 2. 逆序表達式的長度不固定,如何匹配
一、環視基礎
環視只進行子表達式的匹配,不占有字符,匹配到的內容不保存到最終的匹配結果,是零寬度的。環視匹配的最終結果就是一個位置。
環視的作用相當于對所在位置加了一個附加條件,只有滿足這個條件,環視子表達式才能匹配成功。
環視按照方向劃分有順序和逆序兩種,按照是否匹配有肯定和否定兩種,組合起來就有四種環視。順序環視相當于在當前位置右側附加一個條件,而逆序環視相當于在當前位置左側附加一個條件。
| (?<=Expression) | 逆序肯定環視,表示所在位置左側能夠匹配 Expression |
| (?<!Expression) | 逆序否定環視,表示所在位置左側不能匹配 Expression |
| (?=Expression) | 順序肯定環視,表示所在位置右側能夠匹配 Expression |
| (?!Expression) | 順序否定環視,表示所在位置右側不能匹配 Expression |
環視是正則中的一個難點,對于環視的理解,可以從應用和原理兩個角度理解,如果想理解得更清晰、深入一些,還是從原理的角度理解好一些,正則匹配基本原理參考《NFA引擎匹配原理》。
上面提到環視相當于對“所在位置”附加了一個條件,環視的難點在于找到這個“位置”,這一點解決了,環視也就沒什么秘密可言了。
對于順序肯定環視(?=Expression)來說,當子表達式Expression匹配成功時,(?=Expression)匹配成功,并報告(?=Expression)匹配當前位置成功。
對于順序否定環視(?!Expression)來說,當子表達式Expression匹配成功時,(?!Expression)匹配失敗;當子表達式Expression匹配失敗時,(?!Expression)匹配成功,并報告(?!Expression)匹配當前位置成功。
二、順序環視匹配過程
(一)順序肯定環視匹配過程
順序肯定環視的例子已在《NFA引擎匹配原理》中講解過了,請移步參考。
(二)順序否定環視匹配過程
源字符串:aa<p>one</p>bb<div>two</div>cc
正則表達式:<(?!/?p\b)[^>]+>
這個正則的意義就是匹配除 <p> 或 </p>之外的其余標簽。/? 表示匹配正斜杠 0 次或 1 次;\b 表示匹配字符邊界。
首先由表達式的字符 < 取得控制權,從源字符串位置 0 開始匹配,由于 < 匹配 a 失敗,在位置 0 處整個表達式匹配失敗,第一次迭代匹配失敗,正則引擎向前傳動,由位置 1 處開始嘗試第二次迭代匹配。
重復以上過程,直到位置 2,表達式的字符 < 匹配源字符串的字符 < 成功,控制權交給 (?!/?p\b);(?!/?p\b) 子表達式取得控制權后,進行內部子表達式的匹配。首先由 /? 取得控制權,嘗試匹配 p 失敗,進行回溯,不匹配,控制權交給 p;由 p 來嘗試匹配 p,匹配成功,控制權交給 \b;由 \b 來嘗試匹配位置 4,匹配成功。此時子表達式匹配完成,/?p\b 匹配成功,那么環視表達式 (?!/?p\b) 就匹配失敗。在位置 2 處整個表達式匹配失敗,新一輪迭代匹配失敗,正則引擎向前傳動,由位置 3 處開始嘗試下一輪迭代匹配。
在位置 8 處也會遇到一輪 /?p\b 匹配 /p 成功,而導致環視表達式 (?!/?p\b) 匹配失敗,從而導致整個表達式匹配失敗的過程。
重復以上過程,直到位置 14,< 匹配 < 成功,控制權交給 (?!/?p\b);/? 嘗試匹配 d 失敗,進行回溯,不匹配,控制權交給 p;由 p 來嘗試匹配 d,匹配失敗,已經沒有備選狀態可供回溯,匹配失敗。此時子表達式匹配完成,/?p\b 匹配失敗,那么環視表達式 (?!/?p\b) 就匹配成功。匹配的結果是位置15,然后控制權交給 [^>]+;由 [^>]+ 從位置 15 進行嘗試匹配,可以成功匹配到 div,控制權交給 >;由 >來匹配 >,匹配成功。此時正則表達式匹配完成,報告匹配成功。
匹配結果為 <div>,開始位置為 14,結束位置為 19。其中 < 匹配 <,(?!/?p\b) 匹配位置 15,[^>]+ 匹配字符串 div,> 匹配 >。
三、逆序環視匹配過程
(一)逆序環視基礎
對于逆序肯定環視 (?<=Expression) 來說,當子表達式 Expression 匹配成功時,(?<=Expression) 匹配成功,并報告 (?<=Expression) 匹配當前位置成功。
對于逆序否定環視 (?<!Expression) 來說,當子表達式 Expression 匹配成功時,(?<!Expression) 匹配失敗;當子表達式 Expression 匹配失敗時,(?<!Expression)匹配成功,并報告(?<!Expression)匹配當前位置成功;
順序環視相當于在當前位置右側附加一個條件,所以它的匹配嘗試是從當前位置開始的,然后向右嘗試匹配,直到某一位置使得匹配成功或失敗為止。而逆序環視的特殊處在于,它相當于在當前位置左側附加一個條件,所以它不是在當前位置開始嘗試匹配的,而是從當前位置左側某一位置開始,匹配到當前位置為止,報告匹配成功或失敗。
順序環視嘗試匹配的起點是確定的,就是當前位置,而匹配的終點是不確定的。逆序環視匹配的起點是不確定的,是當前位置左側某一位置,而匹配的終點是確定的,就是當前位置。
所以順序環視相對是簡單的,而逆序環視相對是復雜的。這也就是為什么大多數語言和工具都提供了對順序環視的支持,而只有少數語言提供了對逆序環視支持的原因。
JavaScript 中只支持順序環視,不支持逆序環視。
Java 中雖然順序環視和逆序環視都支持,但是逆序環視只支持長度確定的表達式,逆序環視中量詞只支持“?”,不支持其它長度不定的量詞。長度確定時,引擎可以向左查找固定長度的位置作為起點開始嘗試匹配,而如果長度不確定時,就要從當前位置向左逐個位置開始嘗試匹配,不成功則回溯,再向左側位置進行嘗試匹配,然后重復以上過程,直到匹配成功,或是嘗試到位置0處以后,報告匹配失敗,處理的復雜度是顯而易見的。
目前只有.NET中支持不確定長度的逆序環視。
(二)逆序肯定環視匹配過程
1. 逆序表達式的長度固定,如何匹配
源字符串:<div>a test</div>
正則表達式:(?<=<div>)[^<]+(?=</div>)
這個正則的意義就是匹配 <div> 和 </div> 標簽之間的內容,而不包括 <div> 和 </div>標簽本身。
首先由逆序肯定環視表達式 (?<=<div>) 取得控制權,從位置 0 開始匹配,由于逆序肯定環視表達式中的子表達式 <div> 長度是 5,所以正則引擎會從當前位置向左側查找 5 個字符來匹配,可是當前位置是 0,左側沒有任何內容,所以子表達式 <div> 必然匹配失敗,從而逆序肯定環視表達式 (?<=<div>) 匹配失敗,則整個正則表達式在字符串的位置 0 處匹配失敗,即正則表達式的第 1 輪迭代匹配失敗。
正則引擎向前傳動,由位置 1 處開始嘗試第 2 次迭代匹配,由于位置左側的字符數量不足,所以也是匹配失敗。直到傳動到位置 5,正則引擎向左查找到 5 個字符,(?<=<div>) 取得控制權后,由位置 0 開始向右逐個字符匹配,結果子表達式 <div> 匹配字符串 <div> 成功,從而整個逆序肯定環視表達式 (?<=<div>) 匹配成功,匹配成功的位置是 5,控制權交給下一個子表達式 [^<]+;[^<]+ 從位置 5 向右開始逐個字符匹配,匹配字符串 a test 成功,控制權交給順序肯定環視表達式 (?=</div>);由 </div> 匹配 </div> 成功,從而順序肯定環視表達式 (?=</div>) 匹配成功,位置 11 匹配成功。
此時正則表達式匹配完成,報告匹配成功。匹配到的字符串為 a test,匹配開始位置為 5,匹配結束位置為 11。其中 (?<=<div>) 匹配位置 5,[^<]+ 匹配字符串 a test,(?=</div>) 匹配位置 11。
疑問:
逆序環視表達式的匹配是如何確定匹配開始位置的?如果是按照表達式的長度向左查找對應數量的字符數,從而確定匹配起點,那么當前位置左側的字符數量不足時,匹配起點位置就無法確定,也就不會逐個字符去匹配了,因為長度都不同,匹配結果肯定是失敗的。猜測,大概率是按逆序環視子表達式的長度(或者最小長度)來確定起點,如果字符數不足,就沒有必要逐個字符去匹配,因為這是多余的,匹配結果肯定是失敗的。
2. 逆序表達式的長度不固定,如何匹配
源字符串:<div id=“test1”>a test</div>
正則表達式:(?<=<div[^>]*>)[^<]+(?=</div>)
(1)匹配開始位置不確定,匹配結束位置確定
注:我不認可這樣的匹配邏輯。
首先由“(?<=<div[^>]*>)”取得控制權,由位置 0 開始匹配,由于“<div[^>]*>”的長度不固定,可能會由逆序環視表達式的第 1 個字符從當前位置向左逐字符查找(這個可能性不大,因為太傻了!);有可能是先計算逆序表達式最小長度,然后在當前位置向前查找初始的匹配起點位置。在這里“<div[^>]*>”至少需要 5 個字符,所以由當前位置向左查找 5 個字符,然后再從左到右的方向,從這 5 個字符的第 1 個字符開始嘗試匹配,但是由于此時位于位置 0處,前面沒有任何字符,所以嘗試匹配失敗。
正則引擎傳動裝置向右傳動,由位置 1 處開始嘗試匹配,同樣因為左側的字符數不足,所以直接匹配失敗,直到位置 5 處,向左查找 5 個字符,滿足條件,此時把控制權交給“(?<=<div[^>]*>)”中的子表達式“<div[^>]*>”。“<div[^>]*>”取得控制權后,由位置 0 處開始向右嘗試匹配,由于正則都是逐字符進行匹配的,所以這時會把控制權交給“<div[^>]*>”中的“<”,由“<”嘗試匹配字符串中的“<”,匹配成功,接下來由“d”嘗試匹配字符串中的“d”,匹配成功,同樣的過程,由“<div[^>]*”匹配位置 0 到位置 5 之間的“<div ”成功,其中“[^>]*”在匹配“<div ”中的空格時會記錄可供回溯的狀態的,此時控制權交給“>”,由于已沒有任何字符可供匹配,所以“>”匹配失敗,此時進行回溯,由“[^>]*”讓出已匹配的空格給“>”進行匹配,同樣匹配失敗,此時已沒有可供回溯的狀態,所以這一輪迭代匹配失敗。
正則引擎傳動裝置向右傳動,由位置 6 處開始嘗試匹配,同樣匹配失敗,直到位置 16 處,此時的當前位置指的就是位置 16,向左查找到 5 個字符,把控制權交給“(?<=<div[^>]*>)”中的子表達式“<div[^>]*>”。“<div[^>]*>”取得控制權后,由位置 11 處開始向右嘗試匹配, “<div[^>]*>”中的“<”嘗試匹配字符串中的“s”,匹配失敗;繼續向左嘗試,在位置 10 處由“<”嘗試匹配字符串中的“e”,也匹配失敗。同樣的過程,直到嘗試到位置 0 處,最后“<div[^>]*>”以位置 0 作為匹配起點,向右匹配,結果成功匹配到“<div id=“test1”>”,此時“(?<=<div[^>]*>)”匹配成功,控制權交給“[^>]+”,繼續進行下面的匹配…
注:我認為這樣的匹配規則是錯誤的,因為“<div[^>]*>”中的“<”匹配失敗后往左嘗試匹配,這樣的做法很不合理,為什么?假設“<”繼續向左嘗試匹配,最后匹配成功了,控制權交個下個表達式,而該表達式匹配失敗了,“<”會繼續向左嘗試匹配,可能又匹配成功了,但是下個表達式又匹配失敗,這樣的匹配邏輯肯定不對!!!
(2)匹配開始位置確定,匹配結束位置不確定
注:這個更符合逆序的概念,也更加合理,我認可這種匹配邏輯!
源字符串:<div>a test</div>
正則表達式:(?<=<div>)[^<]+(?=</div>)
“(?<=<div>)”獲得控制權,從源字符串位置 0 開始向左匹配,首先“>” 去匹配,但是位置 0 左側沒有字符,所以匹配失敗,第 1 次迭代匹配失敗;接著正則引擎指針向右移動,“>” 去匹配字符串的字符“<”,匹配失敗,第 2 次迭代匹配失敗。
重復上述過程,直到位置 5,子表達式“<div>”中的“>” 去匹配位置 5 左邊的第 1 個字符“>”,匹配成功;子表達式“<div>”中的“v”去匹配位置 5 左邊第 2 個字符“v”,匹配成功…,最后子表達式“<div>”成功匹配位置 5 左邊的字符串“<div>”,那么說明逆序肯定環視表達式“(?<=<div>)”匹配成功,即成功匹配位置 5;接著控制權給表達式“[^<]+”,該表達式從位置 5 開始向右逐個字符匹配,最后成功匹配到字符串“a test”,接著把控制權交個子表達式“(?=</div>)”,由它去驗證字符串“a test”的結尾位置 11 是否符合正則式的要求,結果“(?=</div>)”成功匹配到了字符串“a test”后面的字符串“</div>”,說明字符串“a test”的結尾位置 11 符合要求,后續沒有子表達式了,說明正則表達式迭代匹配成功 1 次,成功匹配到字符串“a test”。接著從位置 11 開始下次迭代匹配…
后面重復上述的過程,直到正則引擎的指針移到字符串的結尾處,則停止迭代匹配。
(三)逆序否定環視匹配過程
源字符串:adf<B>BerBilBlon<B>Ssdfefe</B>dfee
正則表達式:(?<!<B>)B
1. 逆序表達式的長度固定,如何匹配
(1)匹配起始位置不確定,匹配結束位置確定
當前位置是匹配終點,匹配起點在當前位置的左側,最終的匹配起點是不確定的,初始的匹配起點可以根據逆序表達式的長度來查找。
注:我認為這樣的匹配邏輯是錯誤的,不認可
首先由“(?<!<B>)”的子表達式“<B>”取得控制權,由位置 0 開始嘗匹配,由于“<B>”的長度固定為 3,所以會從當前位置向左查找 3個字符,但是由于此時位于位置 0 處,前面沒有任何字符,所以直接匹配失敗,“<B>”匹配失敗,那么整個逆序否定環視表達式“(?<!<B>)”則匹配成功,所以位置 0 滿足逆序否定環視表達式“(?<!<B>)”,那么控制權就傳給了“B”,由“B”從位置 0 開始向右匹配字符,于是“B”就去匹配字符串中的“a”,結果匹配失敗,那么第 1 次迭代匹配失敗。
正則引擎傳動裝置向右傳動,你可以理解為有個指針的東西向右移動,此時指針來到位置 1 處,由位置 1 處向左查找 3 個字符,但是前面只有 1 個字符 a,所以同樣和“<B>”匹配失敗,則整個逆序否定環視表達式“(?<!<B>)”匹配成功,控制權傳給“B”,由“B”從位置 1 開始向右匹配字符,于是“B”就去匹配字符串中的“d”,結果匹配失敗,那么第 2 次迭代匹配失敗。
直到位置 3 處,向左查找到 3 個字符串“abc”,字符數滿足條件,此時“(?<!<B>)”中的子表達式“<B>”獲得控制權。“<B>”取得控制權后,由位置 0 處開始向右逐個字符匹配字符串“abc”,既然是逐字符進行匹配的,所以這時會把控制權交給“<B>”中的“<”,由“<”嘗試匹配字符串中的“a”,匹配失敗,那么“<B>”就和字符串“abc”匹配失敗,則整個逆序否定環視表達式“(?<!<B>)”匹配成功,控制權傳給“B”,由“B”從位置 3 開始向右匹配字符,于是“B”就去匹配字符串中的“<”,結果匹配失敗,那么第 4 次迭代匹配失敗。
正則引擎的傳動指針繼續向右移動,此時來到了位置 4,那么正則引擎向左查找 3 個字符來匹配,查找到的字符串就是“df<”,接著“<B>”獲得控制權,從位置 1 開始向右逐個字符匹配,那么首先由“<B>”中的“<”去匹配字符“d”,匹配失敗,那么整個逆序否定環視表達式“(?<!<B>)”匹配成功,控制權傳給“B”,由“B”從位置 4 開始向右匹配字符,于是“B”就去匹配位置 4 后面的“B”,結果匹配成功。
重復上述的過程直到正則引擎的指針移到字符串結尾才結束迭代匹配。
最后匹配到的“B”,如下所示(高亮部分):
(2)匹配起始位置確定,匹配結束位置不確定
當前位置是匹配起點,逆序環視是從當前位置向左開始匹配的,匹配終點在當前位置的左側。不少人認為應該是這樣的匹配規則,因為更符合逆序的概念。我也支持這個匹配邏輯。
注:需要明確的一點,無論是什么樣的正則表達式,都是要從字符串的位置 0 處開始嘗試匹配的,這點沒有變。
逆序否定環視表達式“(?<!<B>)B”中的“<B>”先獲得控制權,因為匹配從右到左,所以子表達式“<B>”中的“>”會先獲得控制權,去匹配字符串當前位置左邊的第 1 個字符,不過當前位置是 0,所以左側沒有字符,固然匹配失敗,既然“<B>”匹配失敗,那么整個逆序否定環視表達式“(?<!<B>)B”就匹配成功,也就是說位置 0 是匹配成功的,位置 0 是滿足逆序否定環視表達式的,于是控制權交給“B”,由“B”從字符串位置 0 開始向右匹配字符,顯然“B”匹配“a”是失敗的,因此整個正則表達式的第 1 次迭代匹配失敗。
重復上述的過程,直到位置 4,“<B>”從位置 4 開始向左逐個字符匹配,首先由“>”匹配位置 4 左邊的第 1 個字符“<”,結果匹配失敗,于是整個逆序否定環視表達式“(?<!<B>)B”匹配成功,也就是說位置 4 匹配成功,控制權交個了“B”,由“B”從位置 4 開始向右匹配字符,顯示“B”與字符“B”匹配成功。
重復上述過程,直到正則引擎的指針移到位置 6 時,“<B>”逐個字符匹配位置 6 左側的字符,首先“<B>”中的“>”先去匹配位置 6 左邊的第 1 個字符“>”,匹配成功;接著“<B>”中的“B”去匹配位置 6 左邊的第 2 個字符“B”,也匹配成功;接著“<B>”中的“<”去匹配位置 6 左邊的第 3 個字符“<”,也匹配成功。那么最后“<B>”成功匹配到位置 6 左邊的字符串“<B>”,因為是否定環視,所以整個逆序否定環視表達式匹配失敗(即位置 6 不符合要求),所以整個正則表達式的迭代匹配失敗,正則引擎的指針繼續向后移。
重復上述過程,直到正則引擎指針移到字符串結尾處,正則迭代匹配結束。
2. 逆序表達式的長度不固定,如何匹配
略
總結
以上是生活随笔為你收集整理的正则表达式的环视深度剖析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 正则表达式实例解读
- 下一篇: 正则表达式之 NFA 引擎匹配原理详解