從0到1打造正則表示式執行引擎
阿新 • • 發佈:2020-05-04
@[toc]
今天是五一假期第一天,這裡先給大家拜個晚 咳咳!!祝大家五一快樂,我這裡給大家奉上一篇硬核教程。首先宣告,這篇文章不是教你如何寫正則表示式,而是教你寫一個能執行正則表示式的**執行引擎**。 網上教你寫正則表示式的文章、教程很多,但教你寫引擎的並不多。很多人認為我就是用用而已,沒必要理解那麼深,但知道原理是在修煉內功,正則表示式底層原理並不單單是用在這,而是出現在計算機領域的各個角落。理解原理可以讓你以後寫字串匹配時正則表示式能夠信手拈來,理解原理也是觸類旁通的基礎。廢話不多說,直接開始正式內容。
本文是我個人做的動手實踐性專案,所以未完整支援所有語法,而且因為是用NFA實現的所以效能比生產級的執行引擎差好多。目前原始碼已開放至[https://github.com/xindoo/regex](https://github.com/xindoo/regex),後續會繼續更新,歡迎Star、Fork 提PR。
目前支援的正則語義如下:
- 基本語法: **. ? * + () |**
- 字元集合: **[]**
- 特殊型別符號: **\d \D \s \S \w \W**
## 前置知識
宣告:本文不是入門級的文章,所以如果你想看懂後文的內容,需要具備以下的基本知識。
0. 基本的程式設計知識,雖然這裡我是用java寫的,但並不要求懂java,懂其他語法也行,基本流程都是類似,就是語法細節不同。
1. 瞭解正則表示式,知道簡單的正則表示式如何寫。
2. 基本的資料結構知識,知道有向圖的概念,知道什麼是遞迴和回溯。
## 有限狀態機
有限狀態機(Finite-state machine),也被稱為有限狀態自動機(finite-state automation),是表示有限個狀態以及在這些狀態之間的轉移和動作等行為的數學計算模型(From [維基百科 狀態機](https://zh.wikipedia.org/wiki/%E6%9C%89%E9%99%90%E7%8A%B6%E6%80%81%E6%9C%BA)) 。 聽起來晦澀難懂,我用大白話描述一遍,狀態機其實就是用圖把狀態和狀態之間的關係描述出來,狀態機中的一個狀態可以在某些給定條件下變成另外一種狀態。舉個很簡單的例子你就懂了。
比如我今年18歲,我現在就是處於18歲的狀態,如果時間過了一年,我就變成19歲的狀態了,再過一年就20了。當然我20歲時時光倒流2年我又可以回到18歲的狀態。這裡我們就可以把我的年齡狀態和時間流逝之間的關係用一個自動機表示出來,如下。
![在這裡插入圖片描述](https://img2020.cnblogs.com/other/452826/202005/452826-20200504103856987-328174576.png)
每個圈代表一個節點表示一種狀態,每條有向邊表示一個狀態到另一個狀態的轉移條件。上圖中狀態是我的年齡,邊表示時間正向或者逆向流逝。
有了狀態機之後,我們就可以用狀態機來描述特定的模式,比如上圖就是年齡隨時間增長的模式。如果有人說我今年18歲,1年後就20歲了。照著上面的狀態機我們來算下,1年後你才19歲,你這不是瞎說嗎! 沒錯,狀態機可以來判定某些內容是否符合你狀態機描述的模式了。喲,一不小心就快扯到正則表示式上了。
我們這裡再引入兩種特殊的狀態:**起始態**和**接受態(終止態)**,見名知意,不用我過多介紹了吧,起始態和終止態的符號如下。
![在這裡插入圖片描述](https://img2020.cnblogs.com/other/452826/202005/452826-20200504103857305-1829451546.png)
我們拿狀態機來做個簡單的字串匹配。比如我們有個字串“zsx”,要判斷其他字串是否和"zxs"是一致的,我們可以為"zxs"先建立一個自動機,如下。
![在這裡插入圖片描述](https://img2020.cnblogs.com/other/452826/202005/452826-20200504103857503-1232594468.png)
對於任意一個其他的字串,我們從起始態0開始,如果下一個字元能匹配到0後邊的邊上就往後走,匹配不上就停止,一直重複,如果走到終止態3說明這個字串和”zxs“一樣。任意字串都可以轉化成上述的狀態機,其實到這裡你就知道如何實現一個只支援字串匹配的正則表示式引擎了,如果想支援更多的正則語義,我們要做的更多。
## 狀態機下的正則表示式
我們再來引入一條特殊的邊,學名叫$\epsilon$閉包(emm!看到這些符號我就回想起上學時被數學支配的恐懼),其實就是一條不需要任何條件就能轉移狀態的邊。
![在這裡插入圖片描述](https://img2020.cnblogs.com/other/452826/202005/452826-20200504103857703-1015039025.png)
沒錯,就只這條紅邊本邊了,它在正則表示式狀態機中起著非常重要的連線作用,可以不依賴其他條件直接跳轉狀態,也就是說在上圖中你可以直接從1到2。
有了 $\epsilon$閉包的加持,我們就可以開始學如何畫正則表示式文法對應的狀態機了。
### 串聯匹配
首先來看下純字元匹配的自動機,其實上面已經給過一個"zxs"的例子了,這裡再貼一下,其實就是簡單地用字串在一起而已,如果還有其他字元,就繼續往後串。
![在這裡插入圖片描述](https://img2020.cnblogs.com/other/452826/202005/452826-20200504103857503-1232594468.png)
兩個表示式如何傳在一起,也很簡單,加入我們已經有兩個表示式A B對應的狀態機,我們只需要將其用$\epsilon$串一起就行了。
![在這裡插入圖片描述](https://img2020.cnblogs.com/other/452826/202005/452826-20200504103858422-233546961.png)
### 並連匹配 (正則表示式中的 |)
正則表示式中的**|** 標識二選一都可以,比如**A|B** A能匹配 B也能匹配,那麼**A|B**就可以表示為下面這樣的狀態圖。
![在這裡插入圖片描述](https://img2020.cnblogs.com/other/452826/202005/452826-20200504103858596-240684105.png)
從0狀態走A或B都可以到1狀態,完美的詮釋了A|B語義。
### 重複匹配(正則表示式中的 ? + *)
正則表示式裡有4中表示重複的方式,分別是:
1. ?重複0-1次
2. + 重複1次以上
3. * 重複0次以上
4. {n,m} 重複n到m次
我來分別畫下這4種方式如何在狀態機裡表示。
#### 重複0-1次 ?
![在這裡插入圖片描述](https://img2020.cnblogs.com/other/452826/202005/452826-20200504103858829-1049659670.png)
0狀態可以通過E也可以依賴$\epsilon$直接跳過E到達1狀態,實現E的0次匹配。
#### 重複1次以上
![在這裡插入圖片描述](https://img2020.cnblogs.com/other/452826/202005/452826-20200504103859038-1825543515.png)
0到1後可以再通過$\epsilon$跳回來,就可以實現E的1次以上匹配了。
#### 重複0次以上
![在這裡插入圖片描述](https://img2020.cnblogs.com/other/452826/202005/452826-20200504103859198-701783234.png)
仔細看其實就是**? +**的結合。
#### 匹配指定次數
![在這裡插入圖片描述](https://img2020.cnblogs.com/other/452826/202005/452826-20200504103859371-419146799.png)
這種建圖方式簡單粗暴,但問題就是如果n和m很大的話,最後生成的狀態圖也會很大。其實可以把指定次數的匹配做成一條特殊的邊,可以極大減小圖的大小。
### 特殊符號(正則表示式中的 . \d \s……)
正則表示式中還支援很多某類的字元,比如**.**表示任意非換行符,**\d**標識數字,**[]**可以指定字符集…… ,其實這些都和圖的形態無關,只是某調特殊的邊而已,自己實現的時候可以選擇具體的實現方式,比如後面程式碼中我用了策略模式來實現不同的匹配策略,簡化了正則引擎的程式碼。
### 子表示式(正則表示式 () )
子表達可以Tompson演算法,其實就是用遞迴去生成**()**中的子圖,然後把子圖拼接到當前圖上面。(什麼Tompson說的那麼高大上,不就是遞迴嗎!)
### 練習題
來聯絡畫下 **a(a|b)*** 的狀態圖,這裡我也給出我畫的,你可以參考下。
![在這裡插入圖片描述](https://img2020.cnblogs.com/other/452826/202005/452826-20200504103859605-1351574795.png)
## 程式碼實現
### 建圖
看完上文之後相信你一直知道如果將一個正則表示式轉化為狀態機的方法了,這裡我們要將理論轉化為程式碼。首先我們要將圖轉化為程式碼標識,我用State表示一個節點,其中用了Map