讀古今文學網 > 精通正則表達式(第3版) > 遞歸的正則表達式 >

遞歸的正則表達式

Recursive Expressions

preg 引擎所屬的流派的大多數方面都在第3章中有所介紹,但是此流派還提供了某些新的有意思的功能用於匹配嵌套結構:遞歸的表達式。

序列「(?R)」表示「在此處遞歸應用整個表達式」,而「(?num)」表示「在此處遞歸應用 num所對應編號的捕獲型括號中的序列」。命名捕獲的括號則使用「(?P>name)」表示法。

下面幾節展示了常見的遞歸。遞歸在擴展的「tagged data」例子(☞481)中佔有重要地位。

匹配嵌套括號內的文本

Matching Text with Nested Parentheses

基本的遞歸的例子是匹配嵌套的括號內的文本,下面是一種辦法:「(?:[^()]++

這個表達式匹配任意多個雙多選分支結構。第1個多選分支「[^()]++」匹配除括號之外的任何字符。因為外面有「(?:…)*」,這個多選分支要求使用佔有優先的加號,避免「無休止匹配」(☞226)。

另一個多選分支「((?R))」才是問題的關鍵。它匹配一對括號,其中可以包括任何字符(只要括號的嵌套是格式正確的)。這裡的「之間」的部分是整個正則表達式希望匹配的內容,也就是我們可以通過「(?R)」直接遞歸使用整個正則表達式的原因。

這個表達式本身可以正常工作,但如果要添加任何字符,請務必小心,因為調用「(?R)」時添加的任何字符同樣會遞歸。

如果使用這個正則表達式來校驗一個括號配對不正確的字符串,你可能希望在兩端加上「^…$」來確保「整個字符串」。這是不對的,因為添加的行錨點會被遞歸應用到整個字符串之中,導致匹配失敗。

遞歸引用一組捕獲型括號

「(?R)」結構會遞歸引用整個正則表達式,但也可以使用「(?num)」結構引用到其中的子集。它遞歸引用編號為numth的捕獲型括號內的子表達式(注4)。如果用「(?num)」來思考,「(?0)」就等於「(?R)」。

我們可以使用這種部分遞歸因來解決前面那一節中出現的問題:在添加「^…$」之前,我們用一個捕獲型括號把正則表達式的主體部分圍起來,然後在以前使用「(?R)」的地方使用「(?1)」。添加捕獲型括號是為了讓「(?1)」能夠引用,你或許還記得,這就是上一節匹配嵌套括號的表達式。「^…$ 」加在這些括號之外,這樣我們就不會對它們進行遞歸調用:

正則表達式中下畫線的部分是第1組捕獲型括號,所以每次遇到「(?1)」都會重新應用。

下面PHP代碼中的正則表達式會報告$text中的括號能否配對:

通過命名捕獲進行遞歸引用

如果需要遞歸調用的自表達式處於命名捕獲(☞138)中,就可以使用「(?P>name)」進行遞歸引用,而不是之前的「(?num)」表示法。使用這個表示法,我們的例子就成了:

「^(?P<stuff> (?:[^]++|((?P>stuff)))*)$.」

這個表達式可能看起來很複雜,用模式修飾符x可以看得更清楚一點:

關於佔有優先量詞的補充

我會對表達式中的佔有優先量詞做最後的補充。如果外部的「(?:…)*」是佔有優先的,內部就不必使用「[^()]++」。為了阻止這個表達式進入無休止匹配,其中之一(或者是兩者)必須是佔有優先的。如果不能使用佔有優先量詞和固化分組(☞259),則需要去掉所有的量詞

這樣會降低效率,但是至少不會進入無法終止的匹配。要提高效率,可以使用第 6 章介紹的「消除循環」的技巧,得到「[^()]*(?:((?R))[^()]*)*」。

不能回溯到遞歸調用之內

No Backtracking Into Recursion

preg正則流派的遞歸語意的重要特性之一是,它會把遞歸結構匹配的所有內容當作固化分組括號匹配的內容(☞259)。也就是說,遞歸結構不會部分「交還(unmatch)」某些已經匹配的內容來實現全局匹配(而是導致整個匹配失敗)。

這裡說的「部分」是很重要的,因為回溯時,遞歸調用匹配的所有文本可以作為一個整體來交還。但不容許回溯到遞歸調用之內的某個狀態。

匹配一組嵌套的括號

Matching a Set of Nested Parentheses

上文已經介紹過如何匹配包含規範配對括號的行,這裡我會介紹匹配對稱括號的辦法(其中可能包含更多的嵌套括號):「((?:[^()]++|(?R))*)」。

這個表達式的組成部分與之前的一樣,但是順序安排略有不同。同樣,如果你希望把它當作大的正則表達式的一部分,應該把它包括在捕獲型括號中,並且修改「(?R)」來遞歸地引用對應的自表達式,例如「(?1)」(必須使用這組捕獲型括號對應的編號)。