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

使用平衡群組比對巢狀結構

.NET Regex 風格具備一項稱為平衡群組的特殊功能。平衡群組的主要目的是比對平衡結構或巢狀結構,這也是它們名稱的由來。技術上更精確的名稱應該是擷取群組減法。這正是此功能的實際作用。這是 .NET 解決 PerlPCRERuby 等其他 Regex 風格使用 正規表示式遞迴 處理的問題的方法。JGsoft V2 同時支援平衡群組和遞迴。

(?<capture-subtract>regex)(?'capture-subtract'regex) 是平衡組的基本語法。它與 .NET 中用於 命名擷取組 的語法相同,但組名之間以減號分隔。此組的名稱為「capture」。您可以省略組的名稱。(?<-subtract>regex)(?'-subtract'regex) 是非擷取平衡組的語法。

「subtract」的名稱必須是正規表示法中另一個組的名稱。當正規表示法引擎進入平衡組時,它會從組「subtract」中減去一個匹配項。如果組「subtract」尚未匹配,或其所有匹配項已減去,則平衡組無法匹配。您可以將平衡組視為測試組「subtract」的 條件,其中「regex」為「if」部分,而「else」部分永遠無法匹配。不同之處在於,平衡組具有從組「subtract」中減去一個匹配項的附加功能,而條件則不會變更組。

如果平衡組成功且它有一個名稱(此範例中為「capture」),則組會擷取從從組「subtract」中減去的匹配項結束處到平衡組本身匹配項開始處之間的文字(此範例中為「regex」)。

這之所以可以在 .NET 中運作,是因為 .NET 中的擷取組會保留在匹配過程中擷取的所有內容的堆疊,這些內容不會回溯或減去。大多數其他正規表示法引擎只會儲存每個擷取組的最新匹配項。當 (\w)+ 匹配 abc 時,Match.Groups[1].Value 會傳回 c,就像其他正規表示法引擎一樣,但 Match.Groups[1].Captures 會儲存組的所有三個反覆運算:abc

深入了解正規表示法引擎

讓我們將正規表示法 (?'open'o)+(?'between-open'c)+ 套用至字串 ooccc(?'open'o) 匹配第一個 o,並將其儲存為組「open」的第一個擷取項。量詞 + 會重複組。(?'open'o) 匹配第二個 o,並將其儲存為第二個擷取項。再次重複時,(?'open'o) 無法匹配第一個 c。但 + 對重複兩次感到滿意。

正規表示法引擎進展到 (?'between-open'c)。在引擎進入此平衡組之前,它必須檢查減去的組「open」是否已擷取某些內容。它已擷取第二個 o。引擎進入組,從「open」中減去最近的擷取項。這會讓組「open」只剩下第一個 o 作為其唯一的擷取項。現在在平衡組內部,c 匹配 c。引擎退出平衡組。組「between」會擷取從從「open」中減去的匹配項(第二個 o)和平衡組剛才匹配的 c 之間的文字。這是一個空字串,但它還是會被擷取。

平衡群組也有 + 作為量詞。引擎再次發現減去的群組「open」擷取到一些內容,即第一個 o。正規表示式進入平衡群組,讓群組「open」沒有任何配對。 c 配對字串中的第二個 c。群組「between」擷取 oc,也就是從「open」減去的配對(第一個 o)和平衡群組剛剛配對到的第二個 c 之間的文字。

平衡群組再次重複。但這次,正規表示式引擎發現群組「open」沒有配對了。平衡群組配對失敗。群組「between」不受影響,保留其最近一次的擷取。

+ 對兩次反覆運算感到滿意。引擎已到達正規表示式的結尾。它傳回 oocc 作為整體配對。 Match.Groups['open'].Success 會傳回 false,因為該群組的所有擷取都已減去。 Match.Groups['between'].Value 傳回 "oc"

配對平衡對

如果我們要配對平衡數量的 o 和 c,就需要修改這個正規表示式。為了確保正規表示式不會配對 ooccc(c 比 o 多),我們可以加入 錨點^(?'open'o)+(?'-open'c)+$。這個正規表示式經歷與前一個相同的配對程序。但在 (?'-open'c)+ 無法配對其第三次反覆運算後,引擎到達 $,而不是正規表示式的結尾。這無法配對。正規表示式引擎會回溯,嘗試量詞的不同排列組合,但它們都無法配對。找不到任何配對。

但正規表示式 ^(?'open'o)+(?'-open'c)+$ 仍然配對 ooc。配對程序再次相同,直到平衡群組配對到第一個 c,並讓群組「open」只有一個擷取,即第一個 o。量詞讓引擎再次嘗試平衡群組。引擎再次發現減去的群組「open」擷取到一些內容。正規表示式進入平衡群組,讓群組「open」沒有任何配對。但現在, c 無法配對,因為正規表示式引擎已到達字串的結尾。

正規表示式引擎現在必須回溯出平衡組。.NET 在回溯平衡組時,也會回溯減法。由於在進入平衡組時,第一個 o 的擷取已從「open」中減去,因此現在在回溯出平衡組時,會還原此擷取。重複組 (?'-open'c)+ 現在已減少為單一反覆。但量詞對此沒有問題,因為 + 表示「一次或多次」,就像它永遠做的那樣。正規表示式引擎仍位於字串結尾,到達正規表示式中的 $,並匹配。整個字串 ooc 會作為整體匹配項傳回。Match.Groups['open'].Captures 會將字串中的第一個 o 保存在 CaptureCollection 中的唯一項目。這是因為在回溯後,第二個 o 已從組中減去,但第一個 o 沒有。

為了確保正規表示式匹配 ocoocc,但不匹配 ooc,我們需要檢查在匹配程序到達正規表示式結尾時,組「open」沒有任何擷取剩餘。我們可以使用 條件式 來執行此操作。(?(open)(?!)) 是條件式,用於檢查組「open」是否匹配某個項目。在 .NET 中,已匹配某個項目表示堆疊中仍有未回溯或減去的擷取。如果組已擷取某個項目,則會評估條件式的「if」部分。在本例中,這是空的負向先行斷言 (?!)。此先行斷言內的空字串永遠會匹配。由於先行斷言為負向,因此這會導致先行斷言永遠失敗。因此,如果組已擷取某個項目,條件式永遠會失敗。如果組尚未擷取任何項目,則會評估條件式的「else」部分。在本例中,沒有「else」部分。這表示如果組尚未擷取任何項目,條件式永遠會成功。這使得 (?(open)(?!)) 成為驗證組「open」沒有任何擷取剩餘的適當測試。

正規表示式 ^(?'open'o)+(?'-open'c)+(?(open)(?!))$ 無法匹配 ooc。當 c 無法匹配,因為正規表示式引擎已到達字串結尾時,引擎會回溯出平衡組,留下「open」與單一擷取。正規表示式引擎現在到達條件式,但無法匹配。正規表示式引擎會回溯,嘗試量詞的不同排列,但它們都無法匹配。找不到任何匹配項。

正規表示式 ^(?'open'o)+(?'-open'c)+(?(open)(?!))$ 會比對 oocc。在 (?'-open'c)+ 比對 cc 之後,正規表示式引擎無法第三次進入平衡群組,因為「open」沒有任何擷取。引擎會進到條件式。條件式會成功,因為「open」沒有任何擷取,而條件式沒有「else」部分。現在 $ 會在字串結尾比對。

比對平衡結構

^(?:(?'open'o)+(?'-open'c)+)+(?(open)(?!))$ 會將擷取群組和平衡群組包在 非擷取群組 中,而這個群組也會重複。這個正規表示式會比對任何像 ooocooccocccoc 的字串,其中包含任何數量的完美平衡 o 和 c,以及任何數量的成對序列,巢狀到任何深度。平衡群組會確保正規表示式永遠不會比對在字串中任何一點有比其左邊 o 更多 c 的字串。結尾的條件式(必須在重複群組之外)會確保正規表示式永遠不會比對 o 多於 c 的字串。

^(?>(?'open'o)+(?'-open'c)+)+(?(open)(?!))$ 會使用 原子群組 來最佳化前一個正規表示式,而不是使用非擷取群組。原子群組(也是非擷取的)會消除正規表示式找不到比對時幾乎所有的回溯,這可以在使用在 o 和 c 很多但結尾沒有適當平衡的長字串時大幅提升效能。原子群組不會改變正規表示式比對有平衡 o 和 c 的字串的方式。

^m*(?>(?>(?'open'o)m*)+(?>(?'-open'c)m*)+)+(?(open)(?!))$ 允許字串中任何位置出現任意數量的字母 m,同時仍要求所有 o 和 c 平衡。 m* 出現在正規表示式的開頭,允許在第一個 o 之前出現任意數量的 m。 (?'open'o)+ 已變更為 (?>(?'open'o)m*)+ 以允許每個 o 之後出現任意數量的 m。類似地, (?'-open'c)+ 已變更為 (?>(?'-open'c)m*)+ 以允許每個 c 之後出現任意數量的 m。

這是使用 .NET 的平衡群組或擷取群組減法功能來比對平衡結構的通用解決方案。您可以用任何正規表示式取代 omc,只要這三個中的任何兩個都無法比對相同的文字即可。

^[^()]*(?>(?>(?'open'\()[^()]*)+(?>(?'-open'\))[^()]*)+)+(?(open)(?!))$ 套用此技術來比對所有括號都完美平衡的字串。

反向參照到減去的群組

您可以使用 反向參照 到群組,而這些群組的比對結果會被平衡群組減去。反向參照會比對群組最近一次的比對結果,而該結果未被回溯或減去。正規表示式 (?'x'[ab]){2}(?'-x')\k'x' 比對 aaaabababbbb。它不比對 aababbbaabba。字串的第一個和第三個字母必須相同。

讓我們看看 (?'x'[ab]){2}(?'-x')\k'x' 如何比對 aba。第一次迭代 (?'x'[ab]) 擷取 a。第二次迭代擷取 b。現在正規表示式引擎抵達平衡群組 (?'-x')。它檢查群組「x」是否已比對,而它已比對。引擎進入平衡群組,從群組「x」的堆疊中減去比對 b。平衡群組內沒有正規表示式代碼。它比對而不前進字串。現在正規表示式引擎抵達反向參照 \k'x'。群組「x」堆疊頂端的比對是 a。字串中的下一個字元也是反向參照比對的 a。找到 aba 作為整體比對。

當你將這個正規表示式套用至 abb 時,比對程序相同,除了反向參照無法比對字串中的第二個 b。由於正規表示式沒有其他正規表示式引擎可以嘗試的排列組合,因此比對嘗試失敗。

比對迴文

^(?'letter'[a-z])+[a-z]?(?:\k'letter'(?'-letter'))+(?(letter)(?!))$ 比對任何長度的迴文詞彙。這個正規表示式利用反向參照和擷取群組減法搭配良好的事實。它也使用上一個區段中的正規表示式作為空平衡群組。

讓我們看看這個正規表示式如何比對迴文 radar^ 比對字串開頭。然後 (?'letter'[a-z])+ 迭代五次。群組「letter」最終在堆疊中得到五個比對:radar。正規表示式引擎現在在字串結尾和正規表示式中的 [a-z]?。它沒有比對,但沒關係,因為量詞讓它變成可選的。引擎現在抵達反向參照 \k'letter'。群組「letter」在堆疊頂端有 r。這無法比對字串結尾後的空值。

正規表示式引擎會回溯。(?'letter'[a-z])+ 減少到四次迭代,留下 rada 在「letter」群組的堆疊中。[a-z]? 符合 r。反向參照再次無法符合字串結束後的空值。引擎會回溯,強制 [a-z]? 放棄其符合。現在「letter」在堆疊頂端有 a。這會導致反向參照無法符合 r

接著進行更多回溯。(?'letter'[a-z])+ 減少到三次迭代,留下 d 在「letter」群組的堆疊頂端。引擎再次使用 [a-z]?。它再次失敗,因為沒有 d 供反向參照符合。

再次回溯,群組「letter」的擷取堆疊減少到 ra。現在情況逆轉。[a-z]? 符合 d。反向參照符合 a,這是群組「letter」最近一次符合且未回溯的符合。引擎現在到達空的平衡群組 (?'-letter')。這符合,因為群組「letter」有一個符合 a 要減去。

反向參照和平衡群組在重複的非擷取群組內,因此引擎會再次嘗試它們。反向參照符合 r,平衡群組從「letter」的堆疊中減去它,讓擷取群組沒有任何符合。再次迭代,反向參照失敗,因為群組「letter」在堆疊中沒有任何符合。這讓群組作為非參與群組。在 .NET 中,對非參與群組的反向參照總是會失敗,就像在大部分正規表示式風格中一樣。

(?:\k'letter'(?'-letter'))+ 已成功符合兩次迭代。現在,條件 (?(letter)(?!)) 成功,因為群組「letter」沒有任何符合。錨定 $ 也符合。迴文 radar 已符合。