快速入門
教學指南
工具和語言
範例
參考
書籍評論
Regex 教學指南
簡介
目錄
特殊字元
不可列印字元
Regex 引擎內部
字元類別
字元類別減法
字元類別交集
簡寫字元類別
錨定
字詞邊界
交替
選用項目
重複
群組和擷取
反向參照
反向參照,第 2 部分
命名群組
相對反向參照
分支重設群組
自由間距和註解
Unicode
模式修改器
原子群組
獨佔量詞
前瞻和後顧
環顧,第 2 部分
將文字保留在比對之外
條件
平衡群組
遞迴
子常式
無限遞迴
遞迴和量詞
遞迴和擷取
遞迴和反向參照
遞迴和回溯
POSIX 方括號表示式
零長度比對
持續比對
本網站的其他內容
簡介
正規表示式快速入門
正規表示式教學指南
替換字串教學指南
應用程式和語言
正規表示式範例
正規表示式參考
替換字串參考
書籍評論
可列印 PDF
關於本網站
RSS Feed 和部落格
RegexBuddy—Better than a regular expression tutorial!

前瞻和後顧零長度斷言

前瞻和後顧,合稱為「環顧」,是零長度斷言,就像本教學指南稍早說明的 行首和行尾,以及 字首和字尾 錨定。不同的是,環顧實際上會比對字元,但接著放棄比對,只傳回結果:比對或不比對。這就是它們稱為「斷言」的原因。它們不會消耗字串中的字元,只會斷言是否可能比對。環顧可讓您建立沒有它們就無法建立,或沒有它們會變得非常冗長的正規表示式。

正向和負向前瞻

如果你想比對某個東西後面沒有接其他東西,負向前瞻是不可或缺的。在解釋字元類別時,本教學說明了為什麼你無法使用否定的字元類別來比對一個q後面沒有接u。負向前瞻提供了解決方案:q(?!u)。負向前瞻結構是一對括號,開括號後接一個問號和一個驚嘆號。在這個前瞻中,我們有一個平凡的正規表示式u

正向前瞻的作用方式也一樣。q(?=u)比對一個後面接u的q,但不會讓u成為比對的一部分。正向前瞻結構是一對括號,開括號後接一個問號和一個等號。

你可以在前瞻中使用任何正規表示式(但不能使用後瞻,如下所述)。任何有效的正規表示式都可以在前瞻中使用。如果它包含擷取群組,那麼這些群組將會像平常一樣擷取,而對它們的反向參照也會正常運作,即使是在前瞻之外。(唯一的例外是Tcl,它將前瞻中的所有群組都視為非擷取群組。)前瞻本身不是一個擷取群組。它不包含在反向參照編號的計數中。如果你想儲存前瞻中正規表示式的比對結果,你必須在前瞻中的正規表示式周圍加上擷取括號,如下所示:(?=(regex))。反過來的方式不會奏效,因為在擷取群組儲存其比對結果時,前瞻已經捨棄了正規表示式的比對結果。

Regex 引擎內部

首先,讓我們看看引擎如何將 q(?!u) 套用至字串 Iraq。正規表示式中的第一個記號是 字面值 q。正如我們所知,這會讓引擎在字串中尋找,直到找到字串中的 q 為止。字串中的位置現在是字串後的空白。下一個記號是前瞻。引擎現在會注意到它在一個前瞻結構中,並開始比對前瞻中的正規表示式。因此,下一個記號是 u。這不會比對到字串後的空白。引擎會注意到前瞻中的正規表示式失敗了。由於前瞻是負面的,這表示前瞻已在目前位置成功比對。此時,整個正規表示式已比對完成,而 q 會作為比對結果傳回。

讓我們嘗試將相同的正規表示式套用至 quitq 比對到 q。下一個記號是前瞻中的 u。下一個字元是 u。這些會比對。引擎會進到下一個字元:i。然而,它已完成前瞻中的正規表示式。引擎會注意到成功,並捨棄正規表示式比對。這會讓引擎在字串中退回到 u

由於前瞻是負面的,因此前瞻中的成功比對會導致前瞻失敗。由於這個正規表示式沒有其他排列組合,因此引擎必須從頭開始。由於 q 無法在其他任何地方比對,因此引擎會回報失敗。

讓我們再深入了解一次,以確保你了解前瞻的含義。讓我們將 q(?=u)i 套用至 quit。前瞻現在是正面的,並接著另一個記號。同樣地,q 比對到 q,而 u 比對到 u。同樣地,必須捨棄前瞻的比對,因此引擎會從字串中的 i 退回到 u。前瞻成功了,因此引擎會繼續進行 i。但 i 無法比對到 u。因此,這個比對嘗試失敗了。所有剩下的嘗試也會失敗,因為字串中沒有更多 q 了。

正規表示法 q(?=u)i 永遠無法配對到任何東西。它嘗試在同一個位置配對 ui。如果在 q 之後緊接著一個 u,那麼先行斷言就會成功,但接著 i 無法配對到 u。如果在 q 之後緊接著的不是 u,那麼先行斷言就會失敗。

正向和負向後行斷言

後行斷言有相同的效果,但往後運作。它告訴正規表示法引擎暫時往字串中往後走,檢查後行斷言中的文字是否可以在那裡配對到。使用負向後行斷言,(?<!a)b 會配對到一個沒有「a」在前面的「b」。它不會配對到 cab,但會配對到 beddebt 中的 b(而且只會配對到 b)。正向後行斷言 (?<=a)b 會配對到 cab 中的 b(而且只會配對到 b),但不會配對到 beddebt

正向後行斷言的結構是 (?<=文字):一對括號,開括號後面接著一個問號、「小於」符號和一個等號。負向後行斷言寫成 (?<!文字),使用驚嘆號取代等號。

更多正規表示法引擎內部結構

讓我們將 (?<=a)b 套用到 thingamabob。引擎從後行斷言和字串中的第一個字元開始。在這個情況中,後行斷言告訴引擎往後走一個字元,看看 a 是否可以在那裡配對到。引擎無法往後走一個字元,因為在 t 之前沒有任何字元。所以後行斷言失敗,而且引擎從下一個字元 h 重新開始。(請注意,負向後行斷言會在這裡成功。)引擎再次暫時往後走一個字元,檢查是否可以在那裡找到一個「a」。它找到一個 t,所以正向後行斷言再次失敗。

後行斷言會持續失敗,直到正規表示法到達字串中的 m。引擎再次往後走一個字元,並注意到 a 可以在那裡配對到。正向後行斷言配對成功。因為它長度為零,所以字串中的目前位置仍然停留在 m。下一個代碼是 b,它無法在此處配對。下一個字元是字串中的第二個 a。引擎往後走,並發現 m 無法配對到 a

下一個字元是字串中的第一個 b。引擎會往回尋找,並找出 a 符合回溯。 b 符合 b,且整個正規表示式已成功符合。它符合一個字元:字串中的第一個 b

關於回溯的重要注意事項

好消息是,您可以在正規表示式的任何地方使用回溯,不只在開頭。如果您想尋找一個不以「s」結尾的字詞,您可以使用 \b\w+(?<!s)\b。這絕對不同於 \b\w+[^s]\b。當套用於 John's 時,前者符合 John,而後者符合 John'(包括撇號)。我會讓您自己找出原因。(提示:\b 符合撇號和 s 之間)。後者也不符合單字元字詞,例如「a」或「I」。不使用回溯的正確正規表示式為 \b\w*[^s\W]\b(星號取代加號,且在字元類別中使用 \W)。我個人認為回溯較容易理解。最後一個正規表示式運作正常,具有雙重否定(否定字元類別中的 \W)。雙重否定往往會讓人類感到困惑。不過,正規表示式引擎不會。但 Tcl 除外,Tcl 會將否定字元類別中的否定簡寫視為錯誤。)

壞消息是,大多數正規表示式風格不允許您在回溯中使用任何正規表示式,因為它們無法反向套用正規表示式。正規表示式引擎需要能夠找出在檢查回溯之前要往回尋找多少個字元。在評估回溯時,正規表示式引擎會決定回溯中正規表示式的長度,在主旨字串中往回尋找那麼多個字元,然後從左到右套用回溯中的正規表示式,就像使用一般正規表示式一樣。

包括 PerlPythonBoost 所使用的許多 regex 風格都只允許固定長度的字串。你可以使用 文字字元跳脫Unicode 跳脫(除了 \X)和 字元類別。你無法使用 量詞反向參照。你可以使用 交替,但前提是所有選項都具有相同的長度。這些風格會先在主旨字串中向後移動與後向參照相同數量的字元,然後從左至右嘗試後向參照內的 regex。

Perl 5.30 支援變動長度的後向參照作為實驗功能。但有許多情況下它無法正確運作。因此在實際上,上述內容對於 Perl 5.30 仍然成立。

PCRE 在後向參照方面並非完全相容於 Perl。雖然 Perl 要求後向參照內的選項具有相同的長度,但 PCRE 允許變動長度的選項。 PHPDelphiRRuby 也允許這樣做。每個選項仍然必須是固定長度。每個選項都視為一個獨立的固定長度後向參照。

Java 更進一步,允許有限重複。你可以使用 問號大括號,並指定 最大值 參數。Java 會判斷後向參照可能的最小和最大長度。regex (?<!ab{2,4}c{3,5}d)test 中的後向參照有 5 種可能的長度。它的長度可以從 7 到 11 個字元。當 Java(版本 6 或更新版本)嘗試比對後向參照時,它會先在字串中向後移動最少數量的字元(在此範例中為 7),然後從左至右評估後向參照內的 regex。如果失敗,Java 會再向後移動一個字元並重試。如果後向參照持續失敗,Java 會繼續向後移動,直到後向參照比對成功或它已向後移動最大數量的字元(在此範例中為 11)。當後向參照可能長度的數量增加時,這種重複向後移動主旨字串的行為會降低效能。請記住這一點。不要選擇任意大的最大重複次數來解決後向參照內缺乏無限量詞的問題。Java 4 和 5 有錯誤,導致在某些情況下應該成功時,帶有交替或變動量詞的後向參照會失敗。這些錯誤已在 Java 6 中修正。

Java 13 允許您在後向參考中使用 星號加號,以及沒有上限的 大括號。但 Java 13 仍使用 Java 6 引入的後向參考比對方法。此外,如果 Java 13 中有多個量詞,其中一個沒有限制,它也無法正確處理後向參考。在某些情況下,您可能會收到錯誤訊息。在其他情況下,您可能會得到不正確的比對結果。因此,為了正確性和效能,我們建議您在 Java 6 到 13 中只在後向參考中使用上限較低的量詞。

唯一允許您在後向參考中使用完整正規表示式的正規表示式引擎,包括無限重複和反向參考,是 JGsoft 引擎.NET RegEx 類別。這些正規表示式引擎會從後往前套用後向參考中的正規表示式,從右到左逐一比對後向參考中的正規表示式和主旨字串。無論後向參考有多少不同的可能長度,它們只需要評估一次。

最後,像 std::regexTcl 等版本完全不支援後向參考,即使它們支援前向參考。自推出以來,JavaScript 一直都是如此。但現在後向參考已成為 ECMAScript 2018 規格的一部分。截至撰寫本文時(2019 年底),Google 的 Chrome 瀏覽器是唯一支援後向參考的熱門 JavaScript 實作。因此,如果跨瀏覽器相容性很重要,您無法在 JavaScript 中使用後向參考。

環顧斷言是原子性的

環顧斷言長度為零的事實自動使其成為 原子性的。一旦滿足環顧斷言條件,正規表示式引擎就會忘記環顧斷言中的所有內容。它不會在環顧斷言中回溯以嘗試不同的排列組合。

唯一會造成任何差異的情況是,當您在環顧斷言中使用 擷取群組 時。由於正規表示式引擎不會回溯到環顧斷言中,因此它不會嘗試擷取群組的不同排列組合。

因此,正規表示式 (?=(\d+))\w+\1 永遠不會比對 123x12。首先,環顧斷言將 123 擷取到 \1 中。然後,\w+ 比對整個字串並回溯,直到它只比對 1。最後,\w+ 失敗,因為 \1 無法在任何位置比對。現在,正規表示式引擎沒有任何可回溯的內容,因此整體正規表示式失敗。\d+ 建立的回溯步驟已被捨棄。它永遠不會到達前向參考只擷取 12 的地步。

顯然,正規表示式引擎會嘗試字串中的其他位置。如果我們變更主旨字串,正規表示式 (?=(\d+))\w+\1 會在 456x56 中比對到 56x56

如果您不使用環顧中的擷取群組,那麼這一切都不重要。環顧條件可以滿足或無法滿足。它可以滿足多少種方式並不相關。