要了解正則表達式的原理,須要先了解一些計算機語言文法的基礎知識。java
一個文法能夠用一個四元來定義,G = {Vt,Vn,S,P}正則表達式
其中Vt是一個非空有限的符號集合,它的每一個元素成爲終結符號。Vn也是一個非空有限的符號集合,它的每一個元素稱爲非終結符號,而且Vt∩Vn=Φ。S∈Vn,稱爲文法G的開始符號。P是一個非空有限集合,它的元素稱爲產生式。所謂產生式,其形式爲α→β,α稱爲產生式的左部,β稱爲產生式的右部,符號「→」表示「定義爲」,而且α、β∈(Vt∪Vn)*,α≠ε,即α、β是由終結符和非終結符組成的符號串。開始符S必須至少在某一產生式的左部出現一次。算法
文法可推導的語言標記爲L(G)。編程
著名語言學家Chomsky(喬姆斯基)根據對產生式所施加的限制的不一樣,把文法分紅四種類型,即0型、1型、2型和3型。閉包
正則表達式就是最後一種,正則文法,的一種表達形式,以整個字母表做爲終結符集合Vt。編程語言
假設有一個文法的產生式是{S->Sa; S->b;},那麼對應的正則表達式爲ba*
。翻譯
所以正則表達式,正則文法,有限狀態自動機這個三個概念雖然指不一樣的東西,可是具有內在的等價性。設計
正則表達式是正則文法,限制多於上下文無關文法,而咱們使用的編程語言語法都是上下文無關文法,所以試圖經過正則表達式去處理代碼(好比語言翻譯、代碼生成)的努力很可能歸於徒勞。不過,把代碼當作純文本,而後在處理過程當中使用正則表達式,仍然能大大提升效率。3d
正則表達式包含不少的元字符來表達規則,不過本文不是要介紹如何使用正則表達式,關於正則表達式規則最好的參考書是《精通正則表達式》。code
實際上,正則表達式核心的運算符只有如下幾種:
名稱 | 示例 | 備註 |
---|---|---|
或運算 | r|s | 匹配的語言是L(r)和L(s)的並集 |
鏈接運算 | rs | 匹配的語言是L(r)和L(s)鏈接 |
Kleene運算 | r* | 匹配的語言是L(r)和L(s)鏈接 |
括號 | (r) | 匹配的語言與L(r)一致 |
kleene運算符優先級最高,且是左結合的,鏈接第二,或運算優先級最低。
運算定律:
示例 | 備註 |
---|---|
r|s = s|r | | 運算知足交換律 |
r|s|t = r|(s|t) | | 知足結合律 |
r(st) | 鏈接能夠結合 |
r(s|t) = rs|rt | 鏈接對|能夠分配 |
ℇr = rℇ = r | ℇ是鏈接的單位元 |
r* = (r|ℇ)\* | 閉包中必定包含ℇ |
r** = r* | *具備冪等性 |
擴展運算符使得正則表達式更具表達力,下面僅舉幾個例子:
擴展運算符 | 等價形式 |
---|---|
+ | r+ = rr* = r*r |
? | r? = r | ℇ |
字符類 | [a1a2…an] = a1|a2|…|an;若是是連續的字符類,能夠寫成[a1-an] |
高級特性:
正則表達式具有不少高級特性,好比捕獲、環視、固化分組等等,這些特性是爲了提升正則表達式的實用價值被設計出來的,不屬於正則文法的範疇。
前面說過,正則文法對應於有限狀態自動機,又分肯定型有限狀態自動機(DFA)和非肯定型有限狀態自動機(NFA),這兩種狀態機的能力是同樣的,都能識別正則語言。正則表達式的識別引擎,都是基於DFA或NFA構造的。關於狀態機的基礎理論,這裏就不描述了,只要稍微有點印象,就不妨礙繼續閱讀。
NFA
一個字母能夠標記離開狀態的多條邊,而且ℇ 也能夠標記一條邊;這說明NFA的匹配過程面臨不少的岔路,須要作出選擇,一旦某條岔路失敗,就須要回朔。
下圖是正則表達式(a|b)*abb對應的NFA,它至關直觀,基本能夠從正則表達式直接轉換而來。
DFA
對於每一個狀態以及字母表中的每一個字母,只能有一條以該字母爲標記的,離開該狀態的邊;這說明DFA的匹配過程是肯定的,每一個字母是須要匹配一次。
與上面NFA等價的DFA以下圖,至關地不直觀:
因爲NFA和DFA的能力是同樣的,每一個NFA必然能夠轉化成一個等價的DFA。既然DFA對每一個輸入能夠到達的狀態時是肯定的,那麼輸入串s在NFA中可能達到的狀態集合對應爲等價DFA中某個狀態。從這個思路出發,能夠構造出DFA。
最終獲得的DFA以下,(0,3)包含了NFA的終結狀態3,所以也是DFA的中介狀態,對狀態從新命名能夠獲得上面一樣的DFA。
DFA和NFA的效率差別
很容易理解,構造DFA的代價遠大於NFA,假設NFA的狀態數爲K,那麼等價DFA的狀態數目理論上可達2的k次方,不過實際上幾乎不會出現這麼極端的狀況,能夠確定的是構造DFA會消耗更多的時間和內存。
可是DFA一旦構造好了以後,執行效率就很是理想了,若是一個串的長度是n,那麼匹配算法的執行復雜度是O(n);而NFA在匹配過程當中,存在大量的分支和回朔,假設NFA的狀態數爲s,由於每輸入一個字符可能達到的狀態數作多爲s,那麼匹配算法的複雜度及時輸入串的長度乘以狀態數O(ns)。
正則表達式的NFA&DFA構造、轉化、簡化有一整套理論及方法,遠比上面的例子複雜,本文僅經過一個簡單的例子來講明原理。
NFA和DFA這兩種匹配算法,除了效率上的差異外,從更高的視點看,造成了兩種風格的引擎,進而對正則表達式的匹配的其餘方面能力形成差別。NFA被稱之爲"表達式主導"引擎,而DFA被稱之爲「文本主導」引擎。
從表達式的第一個部分開始,每次檢查一部分,同時檢查當前文本是否匹配表達式的當前部分,若是是,則繼續表達式的下一部分,如此繼續,直到表達式的全部部分都能匹配,即整個表達式匹配成功。
咱們來看錶達式to(nite|knight|night)
匹配文本...tonight...
的過程: 表達式的第一個部分是t,它會不斷重複掃描,直到在字符串中找到t,以後就檢查隨後的o,若是能匹配就繼續檢查下面的元素。這個例子中,下面的元素是(nite|knight|night)
,意思是nite或者knight或者night,引擎會依次嘗試這三種可能。
整個過程,控制權在表達式的元素之間轉換,所以被稱之爲「表達式主導」。「表達式主導」的特色是每一個子表達式都是獨立的,不存在內在聯繫。 子表達式與整個正則表達式的控制結構(多選、量詞)的層級關係控制了整個匹配過程。
DFA在讀入一個文本的時候,會記錄當前有效的全部匹配的表達式位置(這些位置集合對應於DFA的一個狀態)。
以上面的匹配過程爲例:
這種方式被稱之「文本主導」是由於被掃描的字符串,控制了引擎的執行過程。
NFA表達式主導的特性,使得經過修改正則表達式來影響引擎,所以下面三個表達式儘管可以匹配一樣的文本,可是引擎的執行過程各不相同:
可是對於DFA來講,沒有任何區別。
對於包含或選項的表達式,NFA在成功匹配一個選項以後可能報告匹配成功,此時並不知道後面的選項是否也會成功,是否包含一個更長的匹配。
假設使用one(self)?(selfsufficient)?
來匹配oneselfsufficient
,NFA首先匹配one,而後匹配self,此時發現selfsufficient
沒法匹配剩餘子串,可是這個子表達式不是必須的,所以能夠當即返回成功,此時匹配的串爲oneself
。
實際上NFA引擎的匹配結果與具體實現有關,而DFA必然會成功匹配oneselfsufficient
。
NFA可以支持「捕獲group」,「環視」,「佔有優先量詞」,「固話分組」等高級功能,這些功能都基於「子表達式獨立進行匹配」這一特色。 而DFA沒法記錄匹配歷史與子表達式之間的關係,於是也沒法實現這些功能。
可見NFA引擎具有更大的實用價值,於是,咱們在編程語言裏面使用的正則表達式庫都是基於NFA的。java的Pattern就是基於NFA的,Pattern.compile()方法顯然就是在構造NFA狀態圖。