環視只進行子表達式的匹配,不佔有字符,匹配到的內容不保存到最終的匹配結果,是零寬度的。環視匹配的最終結果就是一個位置。正則表達式
環視只進行子表達式的匹配,不佔有字符,匹配到的內容不保存到最終的匹配結果,是零寬度的。環視匹配的最終結果就是一個位置。ide
環視的做用至關於對所在位置加了一個附加條件,只有知足這個條件,環視子表達式才能匹配成功。工具
環視按照方向劃分有順序和逆序兩種,按照是否匹配有確定和否認兩種,組合起來就有四種環視。順序環視至關於在當前位置右側附加一個條件,而逆序環視至關於在當前位置左側附加一個條件。測試
表達式spa |
說明ip |
(?<=Expression)ci |
逆序確定環視,表示所在位置左側可以匹配Expression文檔 |
(?<!Expression)字符串 |
逆序否認環視,表示所在位置左側不能匹配Expressionit |
(?=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開始匹配,因爲「<」匹配「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中支持不肯定長度的逆序環視。
源字符串:<div>a test</div>
正則表達式:(?<=<div>)[^<]+(?=</div>)
這個正則的意義就是匹配<div>和</div>標籤之間的內容,而不包括<div>和</div>標籤自己。
匹配過程:
首先由「(?<=<div>)」取得控制權,從位置0開始匹配,因爲位置0是起始位置,左側沒有任何內容,因此「<div>」必然匹配失敗,從而環視表達式「(?<=<div>)」匹配失敗,致使整個表達式在位置0處匹配失敗。第一輪迭代匹配失敗,正則引擎向前傳動,由位置1處開始嘗試第二次迭代匹配。
直到傳動到位置5,「(?<=<div>)」取得控制權,向左查找5個位置,由位置0開始匹配,由「<div>」匹配「<div>」成功,從而「(?<=<div>)」匹配成功,匹配的結果爲位置5,控制權交給「[^<]+」;「[^<]+」從位置5開始嘗試匹配,匹配「a test」成功,控制權交給「(?=</div>)」;由「</div>」匹配「</div>」成功,從而「(?=</div>)」匹配成功,匹配結果爲位置11。
此時正則表達式匹配完成,報告匹配成功。匹配結果爲「a test」,開始位置爲5,結束位置爲11。其中「(?<=<div>)」匹配位置5,「[^<]+」匹配「a test」,「(?=</div>)」匹配位置11。
逆序否認環視的匹配過程與上述過程相似,區別只是當Expression匹配失敗時,逆序否認表達式(?<!Expression)才匹配成功。
到此環視的匹配原理已基本講解完,環視也就沒有什麼祕密可言了,所須要的,也只是多加練習而已。
今天寫累了,暫時就給出一個環視的綜合應用實例吧,至於環視的應用場景和技巧,後面再整理。
需求:數字格式化成用「,」的貨幣格式。
正則表達式:(?<=\d)(?<!\.\d*)(?=(?:\d{3})+(?:\.\d+|$))
測試代碼:
double[] data = new double[] { 0, 12, 123, 1234, 12345, 123456, 1234567, 123456789, 1234567890, 12.345, 123.456, 1234.56, 12345.6789, 123456.789, 1234567.89, 12345678.9 };
foreach (double d in data)
{
richTextBox2.Text += "源字符串:" + d.ToString().PadRight(15) + "格式化:" + Regex.Replace(d.ToString(), @"(?<=\d)(?<!\.\d*)(?=(?:\d{3})+(?:\.\d+|$))", ",") + "\n";
}
輸出結果:
源字符串:0 格式化:0
源字符串:12 格式化:12
源字符串:123 格式化:123
源字符串:1234 格式化:1,234
源字符串:12345 格式化:12,345
源字符串:123456 格式化:123,456
源字符串:1234567 格式化:1,234,567
源字符串:123456789 格式化:123,456,789
源字符串:1234567890 格式化:1,234,567,890
源字符串:12.345 格式化:12.345
源字符串:123.456 格式化:123.456
源字符串:1234.56 格式化:1,234.56
源字符串:12345.6789 格式化:12,345.6789
源字符串:123456.789 格式化:123,456.789
源字符串:1234567.89 格式化:1,234,567.89
源字符串:12345678.9 格式化:12,345,678.9
實現分析:
首先根據需求能夠肯定是把一些特定的位置替換爲「,」,接下來就是分析並找到這些位置的規律,並抽象出來以正則表達式來表示。
一、 這個位置的左側必須爲數字
二、 這個位置右側到出現「.」或結尾爲止,必須是數字,且數字的個數必須爲3的倍數
三、 這個位置左側相隔任意個數字不能出現「.」
由以上三條,就能夠徹底肯定這些位置,只要實現以上三條,組合一下正則表達式就能夠了。
根據分析,最終匹配的結果是一個位置,因此全部子表達式都要求是零寬度。
一、 是對當前所在位置左側附加的條件,因此要用到逆序環視,由於要求必須出現,因此是確定的,符合這一條件的子表達式即爲「(?<=\d)」
二、 是對當前所在位置右側附加的條件,因此要用到順序環視,也是要求出現,因此是確定的,是數字,且個數爲3的倍數,即「(?=(?:\d{3})*)」,到出現「.」或結尾爲止,即「(?=(?:\d{3})*(?:\.|$))」
三、 是對當前所在位置左側附加的條件,因此要用到逆序環視,由於要求不能出現,因此是否認的,即「(?<!\.\d*)」
由於零寬度的子表達式是非互斥的,最後匹配的都是同一個位置,因此前後順序是不影響最後的匹配結果的,能夠任意組合,只是習慣上把逆序環視寫在左側,順序環視寫在右側。