當心別落入正則回溯陷阱

不知才哪兒看來的:javascript

若是你有一個問題,你想到能夠用正則來解決,那麼你有兩個問題了。html

 

回溯

 對於正則而言,回溯並非必需的,這跟具體的正則引擎有關。簡單地說,正則引擎分爲NFA和DFA。這東西難懂且無聊,我就挑重點說。DFA(肯定型有窮自動機),從匹配文本入手,從左到右,每一個字符不會匹配兩次,它的時間複雜度是多項式的,因此一般狀況下,它的速度更快,但支持的特性不多,不支持捕獲組、各類引用等等;而NFA(非肯定型有窮自動機)則是從正則表達式入手,不斷讀入字符,嘗試是否匹配當前正則,不匹配則吐出字符從新嘗試,一般它的速度比較慢,最優時間複雜度爲多項式的,最差狀況爲指數級的。但NFA支持更多的特性,於是絕大多數編程場景下(包括js),咱們面對的是NFA。java

NFA匹配的過程就是吃入字符,嘗試匹配,若是經過,再吃入嘗試;若是不經過,就吐出,回到上一個狀態,由於同一個字符串在正則中可能存在一種狀態不一樣轉化路徑,這時正則引擎換一個轉化狀態進行嘗試,若是經過,繼續吃入字符,不然繼續吐出字符,回到再上一個狀態。這種嘗試不成功就返回上一狀態的過程,咱們稱爲回溯。正則匹配的性能好壞,就看回溯的狀況,回溯越多,性能越差。正則表達式

爲了說清楚這個問題,咱們作一個實驗,用express

a(acd|bsc|bcd)

 這個正則來對「abcd」這個字符串進行匹配。編程

QQ20150601221148

截圖上方是正則表達式,右側是要匹配的文本,左側是匹配的過程。c#

能夠看到,匹配這4個字母花了8步,分別看看這8步都在幹什麼。性能優化

  • 第一步:從正則表達式第一個字符a開始,吃進「abcd」的第一個字符,也是a,匹配成功!
  • 第二步:這時正則表達式遇到了分支,後面有三種匹配可能,分別是acd、bsc、bcd。先選擇第一個路徑acd,吃入「abcd」的第二個字符,是b,匹配不成功。這時就須要進行一次回溯了(backtrack),把吃進來的最後一個字符b還回去,同時放棄第一個路徑,選擇第二個路徑bsc;
  • 第三步:第二個路徑bsc中,第一個字符是b,吃進「abcd」中的第二個字符,也是b,匹配成功!
  • 第四步:第二個路徑bsc中,下一個字符是s,吃進「abcd」中的第三個字符是c,匹配失敗,又要進行回溯了。把剛剛吃進的c和b還回去,回到第二步的狀態,並選擇第三個路徑bcd;
  • 第五步~第七步:依次匹配bcd和「abcd」中的剩餘字符,均匹配成功。
  • 第八步:完成匹配。

從這個並不複雜的例子中,光b這個字母就匹配了三次,可見回溯在正則表達式中至關廣泛。性能

量詞嵌套

考慮這樣一個正則測試

(a*)*b

 ,用它來匹配」aaaaaaaaaa」(10個a組成的字符串)。看起來不復雜呀,實驗一下:

QQ20150601224618

哦,天哪,居然花費了6143步才完成!若是再加上一個a呢,變成11個a組成的字符串會怎麼樣?

QQ20150601224802

變成12287步了,翻了一倍。事實就是這樣,當出現以上這種量詞嵌套時,若是遭遇最壞狀況(最後一個字符才能確實匹配不成功),那麼這時正則引擎陷入災難性回溯,時間複雜度爲指數級)。

若是你試着再嵌套一層,9個a組成的字符串就能突破100萬步了……

其餘狀況

不少時候,並非上面所述的那麼極端的狀況,更多的多是對一個複雜的子句加量詞,而這個子句中自己就含有量詞;或者子句中有比較複雜的分組。這些狀況實際應用中極可能會出現,雖然達不到誇張的指數級複雜度,但對性能依然是不小的挑戰。

有一個例子我以爲比較有趣,對於性能優化這個問題,也有參考價值。什麼例子呢?用正則表達式匹配一個數字是否爲質數。呃……這有點跳躍,看似風馬牛不相及,但還真能作到。咱們就簡單一點,不考慮1。首先,把數字轉成字符串,是幾就寫幾個1。好比5就轉成5個1組成的字符串11111。用來匹配的正則是

^(11+)\1+$

 ,若是匹配經過,則是合數;不經過說明是質數。這個原理並不複雜,我很少說了。一樣還有一些用正則測試二元一次方程整數解的問題,原理也相似。

這個例子其實沒什麼用,由於好玩因此印象深入。那對於咱們有什麼參考價值呢?就是別寫這麼費性能的正則!這個例子中,看起來沒有量詞嵌套等狀況,但與其餘問題相似的,這裏對引用值加了量詞,而這個引用詞並不肯定,回溯仍然會不少。因此咱們除了要注意量詞嵌套、複雜子表達式加量詞或分組加量詞這些狀況外,還要注意引用加量詞,這點是我沒見別人提到過的。

一些解決手段

量詞運算

對於量詞嵌套的狀況,一些簡單的運算能夠消除嵌套:

 

JavaScript

 

1

2

(a*)* <==> (a+)* <==> (a*)+ <==> a*

(a+)+ <==> a+

 

很簡單,很少說。

佔有優先量詞(Possessive Quantifiers)

這個有點意思,惋惜javascript還不支持,我說簡單說說。用法很簡單,在量詞的後面再加上一個+。相似

a++b

 ,那麼這和

a+b

 有什麼區別呢?佔有優先量詞並不保存回溯狀態,換言之,前者不能回溯。若是匹配成功,沒什麼區別;若是最後b匹配不成功,那麼前者不會進行回溯,而是直接匹配失敗,後者會再進行回溯。

固化分組(或原子分組,Atomic Grouping)

這個更有意思,它控制一個這個字串總體不回溯。用法是這樣的

(?>abc)

 。嗯,不幸的是javascript依然不支持。不過用其餘語言的時候必定要對這個特性保持關注,自己它的兼容性要比佔有優先量詞要高(比c#支持原子分組不支持佔有優先量詞),另外它徹底能夠模擬出佔有優先量詞的功能,用法也更靈活。

參考:

 

額外補充:.NET有個平衡組比較適合匹配嵌套的html標籤,這裏不提

另外這裏有個我遇到回溯很是嚴重的例子

<a.*?href="(.*?)".*?(?://gdp.alicdn.com/imgextra/i2/94008339/TB2Y9MSsFXXXXX.XXXXXXXXXXXX_!!94008339.jpg).*?(?:>|\/>)

即對天貓店鋪後面匹配到的banner連接來找相應href的正則。結果執行了兩小時才完成。。

隨後修改爲下面這樣就正常了

<a.*?href="(?<1>[^"]+)".*?(?://gdp.alicdn.com/imgextra/i2/94008339/TB2Y9MSsFXXXXX.XXXXXXXXXXXX_!!94008339.jpg)

結論因此要善用不定量詞 . * ?

 

 

本文基於署名-非商業性使用 3.0許可協議發佈,轉載、演繹必須保留本文的署名周驊(包含連接 http://www.zhouhua.info ),且不得用於商業目的。如您有任何疑問或者受權方面的協商,請與我聯繫

相關文章
相關標籤/搜索