《正則表達式經典實例(第2版)》——2.16 測試一個匹配,但不添加到整體匹配中
本節書摘來自異步社區《正則表達式經典實例(第2版)》一書中的第2章,第2.16節,作者: 【美】Jan Goyvaerts , Steven Levithan著,更多章節內容可以訪問雲棲社區“異步社區”公眾號查看
2.16 測試一個匹配,但不添加到整體匹配中
問題描述
找出在一對HTML粗體標簽之間的任何單詞,但是不要把標簽包含到正則表達式匹配中。例如,如果目標文本是My cat is furry,那麼唯一的匹配應當是cat。
解決方案
(?<=<b>)\w+(?=</b>)
正則選項:不區分大小寫
正則流派:.NET、Java、PCRE、Perl、Python、Ruby 1.9
JavaScript和Ruby 1.8支持順序環視(lookahead)‹(?=)›,但是不支持逆序環視(lookbehind)‹(?<=< b >)›。
討論
環視
現代的正則流派都支持四種類型的環視(lookaround),它們擁有特殊的能力,可以放棄在環視內部的正則表達式所匹配的文本。實質上,環視會檢查某些文本是否可以被匹配,但是並不會實際去匹配它。
向回看的環視被稱作是逆序環視。這是唯一可以從右向左而非從左向右遍曆目標文本的正則表達式結構。肯定型逆序環視(positive lookbehind)的語法是‹(?<= ⋯)›。‹(?<=›4個字符構成了起始括號。你能在逆序環視內部放入什麼內容(這裏由‹⋯›表示)在不同正則表達式流派中是不一樣的。但是簡單的字麵文本總是沒問題的,如‹(?<=< b >)›。
逆序環視會檢查在逆序環視中的文本是否直接出現在正則表達式引擎所到達位置的左邊。如果用‹(?<=< b >)›來匹配My < b >cat</ b > is furry,隻有到正則表達式在目標文本中的字母c處開始進行匹配嚐試時,逆序環視才會匹配成功。正則引擎接著會進入逆序環視分組,告訴它向左邊看。‹< b >›在c的左邊成功匹配。正則引擎會在這個時候退出逆序環視,並且丟棄逆序環視所匹配到的任何文本。換句話說,正在進行的匹配會回到引擎剛剛進入逆序環視的地方。在這個例子中,正在進行匹配的是目標字符串中c之前的一個長度為0的匹配。逆序環視隻會測試或者斷言‹< b >›是否可以被匹配;但是它並不會實際上去匹配它。環視結構因此也被稱作長度為0的斷言。
在逆序環視匹配之後,字符組簡寫‹\w+›會嚐試去匹配一個或者多個單詞字符。它會匹配cat。‹\w+›並不屬於任何類型的環視或者分組,因此它會正常地匹配文本cat。我們說‹\w+›匹配並且消耗(consume)了cat,而環視則隻能匹配內容,卻從來不會消耗任何東西。
向前看的環視,也就是按照正則表達式通常遍曆文本的方向,被稱作順序環視(lookahead)。順序環視在本書中的所有正則流派中都擁有同等的支持。肯定型順序環視(positive lookahead)的語法是‹(?=⋯)›。這3個字符‹(?=›構成了該分組的起始括號。在一個正則表達式中可以使用的任何符號都可以在順序環視內部使用,在這裏用‹⋯›來表示。
當‹(?<=< b >)\w+(?=< /b >)›中的‹\w+›匹配了My < b >cat</ b > is furry中的cat的時候,正則引擎就進入了順序環視。在這個時候順序環視唯一特殊的行為是正則引擎會記住它已經匹配了的文本部分,並把它同順序環視關聯起來。‹</ b >›隨後會正常匹配。現在正則引擎退出順序環視。在環視中的正則表達式成功匹配,因此環視自身也就匹配成功了。正則引擎還原在進入環視之前它記住的正在進行的匹配,從而丟棄由環視匹配的文本。這樣我們整體上的匹配進程就回到了cat。因為我們正則表達式也在此結束,所以cat就成為了最終的匹配結果。
否定型環視
把環視中的等號換成感歎號的話,‹(?!⋯)›就變成了否定型順序環視(negative lookahead)。否定型順序環視與肯定型順序環視用起來是一樣的,唯一的區別是,肯定型順序環視會在順序環視中的正則式匹配時成功匹配,而否定型順序環視則正好相反,它在當順序環視內的正則式匹配時,匹配失敗。
匹配的過程則是完全相同的。引擎會在進入否定型順序環視的時候保存當前匹配進程,然後試圖正常地匹配順序環視中的正則表達式。如果這個子表達式匹配的話,那麼否定型順序環視會失敗,而正則引擎會進行回溯。如果這個子表達式不能匹配的話,那麼引擎會恢複保存的匹配進程,然後繼續處理正則表達式的剩餘部分。
類似的,‹(?<!⋯)›是否定型逆序環視(negative lookbehind)。從正則表達式在目標文本到達位置向回看,逆序環視內的所有選擇分支都無法匹配的時候,否定型逆序環視才會匹配成功。
不同層次的逆序環視
順序環視用起來比較容易。本書中討論的所有正則流派都支持在順序環視中放入一個完整的正則表達式。在正則表達式中可以使用的任何符號都可以用於順序環視之內。你甚至可以在順序環視內嵌套其他順序環視和逆序環視分組。你的大腦可能需要多繞幾個彎,但是正則引擎會把這一切都處理得很好。
逆序環視的情況則不同。正則表達式軟件總是設計成按照從左向右的方式查找目標文本。向回查找的實現通常需要一些特殊的處理:正則引擎會判斷你在逆序環視中輸入了多少個字符,回退那麼多數量的字符,然後再在目標文本中從左向右匹配位於逆序環視中的文本。
基於這個原因,最早的實現中隻允許在逆序環視中包含固定長度的字麵文本。盡管Perl和Python仍需要逆序環視擁有固定的長度,但它們已經允許固定長度的正則表達式記號,如字符組,以及所有分支字符數都相同的選擇分支。
PCRE和Ruby 1.9則更進一步,允許逆序環視中使用不同分支長度的選擇分支,隻要各分支的長度是不變的。它們可以處理類似如下的正則式:‹(?<=one|two|three|forty- two|gr[ae]y)›,但是無法處理更為複雜的情況。
PCRE和Ruby 1.9在內部會把這個表達式擴展為6個逆序環視測試。首先,它們會回跳3個字符來測試‹one|two›,接著回跳4個字符來測試‹gray|grey›,然後回跳5個字符來測試‹three›,最後回跳9個字符測試‹forty-two›。
Java對於逆序環視則更進一步。Java允許在逆序環視中使用任意的有限長度的正則表達式。這意味著你可以使用除了無限長度量詞‹*›、‹+›和‹{42,}›之外的所有符號。Java的正則引擎在內部會計算在逆序環視中的正則表達式可能會匹配的文本的最小和最大長度。如果它匹配失敗的話,那麼引擎會多回退一個字符再試,直到逆序環視成功匹配或者嚐試過了最大字符數目。
似乎這些聽起來都不是很高效,事實上也正是如此。逆序環視用起來是非常方便的結構,但是它的速度就很一般了。稍後,我們會講解在根本不支持逆序環視的JavaScript和Ruby 1.8中的一個解決方案。這個解決方案實際上會比使用逆序環視的效率要高很多。
.NET框架中的正則表達式引擎是唯一可以實際上從右向左應用一個完整正則表達式的引擎1FF。.NET允許在逆序環視中使用任何符號,而且它會實際上從右向左來應用正則表達式。在逆序環視中的正則表達式和目標文本都是按照從右向左來進行掃描的。
匹配相同的文本兩次
如果在正則表達式的開始處使用逆序環視,或者在正則表達式的結尾處使用順序環視,其效果就是要求在正則匹配之前或者之後出現一些東西,但不要把它們包含到匹配中。如果在正則表達式的中間使用環視的話,就可以對同一段文本進行多次測試。
在實例2.3的“流派相關的特性”小節中,我們講解了如何使用字符組補集來匹配一個泰國語的數字。隻有在.NET和Java中才會支持字符組補集。
如果一個字符既是泰國語字符(任何類別),又是數字(任意字母表),那麼它就是一個泰國語數字。如果使用順序環視,你可以在同一個字符上檢查這兩個要求:
(?=\p{Thai})\p{N}
正則選項:無
正則流派:PCRE、Perl、Ruby 1.9
這個正則表達式隻能用於在實例2.7所講解的支持Unicode字母表的3種流派。但是使用順序環視來多次匹配同一個字符的思想則可以用於本書中討論的所有流派。
當正則引擎查找‹(?=\p{Thai})\p{N}›的時候,它首先會在開始進行匹配嚐試的字符串中的每一個位置進入順序環視。如果該位置的字符不在泰國語字母表(也就是說‹\p{Thai}›匹配失敗)中,那麼這次順序環視就會失敗。這也會導致整個匹配嚐試失敗,並迫使正則引擎到下一個字符處重新進行嚐試。
如果正則表達式遇到一個泰國語字符,‹\p{Thai}›成功匹配。因此,環視‹(?=\p{Thai})›也會匹配成功。當引擎退出環視的時候,它會恢複之前的匹配進程。在這個例子中,也就是在剛找到泰國語字符之前的長度為0的匹配。接下來要匹配的是‹\p{N}›。因為順序環視已經丟棄了它的匹配,因此‹\p{N}›會同‹\p{Thai}›已經匹配了的那個字符進行比較。如果該字符擁有Unicode屬性Number的話,那麼‹\p{N}›會匹配成功。因為‹\p{N}›並不在環視之內,所以它會真正匹配這個字符,同時我們也就找到了想要的泰國語數字。
環視是固化分組
當正則表達式引擎退出一個環視分組的時候,它會丟棄掉環視匹配的文本。因為該文本被丟棄了,所以由位於環視之內的選擇分支或者量詞所記住的任意回溯位置也都會被丟棄。這樣實際上就會把順序環視和逆序環視都變成了固化分組。實例2.15中詳細講解了固化分組的概念。
在絕大多數情形下,環視的原子特性是無關緊要的。一個環視隻是用來檢查位於環視中的正則表達式是匹配成功還是失敗的斷言。它可以通過多少種不同方式匹配並不重要,因為它不會消耗目標文本中的任何字符。
當你在順序環視(以及逆序環視,如果你的正則流派支持)之內使用捕獲分組的時候,它的固化特性才會產生意義。雖然順序環視不會消耗任何文本,但是正則引擎會記住文本中哪些部分被位於順序環視中的任何捕獲分組匹配了。如果順序環視位於正則表達式的結尾處,那麼實際上你的捕獲分組所匹配的文本是正則表達式自身沒有匹配的。如果這個順序環視位於正則表達式中間,那麼你的多個捕獲分組匹配到的目標文本可能會相重疊。
環視的固化特性唯一能改變整個正則表達式的匹配的情況是,你在環視之外使用一個反向引用來指向在環視之內所創建的捕獲分組。思考這個正則表達式:
(?=(\d+))\w+\1
正則選項:無
正則流派:.NET、Java、JavaScript、PCRE、Perl、Python、Ruby
乍一看,你可能會認為這個正則表達式能夠匹配123x12。‹\d+›會把12捕獲到第一個捕獲分組中,接著‹\w+›會匹配3x,最後‹\1›會再次匹配12。
但這不可能發生。正則表達式會進入環視及捕獲分組。貪心的‹\d+›會匹配123。這個匹配存儲到第一個捕獲分組中。引擎接著退出順序環視,把當前匹配重新設置為字符串的開始,並且丟棄由貪心的加號所記住的回溯位置,但是會在第一個捕獲分組中保留所存儲的123。
現在,貪心的‹\w+›會在字符串開始處進行嚐試。它會把123x12都吃掉。這時指向123的‹\1›在字符串結尾處匹配失敗。‹\w+›會回溯一個字符。‹\1›還是會失敗。‹\w+›會繼續回溯,直到它放棄了除了目標文本中第一個1之外的所有字符。‹\1›在第一個1之後還是會匹配失敗。
如果正則引擎能夠返回到順序環視中,放棄123而選擇12,那麼最後的12會匹配‹\1›。但是正則引擎並不會這樣做。
正則引擎此時並不存在可以選擇的任何回溯位置。‹\w+›已經回退到頭了,而環視迫使‹\d+›把它的回溯位置都丟掉了。因此匹配嚐試會宣告失敗。
代替逆序環視
<b>\K\w+(?=</b>)
正則選項:不區分大小寫
正則流派:PCRE 7.2、Perl 5.10
Perl 5.10、PCRE 7.2及更高版本,提供了使用‹\K›代替逆序環視的機製。當正則引擎在正則表達式中遇到‹\K›時,引擎會保持之前所匹配的文本。匹配嚐試會如不存在‹\K›一樣繼續。但‹\K›之前所匹配的文本並不會包含在整個匹配結果中。‹\K›之前的捕獲分組所保存的文本仍可以用於‹\K›之後的反向引用。隻有整個匹配結果受‹\K›影響。
結果是許多情況下都可以使用‹\K›代替肯定型逆序環視。與‹(?<=before)text›相同,‹before\Ktext›僅匹配緊跟在before之後的text。在Perl和PCRE中使用‹\K›而非肯定型逆序環視的好處是可以在‹\K›前使用完整的正則表達式語法,而逆序環視有各種限製,如不允許使用量詞。
‹\K›和逆序環視的主要區別是,使用‹\K›時,正則式嚴格地從左向右匹配。它永遠不會向回看,而逆序環視則會向回看。當‹\K›後麵或逆向環視後麵的匹配,與‹\K›前麵或逆向環視內匹配的文本相同時,這個區別就會帶來影響。
正則式‹(?<=a)a›在字符串aaa中可以找到2個匹配。在字符串開始處進行的第一次匹配嚐試會失敗,因為正則引擎無法在開始處之前找到一個a。在第一和第二個a之間的位置進行的匹配嚐試可以成功。正則引擎向回看找到字符串中第一個a滿足逆序環視條件,正則式中第二個‹a›匹配字符串中第二個a。在第二和第三個a之間的位置進行的匹配嚐試同樣會成功。正則引擎向回看找到字符串中第二個a滿足逆序環視條件,隨後正則式匹配第三個a。在字符串末尾進行的最後一次匹配嚐試失敗。向回看第三個a滿足逆序環視條件,但字符串中沒有更多字符可以匹配正則式第二個‹a›。
正則式‹a\Ka›隻能在同一字符串中找到1個匹配。在字符串開始處進行的第一次匹配嚐試成功。正則式中第一個‹a›匹配字符串中第一個a。‹\K›將這一部分匹配排除出最終返回的匹配結果,但並不改變當前匹配過程。隨後正則式中第二個‹a›匹配字符串中第二個a,作為最終返回的完整匹配結果。第二次匹配嚐試從字符串中第二和第三個a之間的位置開始。正則式中第一個‹a›匹配字符串中第三個a。‹\K›將這一匹配排除出最終匹配結果,正則引擎正常前進。不過字符串中已經沒有字符可供正則式中第二個‹a›匹配,於是匹配嚐試失敗。
由此可見,使用‹\K›時,正則式匹配過程正常進行。正則式‹a\Ka›找到的匹配與正則式‹a(a)›中捕獲分組的匹配相同。你不能用‹\K›多次匹配字符串中同一部分。而逆序環視則可以。可以用‹(?<=\p{Thai})(?<=\p{Nd})a›匹配一個緊跟在屬於泰語字母表和數字的單個字符之後的a。如果嚐試的是‹\p{Thai}\K\p{Nd}\Ka›,那匹配的則是一個泰語字符緊跟一個數字再緊跟一個a,即使隻返回a作為匹配結果。而這和使用‹\p{Thai}\p{Nd}(a)›匹配相同3個字符時,捕獲分組所返回的結果相一致。
不使用逆序環視的解決方案
雖然前麵講的這麼複雜,但是如果你用的是Ruby 1.8或者JavaScript,那麼這些對你都毫無用處,因為你根本就不能使用逆序環視。使用這兩種正則流派無法以上述方式解決前麵所給出的問題,但是你可以通過使用捕獲分組來解決需要逆序環視的問題。下麵給出的這個替代方案也可以在所有其他正則流派中使用:
(<b>)(\w+)(?=</b>)
正則選項:不區分大小寫
正則流派:.NET、Java、JavaScript、PCRE、Perl、Python、Ruby
作為逆序環視的替代,我們使用了一個捕獲分組來匹配起始標簽:‹< b >›。我們還把所需要的匹配部分,也就是‹\w+›,放到了另一個捕獲分組中。
當把這個正則表達式應用到My < b >cat</ b > is furry之上的時候,這個正則表達式的完整匹配會是< b >cat。第一個捕獲分組會保存< b >,而第二個會保存cat。
如果題目的要求是隻匹配cat(在兩個< b >標簽之間的單詞),即你隻想提取文本中的這部分內容的話,那麼可以通過隻保存第二個捕獲分組所匹配的文本,而不是整個正則表達式匹配的文本來達到這一目標。
如果要求是想要進行查找和替換,而隻替換在兩個標簽之間的單詞的話,那麼可以使用一個反向引用來指向第一個捕獲分組,把起始標簽重新添加到替代文本中。在這個例子中,實際上並不需要捕獲分組,因為起始標簽總是相同的。但是當它可變的時候,捕獲分組會重新插入與前麵匹配到的一模一樣的內容。實例2.21對此有更詳細的講解。
最後,如果你真的想要模擬逆序環視的話,可以使用兩個正則表達式來完成。首先,使用普通表達式,而不是不使用逆序環視,來查找你的正則表達式。當它匹配成功時,把在匹配部分前麵的目標文本子串複製到一個新的字符串變量中。然後用第二個正則表達式,加上字符串結束定位符(‹\z›或‹$›),進行你在逆序環視中所做的測試。這個定位符會確保第二個正則式的匹配一定位於該字符串的結尾。因為剪切字符串的地方是第一個正則表達式匹配的地方,所以這樣就會把第二個匹配剛好放到第一個匹配的左邊。
在JavaScript中,可以使用如下的代碼來完成這項任務:
var mainregexp = /\w+(?=<\/b>)/;
var lookbehind = /<b>$/;
if (match = mainregexp.exec("My <b>cat</b> is furry")) {
// Found a word before a closing tag </b>
var potentialmatch = match[0];
var leftContext = match.input.substring(0, match.index);
if (lookbehind.exec(leftContext)) {
// Lookbehind matched:
// potentialmatch occurs between a pair of <b> tags
} else {
// Lookbehind failed: potentialmatch is no good
}
} else {
// Unable to find a word before a closing tag </b>
}
最後更新:2017-06-02 19:35:54