淺談正則表達式解析過程 / 效率優化

前言正則表達式

編寫高性能的正則表達式,有以下幾條規則,這幾條規則是本人總結出來的:性能

一、使用正確的邊界匹配器(^、$、\b、\B等)優化

二、使用具體的元字符、字符類(\d、\w、\s等) spa

三、使用正確的量詞(+、*、?、{n,m})指針

四、使用非捕獲組、原子組code

五、注意量詞的嵌套 blog

其實正則表達式的不少優化技巧都是圍繞着「減小回溯」這樣一個原則進行優化的。字符串

至於什麼是「回溯」,筆者就不在這裏重複了,如下經過具體的例子理解這樣的過程。域名

 

示例class

1、如下是一則匹配電子郵件地址的正則表達式:

^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{ 2, 3})+$ 

 先一步步的解析:

一、^\w+:表示必須以字符開始, 且是一個或者多個;

二、([\.-]?\w+)*中的「[\.-]?」表示匹配「.」或者「-」,零次或者一次;

三、([\.-]?\w+)*中的「\w+」則表示匹配一個或者多個的字符;

四、([\.-]?\w+)*整個則表示匹配.xxx、-xxx或者xxx這樣的字符,且零次或者屢次;

五、第1-4步,則匹配sunny或者sunny.yang這樣的字符;

六、「@」則是具體元,匹配具體的@; 

七、 \w+:則表示匹配的一個或者多個的字符,由於email不可能這樣嘛:sunny@.gmail.com;

八、 ([\.-]?\w+)*:則跟第2-4步同樣,匹配.16三、-lib、.gd這樣的字符,且零次或者屢次;

九、 (\.\w{2,3})+$:則匹配.com、.cc這樣結尾的域名,且由於\w{2,3}限定了長度必須爲2-3位,因此不能匹配.c、.n這樣的字符。

 

乍看這樣一個解析過程沒問題,邏輯正確,但其實暗藏不少問題,看看如下的一個匹配圖,backtrack則表示回溯(使用RegexBuddy能夠很清晰的看到這過程)


 整個成功的匹配過程經歷了55步,咱們先分析下整個匹配過程:

 

一、圖中的第1和2步,匹配^\w+,匹配成功,匹配了「admin」;

二、圖中第3步,匹配[\.-]?,固然因爲不存在「.」和「-」,所以沒匹配上具體的字符,但又因爲「?」的限定,能夠匹配零或者一次,所以這個子表達式匹配成功,雖然沒匹配上具體的字符。

三、圖中第4步,匹配\w+,因爲「+」限定一個或者多個以上字符,但後續已經沒[a-zA-Z0-9]能夠匹配了,所以產生回溯,回溯到上次匹配成功的位置,也就admin; 

四、圖中第5步,由於上一步產生了回溯,因此「[\.-]?\w+」匹配了零次,因爲([\.-]?\w+)*中限定零次或者屢次,所以也匹配成功,也沒匹配上具體的字符; 

如下步驟,匹配該過程: 

^\w+([\.-]?\w+)* @ \w+([\.-]?\w+)*(\.\w{ 2 , 3 })+$

五、圖中第6步,匹配了「@」 ,第7步匹配了「\w+」,即匹配了「open」;

六、圖中第8-13步,匹配「([\.-]?\w+)*」 ,匹配了「-lib」、「.com」,匹配「.com」可能與咱們指望不相符,咱們指望這子表達式匹配的是www.xx.gd.cn中的「.gd」;

七、圖中第7-10步,匹配了open-lib,第7-13步則匹配了open-lib.com;

八、由於「([\.-]?\w+)*」中的量詞是「*」,則繼續重複這個過程;

九、 圖中第14步,匹配「([\.-]?\w+)*」中的「[\.-]?」,由於此時指針已經位於@open-lib.com以後了,但因爲量詞「?」,所以也匹配成功,但沒匹配上字符,也沒字符可匹配;

十、 圖中第15步,匹配「([\.-]?\w+)*」中的「\w+」,此時指針仍位於字符串末尾,沒任何字符能匹配,因此匹配失敗,產生回溯,回到上次成功的位置,即圖中的第13步,繼續下個表達式的匹配;

十一、 圖中第16步,匹配(\.\w{2,3})+中的「\.」,因爲沒有任何字符能匹配,匹配失敗,進行回溯;

十二、圖中第17步 ,(\.\w{2,3})+中量詞「+」,表示該表達式必須匹配一次或者屢次,因爲上一步匹配失敗了,因此匹配零次,但不符合一次或者屢次的限定,所以繼續回溯;

1三、因爲上一步匹配失敗,須要進行回溯,所以表達式沒有更多的分支了,只能將指針回退一個字符,回溯上次成功的位置,即([\.-]?\w+)*中\w+的位置(這是上次產生分支的位置);

1四、圖中剩下的步驟,就重複着匹配([\.-]?\w+)*,回退字符,匹配(\.\w{2,3})+這樣的過程,直到匹配成功。 


2、如下看看另一則一樣匹配郵件地址的正則表達式:

 

^\w+([-\.]\w+)*@\w+([-\.]\w+)*\.\w+([-\.]\w+)*$

 

 這個正則跟上面的看起來貌似差很少,不過細看仍是有區別的,也先一步步來解析:

一、^\w+:表示必須以字符開始, 且是一個或者多個(這一步與上面的同樣); 

二、 ([-\.]\w+)*中的「[-\.]」表示匹配「-」或者「.」;

三、 ([-\.]\w+)*中的「\w+」則表示匹配一個或者多個的字符;

四、([-\.]\w+)*整個則表示匹配.xxx、-xxx這樣的字符,且零次或者屢次;

五、第1-4步,則匹配sunny或者sunny.yang這樣的字符; 

六、「@」則是具體元,匹配具體的「@」; 

七、 \w+:則表示匹配的一個或者多個的字符,由於email不可能這樣嘛:sunny@.gmail.com; 

八、([-\.]\w+)*:則跟第2-4步同樣,匹配.16三、-lib、.gd這樣的字符,且零次或者屢次; 

九、「\.」則是具體元,匹配「.」;

十、\w+:則匹配一個或者多個字符;

十一、([-\.]\w+)*:則匹配「.com」、「-lib」、「.c」這樣的字符,且能夠零次或者屢次;

十二、$ :則表示結尾

乍看這個正則的步驟過程貌似比上一則長,其實否則,同時這個正則也存在着問題,先看看匹配圖,一樣backtrack表示回溯:


 

對的,你沒看錯,整個正確的匹配過程用了19步,對比前面的55步,簡直天與地的差異。,咱們繼續分析下匹配過程:

一、圖中的第1和2步,匹配^\w+,匹配成功,匹配了「admin」;

二、圖中第3步,匹配[-\.],固然因爲不存在「.」和「-」,所以沒匹配上具體的字符,也沒具體的量詞容許匹配零次,因此不用繼續往下匹配了,所以直接產生了回溯; 

三、圖中第4步,由於上一步產生了回溯,因此「[-\.]\w+」匹配了零次,因爲([-\.]\w+)*中限定零次或者屢次,所以也匹配成功,也沒匹配上具體的字符;

 如下步驟,匹配該過程: 

^\w+([-\.]\w+)* @ \w+([-\.]\w+)*\.\w+([-\.]\w+)*$

四、圖中第6步\w+,匹配了open;

五、圖中第7-12步匹配([-\.]\w+)*,匹配了「-lib」和「.com」;

六、由於「([-\.]\w+)*」中的量詞是「*」,則繼續重複這個過程;

七、圖第13步,匹配([-\.]\w+)* ,由於此時指針已經位於@open-lib.com以後了,也沒具體的量詞容許匹配零次,所以匹配失敗,回溯到上次成功的位置;

八、圖第14步,匹配 ([-\.]\w+)*$中的「[-\.]」,此時指針仍位於字符串末尾,沒任何字符能匹配,因此匹配失敗,產生回溯,回到上次成功且還沒嘗試過的位置,即圖中的第9步;

九、通過上面的回溯,指針已經位於@open-lib以後的位置了;

十、圖第15步匹配了「.」,第16步\w+則匹配了「com」 ;

十一、圖第17步匹配([-\.]\w+)*,因爲此時指針又位於字符串末尾,所以[-\.]部分沒匹配上任何字符,所以產生回溯;

十二、圖第18步,因爲 ([-\.]\w+)*的量詞是「*」,表示匹配零次或屢次,雖然子表達式[-\.]匹配失敗,因此整個表達式匹配了零次,也是匹配成功;

1三、最後一步第19步,「$」表示末尾匹配,由於此時指針位於字符串末尾,故符合,所以也匹配成功。

 

分析

整個匹配過程關鍵優化地方,仍是回溯,兩個示例表達式看起來相近,匹配過程也部分相似,但兩個例子的效率卻如此大的分別,如今來分析一下形成回溯的緣由。

對比下兩個表達式不一樣的部分:

 

^\w+ ([\.-]?\w+)*@\w+ ([\.-]?\w+)*(\.\w{ 2, 3})+$ 

^\w+([\.-]\w+)*@\w+([\.-]\w+)*\.\w+([-\.]\w+)*$

 

 

PS:

這是11年寫的文章,當時沒寫完,如今。。。不記得思路了。。。so。。。

相關文章
相關標籤/搜索