我這裏給你們奉上一篇硬核教程。首先聲明,這篇文章不是教你如何寫正則表達式,而是教你寫一個能執行正則表達式的執行引擎。 網上教你寫正則表達式的文章、教程不少,但教你寫引擎的並很少。不少人認爲我就是用用而已,不必理解那麼深,但知道原理是在修煉內功,正則表達式底層原理並不僅僅是用在這,而是出如今計算機領域的各個角落。理解原理可讓你之後寫字符串匹配時正則表達式可以信手拈來,理解原理也是舉一反三的基礎。廢話很少說,直接開始正式內容。 java
本文是我我的作的動手實踐性項目,因此未完整支持全部語法,並且由於是用NFA實現的因此性能比生產級的執行引擎差好多。目前源碼已開放至https://github.com/xindoo/regex,後續會繼續更新,歡迎Star、Fork 提PR。 git
目前支持的正則語義以下:github
聲明:本文不是入門級的文章,因此若是你想看懂後文的內容,須要具有如下的基本知識。正則表達式
有限狀態機(Finite-state machine),也被稱爲有限狀態自動機(finite-state automation),是表示有限個狀態以及在這些狀態之間的轉移和動做等行爲的數學計算模型(From 維基百科 狀態機) 。 聽起來晦澀難懂,我用大白話描述一遍,狀態機其實就是用圖把狀態和狀態之間的關係描述出來,狀態機中的一個狀態能夠在某些給定條件下變成另一種狀態。舉個很簡單的例子你就懂了。算法
好比我今年18歲,我如今就是處於18歲的狀態,若是時間過了一年,我就變成19歲的狀態了,再過一年就20了。固然我20歲時時光倒流2年我又能夠回到18歲的狀態。這裏咱們就能夠把個人年齡狀態和時間流逝之間的關係用一個自動機表示出來,以下。
每一個圈表明一個節點表示一種狀態,每條有向邊表示一個狀態到另外一個狀態的轉移條件。上圖中狀態是個人年齡,邊表示時間正向或者逆向流逝。 編程
有了狀態機以後,咱們就能夠用狀態機來描述特定的模式,好比上圖就是年齡隨時間增加的模式。若是有人說我今年18歲,1年後就20歲了。照着上面的狀態機咱們來算下,1年後你才19歲,你這不是瞎說嗎! 沒錯,狀態機能夠來斷定某些內容是否符合你狀態機描述的模式了。喲,一不當心就快扯到正則表達式上了。
咱們這裏再引入兩種特殊的狀態:起始態和接受態(終止態),見名知意,不用我過多介紹了吧,起始態和終止態的符號以下。
咱們拿狀態機來作個簡單的字符串匹配。好比咱們有個字符串「zsx」,要判斷其餘字符串是否和"zxs"是一致的,咱們能夠爲"zxs"先創建一個自動機,以下。
對於任意一個其餘的字符串,咱們從起始態0開始,若是下一個字符能匹配到0後邊的邊上就日後走,匹配不上就中止,一直重複,若是走到終止態3說明這個字符串和」zxs「同樣。任意字符串均可以轉化成上述的狀態機,其實到這裏你就知道如何實現一個只支持字符串匹配的正則表達式引擎了,若是想支持更多的正則語義,咱們要作的更多。segmentfault
咱們再來引入一條特殊的邊,學名叫$\epsilon$閉包(emm!看到這些符號我就回想起上學時被數學支配的恐懼),其實就是一條不須要任何條件就能轉移狀態的邊。
沒錯,就只這條紅邊本邊了,它在正則表達式狀態機中起着很是重要的鏈接做用,能夠不依賴其餘條件直接跳轉狀態,也就是說在上圖中你能夠直接從1到2。
有了 $\epsilon$閉包的加持,咱們就能夠開始學如何畫正則表達式文法對應的狀態機了。設計模式
首先來看下純字符匹配的自動機,其實上面已經給過一個"zxs"的例子了,這裏再貼一下,其實就是簡單地用字符串在一塊兒而已,若是還有其餘字符,就繼續日後串。
兩個表達式如何傳在一塊兒,也很簡單,加入咱們已經有兩個表達式A B對應的狀態機,咱們只須要將其用$\epsilon$串一塊兒就好了。
數據結構
正則表達式中的| 標識二選一均可以,好比A|B A能匹配 B也能匹配,那麼A|B就能夠表示爲下面這樣的狀態圖。
從0狀態走A或B均可以到1狀態,完美的詮釋了A|B語義。閉包
正則表達式裏有4中表示重複的方式,分別是:
我來分別畫下這4種方式如何在狀態機裏表示。
0狀態能夠經過E也能夠依賴$\epsilon$直接跳過E到達1狀態,實現E的0次匹配。
0到1後能夠再經過$\epsilon$跳回來,就能夠實現E的1次以上匹配了。
仔細看其實就是? +的結合。
這種建圖方式簡單粗暴,但問題就是若是n和m很大的話,最後生成的狀態圖也會很大。其實能夠把指定次數的匹配作成一條特殊的邊,能夠極大減少圖的大小。
正則表達式中還支持不少某類的字符,好比.表示任意非換行符,d標識數字,[]能夠指定字符集…… ,其實這些都和圖的形態無關,只是某調特殊的邊而已,本身實現的時候能夠選擇具體的實現方式,好比後面代碼中我用了策略模式來實現不一樣的匹配策略,簡化了正則引擎的代碼。
子表達能夠Tompson算法,其實就是用遞歸去生成()中的子圖,而後把子圖拼接到當前圖上面。(什麼Tompson說的那麼高大上,不就是遞歸嗎!)
來聯繫畫下 a(a|b)* 的狀態圖,這裏我也給出我畫的,你能夠參考下。
看完上文以後相信你一直知道若是將一個正則表達式轉化爲狀態機的方法了,這裏咱們要將理論轉化爲代碼。首先咱們要將圖轉化爲代碼標識,我用State表示一個節點,其中用了Map<MatchStrategy, List<State>> next表示其後繼節點,next中有個key-value就是一條邊,MatchStrategy用來描述邊的信息。
public class State { private static int idCnt = 0; private int id; private int stateType; public State() { this.id = idCnt++; } Map<MatchStrategy, List<State>> next = new HashMap<>(); public void addNext(MatchStrategy path, State state) { List<State> list = next.get(path); if (list == null) { list = new ArrayList<>(); next.put(path, list); } list.add(state); } protected void setStateType() { stateType = 1; } protected boolean isEndState() { return stateType == 1; } }
NFAGraph表示一個完整的圖,其中封裝了對圖的操做,好比其中就實現了上文中圖串 並連和重複的操做(注意我沒有實現{})。
public class NFAGraph { public State start; public State end; public NFAGraph(State start, State end) { this.start = start; this.end = end; } // | public void addParallelGraph(NFAGraph NFAGraph) { State newStart = new State(); State newEnd = new State(); MatchStrategy path = new EpsilonMatchStrategy(); newStart.addNext(path, this.start); newStart.addNext(path, NFAGraph.start); this.end.addNext(path, newEnd); NFAGraph.end.addNext(path, newEnd); this.start = newStart; this.end = newEnd; } // public void addSeriesGraph(NFAGraph NFAGraph) { MatchStrategy path = new EpsilonMatchStrategy(); this.end.addNext(path, NFAGraph.start); this.end = NFAGraph.end; } // * 重複0-n次 public void repeatStar() { repeatPlus(); addSToE(); // 重複0 } // ? 重複0次哦 public void addSToE() { MatchStrategy path = new EpsilonMatchStrategy(); start.addNext(path, end); } // + 重複1-n次 public void repeatPlus() { State newStart = new State(); State newEnd = new State(); MatchStrategy path = new EpsilonMatchStrategy(); newStart.addNext(path, this.start); end.addNext(path, newEnd); end.addNext(path, start); this.start = newStart; this.end = newEnd; } }
整個建圖的過程就是依照輸入的字符創建邊和節點之間的關係,並完成圖的拼接。
private static NFAGraph regex2nfa(String regex) { Reader reader = new Reader(regex); NFAGraph NFAGraph = null; while (reader.hasNext()) { char ch = reader.next(); MatchStrategy matchStrategy = null; switch (ch) { // 子表達式特殊處理 case '(' : { String subRegex = reader.getSubRegex(reader); NFAGraph newNFAGraph = regex2nfa(subRegex); checkRepeat(reader, newNFAGraph); if (NFAGraph == null) { NFAGraph = newNFAGraph; } else { NFAGraph.addSeriesGraph(newNFAGraph); } break; } // 或表達式特殊處理 case '|' : { String remainRegex = reader.getRemainRegex(reader); NFAGraph newNFAGraph = regex2nfa(remainRegex); if (NFAGraph == null) { NFAGraph = newNFAGraph; } else { NFAGraph.addParallelGraph(newNFAGraph); } break; } case '[' : { matchStrategy = getCharSetMatch(reader); break; } case '^' : { break; } case '$' : { break; } case '.' : { matchStrategy = new DotMatchStrategy(); break; } // 處理特殊佔位符 case '\\' : { char nextCh = reader.next(); switch (nextCh) { case 'd': { matchStrategy = new DigitalMatchStrategy(false); break; } case 'D': { matchStrategy = new DigitalMatchStrategy(true); break; } case 'w': { matchStrategy = new WMatchStrategy(false); break; } case 'W': { matchStrategy = new WMatchStrategy(true); break; } case 's': { matchStrategy = new SpaceMatchStrategy(false); break; } case 'S': { matchStrategy = new SpaceMatchStrategy(true); break; } // 轉義後的字符匹配 default:{ matchStrategy = new CharMatchStrategy(nextCh); break; } } break; } default : { // 處理普通字符 matchStrategy = new CharMatchStrategy(ch); break; } } // 代表有某類單字符的匹配 if (matchStrategy != null) { State start = new State(); State end = new State(); start.addNext(matchStrategy, end); NFAGraph newNFAGraph = new NFAGraph(start, end); checkRepeat(reader, newNFAGraph); if (NFAGraph == null) { NFAGraph = newNFAGraph; } else { NFAGraph.addSeriesGraph(newNFAGraph); } } } return NFAGraph; } private static void checkRepeat(Reader reader, NFAGraph newNFAGraph) { char nextCh = reader.peak(); switch (nextCh) { case '*': { newNFAGraph.repeatStar(); reader.next(); break; } case '+': { newNFAGraph.repeatPlus(); reader.next(); break; } case '?' : { newNFAGraph.addSToE(); reader.next(); break; } case '{' : { // break; } default : { return; } } }
這裏我用了設計模式中的策略模式將不一樣的匹配規則封裝到不一樣的MatchStrategy類裏,目前我實現了. d D s S w w,具體細節請參考代碼。這麼設計的好處就是簡化了匹配策略的添加,好比若是我想加一個x 只匹配16進制字符,我只須要加個策略類就行了,沒必要改不少代碼。
其實匹配的過程就出從起始態開始,用輸入做爲邊,一直日後走,若是能走到終止態就說明能夠匹配,代碼主要依賴於遞歸和回溯,代碼以下。
public boolean isMatch(String text) { return isMatch(text, 0, nfaGraph.start); } private boolean isMatch(String text, int pos, State curState) { if (pos == text.length()) { if (curState.isEndState()) { return true; } return false; } for (Map.Entry<MatchStrategy, List<State>> entry : curState.next.entrySet()) { MatchStrategy matchStrategy = entry.getKey(); if (matchStrategy instanceof EpsilonMatchStrategy) { for (State nextState : entry.getValue()) { if (isMatch(text, pos, nextState)) { return true; } } } else { if (!matchStrategy.isMatch(text.charAt(pos))) { continue; } // 遍歷匹配策略 for (State nextState : entry.getValue()) { if (isMatch(text, pos + 1, nextState)) { return true; } } } } return false; }
還有下集?沒錯,雖然到這裏已是實現了一個基本的正則表達式引擎,但距離可用在生產環境還差很遠,預告以下。
自己上面的引擎對正則語義支持不是很完善,後續我會繼續完善代碼,有興趣能夠收藏下源碼https://github.com/xindoo/regex,但應該不會出一篇新博客了,由於原理性的東西都在這裏,剩下的就是隻是一些編碼工做 。
上文只是實現了NFA引擎,NFA的引擎建圖時間複雜度是O(n),但匹配一個長度爲m的字符串時由於涉及到大量的遞歸和回溯,最壞時間複雜度是O(mn)。與之對比DFA引擎的建圖時間複雜度O(n^2),但匹配時沒有回溯,因此匹配複雜度只有O(m),性能差距仍是挺大的。
DFA引擎實現的大致流程是先構造NFA(本文內容),而後用子集構造法將NFA轉化爲DFA,預計將來我會出一篇博客講解細節和具體實現。
首先DFA引擎是能夠繼續優化的,使用Hopcroft算法能夠近一步將DFA圖壓縮,更少的狀態節點更少的轉移邊能夠實現更好的性能。其次,目前生產級的正則引擎不少都不是單純用NFA或者DFA實現的,而是兩者的結合,不一樣正則表達式下用不一樣的引擎能夠達到更好的綜合性能,簡單說NFA圖小但要回溯,DFA不須要回溯但有些狀況圖會特別大。敬請期待我後續博文。