什么是正則表達(dá)式
工作中我們經(jīng)常使用正則表達(dá)式來解決問題。正則表達(dá)式又稱規(guī)則表達(dá)式,其實(shí)就是事先定義好的一些特定字符、及這些特定字符的組合,組成一個(gè)“規(guī)則字符串”,這個(gè)“規(guī)則字符串”用來表達(dá)對(duì)字符串的一種過濾邏輯。它是一個(gè)強(qiáng)大便捷高效的文本處理工具,正則表達(dá)式可以添加、刪除、分離、疊加、插入和修正各種類型的文本和數(shù)據(jù)。
正則表達(dá)式是一個(gè)工具,各種不同的語(yǔ)言對(duì)正則表達(dá)式可能都有著不同的實(shí)現(xiàn)。但是使用的方法大同小異,各個(gè)不同語(yǔ)言也有自己特有的特性和能力,需要我們?cè)趯?shí)際使用的過程中自己發(fā)掘。
正則表達(dá)式的原理
正則表達(dá)式應(yīng)用到目標(biāo)字符串大致分為下面幾步:
1、正則表達(dá)式編譯。
2、引擎?zhèn)鲃?dòng)開始
3、元素檢查
4、尋找匹配結(jié)果(成功則結(jié)束)
5、引擎?zhèn)鲃?dòng)裝置驅(qū)動(dòng)(下一個(gè)字符開始,回到3)
6、匹配徹底失敗(結(jié)束)
我們知道正則表達(dá)式其實(shí)是一個(gè)“字符串”,因此編譯和引擎是什么呢?其實(shí)計(jì)算機(jī)在實(shí)際使用正則表達(dá)式的時(shí)候,并不是直接使用這個(gè)字符串,而是通過編譯翻譯成特定的機(jī)器,這個(gè)機(jī)器就是我們所說的引擎。
這個(gè)引擎分為兩種,NFA(非確定型有窮自動(dòng)機(jī))和DFA(確定型有窮自動(dòng)機(jī)),有印象嗎?學(xué)過編譯原理的同學(xué)們?沒學(xué)過或者忘了也不要緊。下面我們一起來學(xué)習(xí)下這兩種機(jī)器吧。
正則表達(dá)式引擎
我們通過一個(gè)例子來看看NFA和DFA有哪些不同吧。
對(duì)于正則表達(dá)式:(a|b)*abb來說,NFA和DFA的示意圖如下:
怎么樣,是不是有點(diǎn)懵懵的呢?
對(duì)于一個(gè)正則表達(dá)式來說通常情況下有三種形式:ab(連接),a|b(或),a* (0到多個(gè)a)。對(duì)應(yīng)的三種形態(tài):
基于這三種基本的形態(tài),根據(jù)正則表達(dá)式,我們很容易畫出NFA的模型。
那么為什么我們要叫它不確定型呢?加入我們當(dāng)前的狀態(tài)是0,當(dāng)輸入a時(shí),我們既可以得到狀態(tài)0也可以得到狀態(tài)1,我們不確定得到的狀態(tài)是0還是1,所以這個(gè)自動(dòng)機(jī)是不確定型的。
那么DFA就是確定型有窮自動(dòng)機(jī),不管輸入什么我們都可以得到一個(gè)確定的狀態(tài),但是DFA又該如何得到呢?
DFA需要通過NFA推導(dǎo)出來。
1、首先我們開始的狀態(tài)是0
2、0狀態(tài)可以輸入a和b,我們先輸入a,這時(shí)可以得到0狀態(tài)或者1狀態(tài)。我們把這個(gè)狀態(tài)成為一個(gè)新的狀態(tài)(0,1),這個(gè)狀態(tài)是狀態(tài)0和狀態(tài)1的集合態(tài)。然后狀態(tài)0輸入b得到的還是狀態(tài)0。
3、然后看(0,1)狀態(tài)。根據(jù)NFA的圖,我們發(fā)現(xiàn)0可以輸入a和b,1可以輸入b,因此(0,1)可以輸入a和b。輸入a可以得到0和1,輸入b可以得到0和2。因此(0,1)輸入得到的還是(0,1),輸入b得到(0,2)。
4、根據(jù)上述規(guī)律繼續(xù)推導(dǎo),我們得到下面的圖。
這時(shí)我們現(xiàn)在只有0,(0,1),(0,2),(0,3)這四種狀態(tài),改變下狀態(tài)的名稱就得到了最開始的DFA的狀態(tài)圖。
當(dāng)然這只是個(gè)簡(jiǎn)單的例子,實(shí)際中式子可能會(huì)非常復(fù)雜,DFA的狀態(tài)數(shù)可能會(huì)遠(yuǎn)遠(yuǎn)大于NFA的總狀態(tài)數(shù)。
那么NFA和DFA各自有什么特點(diǎn),對(duì)于正則表達(dá)式又有什么影響呢?
NFA與DFA
編譯速度:
通過上述的NFA和DFA的原理我們可以看到,NFA的狀態(tài)數(shù)與正則表達(dá)式的長(zhǎng)度有關(guān)系,而DFA的狀態(tài)數(shù)最大可能是NFA狀態(tài)數(shù)的排列組合之和。因此DFA編譯起來要比NFA復(fù)雜的多。
匹配速度:
在輸入一個(gè)字符之后,NFA可能到達(dá)的狀態(tài)不一樣,這時(shí)候需要進(jìn)行選擇,加入剛開始選擇的分支最終無法匹配,那么我們需要重新回退到這個(gè)選擇,選擇另一個(gè)分支進(jìn)行匹配。這就產(chǎn)生了回溯。而DFA到達(dá)的狀態(tài)是確定的,因此只需要匹配一遍字符串就可以得到結(jié)果。雖然DFA有編譯上的損耗,但是通常情況下回溯的損耗比較大,因此匹配上速度上DFA勝。
其他對(duì)比:
根據(jù)NFA和DFA的特點(diǎn),我們還可以知道一些其他的對(duì)比情況,感興趣的讀者可以去查相關(guān)的資料。
最終兩者的比較如下表:
現(xiàn)在大部分的語(yǔ)言都是基于NFA的,因?yàn)樗С值墓δ芨妗?/p>
匹配過程
NFA其實(shí)是表達(dá)式主導(dǎo)的,每次先檢查正則的一部分,當(dāng)遇到分歧時(shí),先檢查一個(gè)路徑如果適合則匹配,如果不適合,則回退到分歧點(diǎn)進(jìn)行另一部分的匹配,因此要檢查的文本字符可能被檢查多次,這就是回溯;
回溯(或者說選擇路徑)的原則有兩個(gè):1、優(yōu)先量詞還是忽略量詞,2、LIFO。后進(jìn)先出,回溯到最近的儲(chǔ)存的路徑。
DFA是文本主導(dǎo)的,掃描字符時(shí),會(huì)記錄”當(dāng)前有效“的所有匹配可能,盡可能多的匹配字符串。例如:DFA中,today去匹配to(day)?這樣的字符串,最終匹配出來的就是today。如果是toda去匹配則會(huì)得到to。DFA中每個(gè)字符只會(huì)匹配一次。
還是以(a|b)*abb這個(gè)為例,我們用分別用NFA和DFA來匹配babb這個(gè)字符串。
假設(shè)我們的NFA實(shí)現(xiàn)是以量詞優(yōu)先。
1、起始狀態(tài)0,輸入b得到狀態(tài)0
2、狀態(tài)0,輸入a可以得到0或者1,我們先記住這次選擇的路徑,由于是量詞優(yōu)先,我們選擇跳到狀態(tài)0
3、狀態(tài)0,輸入b得到0
4、狀態(tài)0,輸入b得到0,輸入完畢,0不是最終態(tài),回退到之前的分叉2
5、狀態(tài)0,輸入a,由于選過0,這次選擇到達(dá)狀態(tài)1。
6、狀態(tài)1,輸入b,得到狀態(tài)2
7、狀態(tài)2,輸入b,得到狀態(tài)3,輸入完畢,3是最終態(tài),匹配成功。
DFA匹配:
1、起始狀態(tài)0,輸入b得到狀態(tài)0
2、狀態(tài)0,輸入a得到狀態(tài)1
3、狀態(tài)1,輸入b得到狀態(tài)2
4、狀態(tài)2,輸入b得到狀態(tài)3,輸入完畢,狀態(tài)3是最終態(tài),匹配成功。
總結(jié)
正則表達(dá)式一直在用,但是一直也沒有研究過正則表達(dá)式是怎么實(shí)現(xiàn)的。因?yàn)橹翱吹竭^有人說正則表達(dá)式的匹配可能會(huì)卡進(jìn)程,剛開始不太相信,經(jīng)過這次的學(xué)習(xí)總結(jié),了解了正則表達(dá)式的原理,發(fā)現(xiàn)原來正則表達(dá)式的寫法真的會(huì)影響性能(由于大部分語(yǔ)言的實(shí)現(xiàn)都是基于NFA的)。這一篇先介紹原理,之后有時(shí)間會(huì)再出一篇,大家一塊研究探討下如何提高正則表達(dá)式的效率。