按部就班掌握遞歸正則表達式

通常來講,遞歸的正則表達式用來匹配任意嵌套層次的結構或左右對稱的結構。例如匹配:正則表達式

((((()))))
(hello (world) good (boy) bye)
<p>hello world <strong>hello world</strong> </p>
abc.def.ghij...stu.vwx.yz
abcdcba
123454321

遞歸正則在正則表達式裏算是比較靈活的部分,換句話說就是可能會比較難。下面這個正則表達式是在網上流傳的很是普遍的遞歸正則的示例,它用來匹配嵌套任意次數的括號,括號內能夠有其它字符,好比能夠匹配(a(bc)de)(abc(bc(def)c)de)ruby

# 使用了x修飾符,忽略正則表達式內的空白符號
/\( ( (?>[^()]+) | (\g<0>) )* \)/x

這彷佛看不怎麼懂?其實即便知道了正則遞歸的方式,也仍是很難看懂(至少,我分析了好久)。bash

難懂的緣由大概是由於這裏使用的固化分組在多選分支|中屬於一個技巧性的寫法,並且分組外還使用了量詞*,這些結合起來就太難懂了。函數

正由於網上處處流傳這個例子,曾使我屢次對遞歸正則的學習望而卻步。這裏我也不去解釋這個遞歸正則的含義,由於"太學術化"或者說"太裝xyx逼",而通常遞歸正則徹底能夠寫的很簡單但卻能實現目標性能

如何寫出簡單易懂版本的遞歸正則而且理解遞歸正則的匹配方式,正是本文的目標。在後文,我介紹了一個更加簡單、更加容易理解的版本,一樣能實現這個遞歸匹配的需求。學習

爲了解釋清楚遞歸正則,本文會以按部就班的方式逐步深刻到遞歸正則的方方面面。因此,篇幅可能稍大,其中大量篇幅都用在瞭解釋分析遞歸正則是如何遞歸匹配上。測試

注:
本文以Ruby的正則表達式來介紹遞歸正則,但對其它支持遞歸正則的語言也是能通用的。例如Perl、PHP、Python(自帶的re不提供,但第三方庫regex提供遞歸正則)等。優化

理解反向引用\N和\g

首先經過正則表達式的反向引用的用法來逐步引入遞歸正則表達式的用法。3d

正則表達式(abc|def) and \1xyz能夠匹配字符串"abc and abcxyz"或"def and defxyz",可是不能匹配"abc and defxyz"或def and abcxyz。這是由於,反向引用在引用的時候,只能引用以前分組捕獲成功後的那個結果。調試

reg = /(abc|def) and \1xyz/

reg =~ "abc and abcxyz"  #=>0
reg =~ "def and defxyz"  #=>0
reg =~ "def and abcxyz"  #=>nil
reg =~ "abc and defxyz"  #=>nil

可是,若是使用\g<1>來代替\1,那麼就能匹配這四種情形的字符串(Perl中使用(?1)對應這裏的\g<1>):

reg = /(abc|def) and \g<1>xyz/

reg =~ "abc and abcxyz"  #=>0
reg =~ "def and defxyz"  #=>0
reg =~ "def and abcxyz"  #=>0
reg =~ "abc and defxyz"  #=>0

\g<1>\1的區別在於:\1在反向引用的時候,引用的是該分組捕獲到的結果值,\g<1>則不是反向引用,而是直接將索引號爲1的分組捕獲從新執行捕獲分組的匹配操做。至關因而/(abc|def) and (abc|def)xyz/

因此,\1至關因而在引用的位置插入索引號爲1的分組捕獲的結果,\g<1>至關因而在此處插入索引號爲1的分組捕獲表達式,讓其能再次進行分組表達式這部分的匹配操做。

若是把分組捕獲表達式看做是函數的定義,那麼開始匹配時表示調用該函數進行分組捕獲。而反向引用\N則是在引用位置處插入該函數的返回值,\g<name>則表示在此處再次調用該函數進行匹配。

\g<name>的name能夠是數值型的分組索引號,也能夠是命名捕獲的名稱索引,還能夠是0表示整個正則表達式自身。

/(abc|def) and \g<1>xyz/
/(?<var>abc|def) and \g<var>xyz/
/(abc|def) and \g<0>xyz/  # 錯誤正則,稍後分析

=begin
# Perl、Python(regex,非re)、PHP與之對應的方式:
\g<0>    -> (?R)或(?0)
\g<N>    -> (?N)
\g<name> -> (?P>name)或(?&name)
=end

前面兩種好理解,第三種使用\g<0>就不太能理解了,繼續向下看。

初探遞歸正則:遞歸正則匹配什麼

\g<0>表示正則表達式自身,因此這至關因而遞歸正則表達式,假如進行第一輪正則表達式替換的話,至關於:

/(abc|def) and (abc|def) and \g<0>xyzxyz/

固然,這裏只是爲了幫助理解纔將\g<0>替換成正則表達式,但它不會真的直接替換正則表達式的定義。就像函數調用時,不會在調用函數的地方替換成函數定義裏的代碼再去執行,函數定義了就能屢次複用。

無論怎樣,不難發現這裏已經出現了無限遞歸的可能性,由於替換一輪後的正則表達式中再次包含了\g<0>,它能夠再次進行第二輪替換、第三輪替換......

那麼,對於/(abc|def) and \g<0>xyz/這個遞歸的正則表達式來講,它能匹配什麼樣的字符串呢?這纔是理解正則遞歸時最須要關心的。

能夠將上面的\g<0>看做是一個佔位符,首先它能夠匹配"abc and _xyz"或者def and _xyz這種格式的字符串,這裏我用了_表示\g<0>佔位符。遞歸一輪的話,它能夠匹配"abc and def and _xyzxyz",這裏又會繼續遞歸下去,將沒完沒了。因此這裏先將該正則匹配什麼字符串的問題保留,稍後再回頭分析。

事實上,/(abc|def) and \g<0>xyz/是錯誤的正則表達式,它會提示咱們,遞歸沒有終點:

/(abc|def) and \g<0>xyz/
#=>SyntaxError: never ending recursion

因此,使用遞歸正則必需要保證遞歸可以有終點

保證正則遞歸的終點

怎麼保證遞歸正則的終點呢?只要給\g<>這部分作一個量詞的限定便可,好比:

\g<0>+        # 錯誤正則
\g<0>{3}      # 錯誤正則
\g<0>{,3}     # 錯誤正則

\g<0>*        # 正確正則
\g<0>?        # 正確正則
\g<0>{0}      # 正確正則
pat|\g<0>     # 正確正則
(\g<0>)*      # 正確正則
(\g<0>)?      # 正確正則
...

\g<0>+表示遞歸至少1輪,可是這裏已經錯了,由於遞歸屢次的時候,\g<0>這個佔位符及其量詞+將始終保留在最後一輪的結果中,因而致使無限遞歸。同理\g<0>{3}這種表示嚴格遞歸三次的方式也是錯誤的,由於遞歸第三次後仍然保留了\g<0>{3}佔位符及其量詞{3},這也將無限遞歸。

因此,只有\g<0>*\g<0>?\g<0>{0}pat|\g<0>等這種能在量詞數量選擇意義上表示遞歸0次的方式纔是正確的正則表達式語法,由於不管遞歸多少次,最後一次的佔位符的量詞均可以是0次,從而達到遞歸的終點,即中止遞歸。

因此,修改前面的正則表達式,假如使用?量詞修飾\g<>

/(abc|def) and \g<0>?xyz/

再探遞歸正則:遞歸正則匹配什麼

回到以前遺留的問題,如今這個正確的遞歸正則表達式/(abc|def) and \g<0>?xyz/能匹配什麼樣的字符串呢?

按照以前的分析,它能匹配的字符串的模式相似於abc and _?xyz或者def and _?xyz

若是量詞?取0次,那麼該遞歸正則匹配的是"abc and xyz"或"def and xyz":

reg = /(abc|def) and \g<0>?xyz/
reg =~ "abc and xyz"  #=> 0
reg =~ "def and xyz"  #=> 0

若是量詞?取1次,那麼該遞歸一輪後的正則模式爲abc and abc and _?xyzxyz,其中任何一個"abc"替換成"def"都是知足條件的。那麼這裏又有了\g<>量詞的次數選擇問題。

假如這裏量詞?取0次,也就是從開始到如今整體遞歸了一輪。那麼該遞歸正則匹配到是:

reg = /(abc|def) and \g<0>?xyz/
reg =~ "abc and abc and xyzxyz"  #=> 0
reg =~ "abc and def and xyzxyz"  #=> 0
reg =~ "def and def and xyzxyz"  #=> 0
reg =~ "def and abc and xyzxyz"  #=> 0

若是遞歸一輪後的量詞?繼續取1次呢?那麼下一輪遞歸仍將會有量詞次數選擇的問題。

至此,應該理解了遞歸正則的基本匹配方式。不過這裏使用的\g<0>遞歸還很基礎,下面將繼續逐步深刻。

深刻遞歸(1):括號分組內的\g

前面的遞歸示例中是將能表示遞歸的表達式\g<0>部分放在分組的外面,這種狀況下,只有\g<0>這種形式才能算是遞歸,若是是\g<1>\g<name>,就算不上是遞歸,充其量也就是個表達式的調用。

可是,當須要使用遞歸正則來解決問題的時候,遞歸表達式每每是在分組內部而不是在分組外部的。因此,前面解釋的遞歸方式其實很是少見。因而,要使用遞歸正則,還得繼續深刻探索。

首先看一個很是簡單的組內遞歸正則表達式:

/(abc\g<1>?xyz)+/

這個表達式中,進行了一個分組捕獲,這個分組首先匹配abc字符,而後在分組捕獲內使用了表達式\g<1>?(注意這個?是不能少的,固然?也能夠換成其它的前面解釋過的量詞),緊隨其後的是匹配字符xyz。因爲這裏的\g<1>?放在1號索引對應的分組捕獲的內部,因此就造成了一個遞歸的正則表達式。

問題是,這個正則表達式能匹配什麼樣的字符串呢?要學會遞歸正則表達式,必須會分析它可以匹配什麼類型的字符串。

仍然,以佔位符的方式來表示\g<1>,那麼該遞歸正則表達式匹配的字符串模式爲:"abc_?xyz" * N,這個* N表示重複N次,由於這種表達式的括號分組外面有一個+符號。

若是量詞?選擇爲0次,也就是不進行遞歸,則匹配字符串"abcxyz" * N

/(abc\g<1>?xyz)+/ =~ "abcxyz"  #=> 0

/(abc\g<1>?xyz)+/ =~ "abcxyzabcxyz"
#=> 0

/(abc\g<1>?xyz)+/ =~ "abcxyzabcxyzabcxyz"
#=> 0

/(abc\g<1>?xyz)+/ =~ "abcxyz" * 10
#=> 0

若是量詞?選擇爲1次,那麼進行一輪遞歸後,匹配的字符串模式爲:"abcabc_?xyzxyz" * N。再次進行?量詞的次數選擇,假如選0次,那麼匹配的字符串是"abcabcxyzxyz" * N

/(abc\g<1>?xyz)+/ =~ "abcabcxyzxyz" #=> 0
/(abc\g<1>?xyz)+/ =~ "abcabcxyzxyzabcabcxyzxyz"
#=> 0
/(abc\g<1>?xyz)+/ =~ "abcabcxyzxyz" * 3
#=> 0

再繼續分析一輪遞歸。假設這是?量詞選擇1次,那麼進行第二輪的遞歸,匹配的字符串模式爲:"abcabcabc_?xyzxyzxyz" * N

至此,應該不難推測出遞歸正則表達式/(abc\g<1>?xyz)+/匹配的字符串的模式:

"abcxyz" * N
"abcabcxyzxyz" * N
"abcabcabcxyzxyzxyz" * N
# 概括後,即匹配以下通用模式:n和N均大於等於1
("abc" * n + "xyz" * n) * N

將目光集中於剛纔的遞歸正則表達式/(abc\g<1>?xyz)+/,若是能經過這個正則表達式直接推測匹配何種類型字符串呢?

量詞+或其它可能的量詞先不看,先將焦點放在分組捕獲。這個分組捕獲匹配的是abc_?xyz,若是要進行遞歸N輪,那麼每一輪都是abc_?xyz這種模式,直接將其替換到該正則中去觀察:abc(abc_?xyz)*xyz,其中(abc_?xyz)*表示這部分重複0或N次。固然替換後的這部分不是標準的正則,只是爲了有助於理解纔將不一樣地方的概念混在一塊兒,我想並不會對你的理解形成歧義。

這樣理解起來就不難了。固然這個遞歸正則比較簡單,若是把上面的\g<1>?換成\g<1>*,看上去又會更復雜一點。那麼它匹配什麼樣的字符串呢?

一樣的分析方式,將/(abc\g<1>*xyz)+/看做是"abc_*xyz" * N的結構,而後對*取值,假設取值3次,因此遞歸後的結果看上去相似於:

"abc(abc_*xyz)(abc_*xyz)(abc_*xyz)xyz" * N

上面的每一個括號裏均可以對量詞*作選擇,但要到達遞歸的終點,最後(多是遞歸了好多輪後)每個遞歸裏的*都必須取值0次才能終結這個遞歸。

因此,假如如今這3個括號裏的每一個*都選擇0次,那麼匹配的字符串模式相似於:

"abc(abcxyz)(abcxyz)(abcxyz)xyz" * N

# 即等價於:n和N均大於等於1
( "abc" + "abcxyz" * n + "xyz" ) * N

例如:

/(abc\g<1>*xyz)+/ =~ ( "abc" + "abcxyz" * 1 + "xyz" ) * 1
#=> 0
/(abc\g<1>*xyz)+/ =~ ( "abc" + "abcxyz" * 1 + "xyz" ) * 2
#=> 0
/(abc\g<1>*xyz)+/ =~ ( "abc" + "abcxyz" * 4 + "xyz" ) * 2
#=> 0

假如上面三個括號裏第一個括號裏的*取值1次,後面兩個括號裏的*取值0次,那麼再次遞歸後,匹配的字符串模式相似於:

"abc(abc(abc_*xyz)xyz)(abcxyz)(abcxyz)xyz" * N

沒錯,又要作量詞的次數選擇。假如此次*取0次,那麼將終結本次遞歸匹配,它匹配的字符串模式爲:

"abc(abc(abcxyz)xyz)(abcxyz)(abcxyz)xyz" * N

那麼若是*不是按照上面的次數進行選擇的,那麼匹配的字符串模式是怎樣的?

沒有答案,惟一準確的答案就是迴歸這個正則表達式的含義:它匹配的字符串模式爲(abc\g<1>*xyz)+

深刻遞歸(2):寫遞歸正則(入門)

前面一直都是根據給定的遞歸正則表達式去分析能匹配什麼樣的字符串,這對於理解遞歸正則有所幫助。可是咱們更想要掌握的是如何根據字符串寫出遞歸的正則表達式。

通常來講,要使用遞歸正則去匹配,每每是要匹配嵌套的一些東西,若是不是匹配嵌套內容,極可能不會想到要去用遞歸正則。這裏,假設也要去匹配嵌套的東西。

先從簡單的嵌套開始。好比,如何匹配無限嵌套的空括號()(())((())),即"(" * n + ")" * n

分析一下。若是不遞歸的話,那就是匹配一對小括號(),因此這兩小括號字符必需要在分組內,即(\(\))。(若是使用\g<0>來遞歸的話,則能夠不用在分組內,不過這裏先不考慮這種狀況。)

按照前文屢次對遞歸正則表達式匹配何種字符串的分析,用佔位符替代要遞歸的話,要匹配的嵌套括號的字符串模式大概是這樣的:(_)。因此遞歸表達式\g<1>要在\(\)的中間,即(\(\g<1>\))

這裏還少了個量詞來保證遞歸的終點。那麼使用什麼樣的量詞呢?

使用\g<1>*確定沒問題,只要*號每次遞歸都只選擇量詞1次,而且最後一輪遞歸選擇0次終結遞歸便可,那麼匹配的模式是((_*))(((_*)))等等,這正好符合嵌套匹配。

/(\(\g<1>*\))/ =~ "(" * 1 + ")" * 1
#=> 0
/(\(\g<1>*\))/ =~ "(" * 3 + ")" * 3
#=> 0
/(\(\g<1>*\))/ =~ "(" * 10 + ")" * 10
#=> 0

看別人寫的遞歸正則,每每會在分組後加上*號量詞,即(\(\g<1>*\))*,針對於這種模式的嵌套,其實這個*是多餘的,它要匹配成功,這個量詞必須只能選0或1次。若是選擇多於1次,那麼匹配的字符串模式就變成了"((_*))" * N,更標準一點的表示方式是( "(" * n + ")" * n ) * N,固然,前面也說了,這還有無數種其餘的匹配可能。

因此,在這裏我不在分組的後面加*+這樣的量詞。要繼續剛纔的討論。

使用\g<1>?這種量詞方式能夠嗎?固然能夠,上面分析\g<1>*的時候,是說當每一輪遞歸時的*次數選擇都是1次或0次,就能匹配無限嵌套的小括號。對於\g<1>?來講固然也能夠,由於?也能夠表示0或1次。

/(\(\g<1>?\))/ =~ "(" * 1 + ")" * 1
#=> 0
/(\(\g<1>?\))/ =~ "(" * 3 + ")" * 3
#=> 0
/(\(\g<1>?\))/ =~ "(" * 10 + ")" * 10
#=> 0

這兩種遞歸正則表達式,都是符合要求的,都能匹配無限嵌套的小括號。

下面是命名捕獲版本的:

/(?<var>\(\g<var>?\))/ =~ "(" * 3 + ")" * 3
#=> 0

也能直接使用\g<0>做爲嵌套表達式,這時甚至能夠去掉分組:

/(?<var>\(\g<0>?\))/ =~ "(" * 3 + ")" * 3
#=> 0

# 去掉分組,直接遞歸這種自己
/\(\g<0>?\)/ =~ "(" * 3 + ")" * 3
#=> 0

這樣看上去,寫遞歸正則好像也不難。其實嵌套模式簡單的遞歸正則確實不難,只要理解遞歸的含義基本上就能寫出來。再看另外一個示例。

深刻遞歸(3):寫遞歸正則(進階)

假設要匹配的字符串模式爲:(abc(d(xy)e)fgh),其中每一個括號內的字符長度任意。這彷佛正是本文開頭所舉的例子。

這一個遞歸寫起來其實很是很是簡單:

# 爲了可讀性,使用了x修飾符忽略表達式內的空白符號
/\( [^()]* \g<0>* [^()]* \)/x

# 匹配:
reg = /\( [^()]* \g<0>* [^()]* \)/x
reg =~ "(abc(d(xy)e)fgh)"  #=> 0
reg =~ "(abc(d(xy)))"      #=> 0
reg =~ "((()e)fgh)"        #=> 0
reg =~ "((()))"            #=> 0

其中\([^()]*[^()]*\)是頭和尾,中間使用\g<0>來無限嵌套頭和尾。邏輯其實很簡單。

相比於網上流傳的版本/\( ( (?>[^()]+) | (\g<0>) )* \)/x,此處所給出的寫法應該容易理解的多。

再回頭擴充剛纔的遞歸匹配需求,若是須要匹配的字符串是ab(abc(d(xy)e)fgh)df這種模式呢?另外一個問題,這種字符串模式和(abc(d(xy)e)fgh)有什麼區別呢?

仔細比對一下,(abc(d(xy)e)fgh)按左右括號劃分配對的話,它左右恰好可以成對數:(abc (d (xy ) e) fgh)(這裏用一個空格分隔,從內向外互相成對)。但ab(abc(d(xy)e)fgh)df按左右括號劃分配對的話,獲得的是ab( abc( d( xy )e )fgh )df,顯然,它中間多了一層沒法成對的內容xy

爲了寫出按照這種成對劃分的遞歸表達式,先不考慮多出來沒法成對的xy這一層。那麼對應的遞歸正則表達式爲:

/[^()]* \( \g<0>* \) [^()]*/x

其中[^()]*\(是頭部,\)[^()]*是尾部,中間用\g<0>*實現頭尾成對的無限嵌套。

再來考慮中間多出來的沒法成對的xy這部分。其實直接將這部分放在\g<0>*的左邊或右邊都無所謂。例如:

# 放\g<0>*的左邊
/[^()]* \( [^()]* \g<0>* \) [^()]*/x

# 放\g<0>*的右邊
/[^()]* \( \g<0>* [^()]* \) [^()]*/x

沒錯,寫遞歸的正則表達式就是這麼簡單粗暴。

只是,現實並不這麼美好,上面將多餘的沒法配對的部分放在了遞歸表達式的左邊或右邊,但有時候這樣是不行的。

解決多餘沒法成對內容的更通用方法是使用二選一的分支結構,即|結合遞歸表達式一塊兒使用,參見下一小節。

深刻遞歸(4):遞歸結合二選一分支

要處理上面多出的沒法成對的數據,能夠經過二選一結構|改寫成以下更通用的方式:

/[^()]* \( \g<0>* \) [^()]* |./x

進行匹配測試:

reg = /[^()]* \( \g<0>* \) [^()]* |./x
reg =~ "ab(abc(d(xy)e)fgh)df"
#=> 0

當遞歸正則表達式結合了|提供的二選一分支功能時,|左邊或右邊(和\g<>相反的那一邊)均可以用來提供這些"孤兒"數據。

例如,上面示例中,當遞歸進行到發現xy這部分是多餘的時候將沒法繼續匹配,這時候將能夠從二選一的另外一個分支來匹配這個多餘的數據。

可是這個二選一分支帶來了一個新的問題:只要有沒法匹配的,均可以去另外一個分支匹配。假如右邊的分支是個.,這就至關於多了一個萬能箱,什麼均可以從這裏匹配。

但若是沒法匹配的多餘字符是右括號或左括號這個必須的字符呢?少了任何一個括號,都再也不算是成對的嵌套結構,但卻由於二選一分支而匹配成功。

如何解決這個問題?第一,須要保證另外一分支不是萬能的.;第二,需將整個結構作位置錨定。例如:

/\A ( [^()]* \( \g<1>* \) [^()]* | [^()] ) \Z/x

注意,上面加了括號分組,因此\g<0>隨之改變成\g<1>,由於遞歸的時候並不須要將錨定也包含進來。

固然,上面示例中二選一分支的另外一個分支所使用的是單字符匹配[^()],若是有多個連續的多餘字符,這會致使屢次選中該分支。爲了減小匹配的測試次數,能夠將其直接寫成[^()]*

/\A ( [^()]* \( \g<1>* \) [^()]* | [^()]* ) \Z/x

但這有可能會在匹配失敗的時候致使大量的回溯,從而性能暴降。例如,以下失敗的匹配:

reg = /\A([^()]* \( \g<1>* \) [^()]* | [^()]* )\Z/x

# 匹配失敗性能暴降
(st=Time.now) ; (reg =~ "ab(abc(d(xy)e)fghdf") ; (Time.now - st)
#=> 1.7730072
(st=Time.now) ; (reg =~ "ab(abc(d(xy)e)fghdffds") ; (Time.now - st)
#=> 47.5858051

# 匹配成功則無影響
(st=Time.now) ; (reg =~ "ab(abc(d(xy)e)fgh)df") ; (Time.now - st)
#=> 5.9e-06

從結果發現,就這麼短的字符串,第一個匹配失敗竟須要花費1.8秒,第二個字符串更誇張,僅僅只是多了3個字符,耗費的時間飆升到47秒。

解決方法有不少種,這裏提供兩種:一種是將*號直接移到分組外,這雖然並不等價,但並不影響最終的匹配結果;另外一種是將該多選分支使用固化分組或佔有優先的模式。

reg1 = /\A([^()]* \( \g<1>* \) [^()]* | [^()] )*\Z/x
reg2 = /\A([^()]* \( \g<1>* \) [^()]* | (?>[^()]*) )\Z/x

# 匹配成功
(st=Time.now) ; (reg1 =~ "ab(abc(d(xy)e)fgh)df") ; (Time.now - st)
#=> 6.1e-06
(st=Time.now) ; (reg2 =~ "ab(abc(d(xy)e)fgh)df") ; (Time.now - st)
#=> 5.8e-06

# 匹配失敗
(st=Time.now) ; (reg1 =~ "ab(abc(d(xy)e)fghdf") ; (Time.now - st)
#=> 8.46e-05
(st=Time.now) ; (reg2 =~ "ab(abc(d(xy)e)fghdf") ; (Time.now - st)
#=> 0.0004223

深刻遞歸(5):當心遞歸中的分組捕獲

在介紹示例以前,先驗證一下結論。

在遞歸過程當中,可能也會有分組捕獲的表達式,因此,遞歸正則設置的相關變量值是最後一次分組捕獲對應的狀態。例如:

reg = /(abc|def) and \g<0>?xyz/

# 只遞歸一輪
reg =~ "abc and def and xyzxyz"  #=> 0

# $~表示本次所匹配到的全部字符串
$~
#=> #<MatchData "abc and def and xyzxyz" 1:"def">

# $1表示第一個分組捕獲所對應的內容
$1   #=> "def"

上面結果能夠看出,在遞歸過程當中,最後一輪的遞歸操做(此處示例即第一輪遞歸)設置了一些正則匹配時的變量,它會覆蓋在它以前的遞歸設置的結果。

再來看一個示例。如今有個需求:匹配任何長度的迴文字符串(palindrome),好比123432一、abcba、好很差、abccba、好、好好、123321,該示例只能使用二選一的分支來實現。

這裏簡單分析一下,如何經過遞歸正則來實現該需求。

假設要匹配的這個字符串是abcdcba,先把多餘的字符d去掉,那麼要匹配的是abccba,這也是咱們想要匹配的一種字符串模式。首先,左右配對的部分必須是徹底一致的數據,這個遞歸正則其實很容易實現,用佔位符來描述,大概模式爲:(.)_*\1。將其替換成遞歸正則表達式:

/(.) \g<0>* \1/x

再來考慮多餘的那個字符,直接將其放在二選一分支的另外一分支便可:由於二選一分支,因此這裏的\g<0>就能夠不用量詞修飾來保證遞歸的終點

/(.) \g<0> \1 |./x

最後,加上位置錨定。

/\A ( (.) \g<1> \2|.) \Z/x

彷佛已經沒問題了,去測試匹配下:

/\A ( (.) \g<1> \2|.) \Z/x =~ "abcba"
#=> nil

結果卻並不如想象中那樣成功。

不過,這個正則表達式的邏輯確實是沒有問題的。例如,使用grep -P(使用PCRE)執行等價的正則去匹配迴文字符串。

$ grep -P "^((.)(?1)\2|.)$" <<<"abcdcba"
abcdcba

# 下面的則失敗
$ grep -P "^((.)(?1)\2|.)$" <<<"abcdcbad"

可是這個"正確的"正則表達式在Ruby中卻沒法達到目標。這是由於Ruby中的遞歸也會設置分組捕獲,每一個\2所反向引用的就再也不是每輪遞歸中同層次的分組捕獲(.)的內容了,而是真正的從左向右的第二個分組捕獲括號所捕獲的內容。

好在,Ruby提供了更加靈活的分組捕獲的引用控制。除了\N這種方式的反向引用,也能夠經過\k<N>\k<name>來引用,靈活之處在於\k<>支持遞歸層次的偏移,例如\k<name+0>表示取當前遞歸層次裏的name分組捕獲,\k<name+1>\k<name-1>分別表示取當前遞歸層的下一層和上一層裏的name分組捕獲。

因此,在Ruby中改一下這個正則表達式就能正常工做:

/\A ( (.) \g<1> \k<2+0>|.) \Z/x =~ "abcba"
#=> 0
/\A ( (.) \g<1> \k<2+0>|.) \Z/x =~ "abcbaa"
#=> nil

固然,用命名捕獲也是能夠的:

/\A (?<i> (?<j>.) \g<i> \k<j+0>|.) \Z/x

最後,能夠將上面的正則表達式改動一番。上面正則中,多選分支的.一直都是放在尾部的(放頭部也沒問題),但下面這種將多選分支和遞歸表達式嵌在一個分組內也是很常見的用法。下面這兩種遞歸正則表達式是等價的。

/\A (?<i> (?<j>.) \g<i>       \k<j+0>|.) \Z/x
/\A (?<i> (?<j>.) (?:\g<i>|.) \k<j+0> ) \Z/x

(?:\g<i>|.)進行了分捕獲的分組,分組將它們兩綁定在一個組內,若是不分組將會出錯,由於|的優先級過低。

不要濫用遞歸正則

雖然遞歸正則確實能解決一些特殊需求,可是能不用盡可能不用,由於遞歸正則要配合量詞來修飾遞歸表達式,這自己不是問題,可是遞歸表達式不少時候在分組內,而分組自己可能也會用量詞去修飾,這樣兩個量詞一結合,一不當心可能就出現大量的回溯,致使匹配效率瘋狂降低。

前文已經演示過一個這樣的現象,僅僅只是多了3個字符,匹配失敗居然須要多花費40多秒,並且隨着字符的增多,匹配失敗所需時間飆升的更快。這絕對是咱們要去避免的。

因此,當寫出來的遞歸正則表達式裏又是分組、又是量詞,看上去還"亂七八糟"的結合在一塊兒,極可能會出現性能不佳的問題。這時候可能須要去調試優化,以便寫出高性能的遞歸正則,但這可能會耗去大量的時間。

因此,儘可能想其它方法來解決遞歸正則想要實現的匹配需求,或者只寫看上去就很簡單的遞歸正則。

相關文章
相關標籤/搜索