正則表達(dá)式和NFA

作為前端大佬的你平斩,想必對(duì)于 JavaScript 的正則表達(dá)式非常熟悉了夫椭,甚至隨手就能利用正則表達(dá)式寫出一些驚世駭俗的代碼劣欢。只是不知道你是否有和我一樣的疑惑:正則表達(dá)式是怎么執(zhí)行的呢盛垦?

我們寫下這樣的正則表達(dá)式 (a+|b)c湿弦,然后用它來(lái)匹配字符串 aacdeabcde腾夯,這是怎樣的一個(gè)過(guò)程呢颊埃?

前段時(shí)間,我試著去查找蝶俱、學(xué)習(xí)相關(guān)的資料班利,然后知道了以下的內(nèi)容:

  • 目前正則表達(dá)式引擎主要有兩種:NFADFA
  • JavaScript 采用的是 NFA 引擎

那么 NFA 又是啥,跟 DFA 有什么不同榨呆?NFA 又是怎么實(shí)現(xiàn)正則表達(dá)式匹配的呢罗标?

接下來(lái),我試著用我自己的方式來(lái)介紹积蜻,希望也能幫助對(duì)此感興趣的你闯割。

NFA

NFA 是指 Nondeterministic Finite Automaton,非確定有限狀態(tài)自動(dòng)機(jī)竿拆。

有點(diǎn)深?yuàn)W宙拉,我剛看到的時(shí)候也很懵,咱們慢慢來(lái)丙笋。

先說(shuō)有限狀態(tài)機(jī)(Automation)谢澈,來(lái)個(gè)示例圖看下:

有限狀態(tài)機(jī)

狀態(tài)機(jī)中有這樣一些要素煌贴,對(duì)照上圖分別說(shuō)下:

  • 開始狀態(tài):圓圈表示狀態(tài),被一個(gè)“沒(méi)有起點(diǎn)的箭頭”指向的狀態(tài)澳化,是開始狀態(tài)崔步,上例中是 S1
  • 最終狀態(tài):也叫接受狀態(tài),圖中用雙圓圈表示缎谷,這個(gè)例子中也是 S1
  • 輸入:在一個(gè)狀態(tài)下井濒,向狀態(tài)機(jī)輸入的符號(hào)/信號(hào),不同輸入導(dǎo)致?tīng)顟B(tài)機(jī)產(chǎn)生不同的狀態(tài)改變
  • 轉(zhuǎn)換:在一個(gè)狀態(tài)下列林,根據(jù)特定輸入瑞你,改變到特定狀態(tài)的過(guò)程,就是轉(zhuǎn)換

所以有限狀態(tài)機(jī)的工作過(guò)程希痴,就是從開始狀態(tài)者甲,根據(jù)不同的輸入,自動(dòng)進(jìn)行狀態(tài)轉(zhuǎn)換的過(guò)程砌创。

上圖中的狀態(tài)機(jī)的功能虏缸,是檢測(cè)二進(jìn)制數(shù)是否含有偶數(shù)個(gè) 0。從圖上可以看出嫩实,輸入只有 1 和 0 兩種刽辙。從 S1 狀態(tài)開始,只有輸入 0 才會(huì)轉(zhuǎn)換到 S2 狀態(tài)甲献,同樣 S2 狀態(tài)下只有輸入 0 才會(huì)轉(zhuǎn)換到 S1宰缤。所以,二進(jìn)制數(shù)輸入完畢晃洒,如果滿足最終狀態(tài)慨灭,也就是最后停在 S1 狀態(tài),那么輸入的二進(jìn)制數(shù)就含有偶數(shù)個(gè) 0球及。

還是有點(diǎn)暈氧骤,這個(gè)和正則表達(dá)式有什么關(guān)系呢?

正則表達(dá)式吃引,可以認(rèn)為是對(duì)一組字符串集合的描述筹陵。例如 (a+|b)c 對(duì)應(yīng)的字符串集合是:

ac
bc
aac
aaac
aaaac
...

有限狀態(tài)機(jī)也可以用來(lái)描述字符串集合,同樣是正則表達(dá)式所描述的集合际歼,用有限狀態(tài)機(jī)來(lái)表示,可以是這樣的:

NFA - (a+|b)c

這里的 NFA 狀態(tài)圖是我用自己寫的頁(yè)面繪制出來(lái)的姑蓝,比較簡(jiǎn)陋鹅心,不過(guò)我相信你可以看懂。
你可以在這里(luobotang/nfa)自己試試看纺荧,只支持簡(jiǎn)單的正則表達(dá)式旭愧。

并且颅筋,有限狀態(tài)機(jī)是可以“執(zhí)行”的,給出如上的狀態(tài)機(jī)之后输枯,就可以用來(lái)對(duì)輸入的字符串進(jìn)行檢測(cè)议泵。如果最終匹配,也就意味著輸入的字符串和正則表達(dá)式 (a+|b)c 匹配桃熄。

所以先口,編程語(yǔ)言中的正則表達(dá)式,一般是通過(guò)有限狀態(tài)機(jī)來(lái)實(shí)現(xiàn)瞳收。正則表達(dá)式匹配字符串的過(guò)程碉京,可以分解為:

  • 正則表達(dá)式轉(zhuǎn)換為等價(jià)的有限狀態(tài)機(jī)
  • 有限狀態(tài)機(jī)輸入字符串執(zhí)行

到這里,我想你大概知道有限狀態(tài)機(jī)在正則表達(dá)式中的作用了螟深,當(dāng)然谐宙,只是具體實(shí)現(xiàn)還不清楚。

這里再講一下 NFA 和 DFA 的區(qū)別界弧。DFA 是 Deterministic Finite Automaton凡蜻,確定有限狀態(tài)機(jī)。DFA 可以認(rèn)為是一種特殊的 NFA垢箕,它最大的特點(diǎn)划栓,就是確定性。它的確定性在于舰讹,在一個(gè)狀態(tài)下茅姜,輸入一個(gè)符號(hào),一定是轉(zhuǎn)換到確定的狀態(tài)月匣,沒(méi)有其他的可能性钻洒。

舉個(gè)例子,對(duì)于正則表達(dá)式 ab|ac锄开,對(duì)應(yīng) NFA 可以是這樣的:

NFA - ab|ac

可以看到素标,在狀態(tài) 1 這里,如果輸入 a萍悴,其實(shí)有兩種可能头遭,如果后面的符號(hào)是 b,那么可以匹配成功癣诱,后面符號(hào)是 c 也能匹配成功计维。所以狀態(tài)機(jī)在執(zhí)行過(guò)程中,可能要嘗試所有的可能性撕予。在嘗試一種可能路徑匹配失敗后鲫惶,還要回到之前的狀態(tài)再嘗試其他的路徑,這就是“回溯”实抡。

但是 DFA 消除了這種不確定性欠母,所以可以想見(jiàn)欢策,其執(zhí)行性能應(yīng)該要比 NFA 更好,因?yàn)椴恍枰厮荨?/p>

NFA 是可以轉(zhuǎn)換為等價(jià)的 DFA 的赏淌,也就是說(shuō)踩寇,理論上講,正則表達(dá)式可以用 DFA 來(lái)實(shí)現(xiàn)六水,從而獲得優(yōu)于 NFA 的執(zhí)行性能俺孙。但是 NFA 轉(zhuǎn)換 DFA 的過(guò)程,會(huì)消耗更多資源缩擂,甚至最終得到的 DFA 要占用大量存儲(chǔ)空間(據(jù)有的資料的說(shuō)法鼠冕,可能會(huì)產(chǎn)生指數(shù)級(jí)增長(zhǎng))。而且胯盯,DFA 相比 NFA懈费,在實(shí)現(xiàn)一些正則表達(dá)式的特性時(shí)會(huì)更復(fù)雜,成本更高博脑。所以當(dāng)前的許多編程語(yǔ)言憎乙,其正則表達(dá)式引擎為 NFA 模式。

可以用如下的正則表達(dá)式測(cè)試當(dāng)前編程語(yǔ)言采用的引擎是否 NFA:

nfa|nfa not

用上面的正則表達(dá)式來(lái)測(cè)試字符串 nfa not叉趣,NFA 引擎在檢測(cè)滿足 nfa 就返回匹配成功的結(jié)果了泞边,而 DFA 則會(huì)嘗試?yán)^續(xù)查找汁汗,也就是說(shuō)會(huì)得到“最長(zhǎng)的匹配結(jié)果”膀捷。

從正則表達(dá)式到 NFA

了解了 NFA 在正則表達(dá)式中的應(yīng)用,接下來(lái)要介紹的是如何將正則表達(dá)式轉(zhuǎn)換得到對(duì)應(yīng)的 NFA严卖。這一部分會(huì)稍微有些枯燥烟具,不過(guò)對(duì)于深入理解正則表達(dá)式和 NFA 還是挺有幫助的梢什。

Thompson 算法

Thompson 算法用于轉(zhuǎn)換正則表達(dá)式為NFA,它并非最高效的算法朝聋,但是實(shí)用嗡午,易于理解。

Thompson 算法中使用最基本的兩種轉(zhuǎn)換:

Thompson 算法基本元素

普通轉(zhuǎn)換就是在一個(gè)狀態(tài)下冀痕,輸入字符a后轉(zhuǎn)換至另一個(gè)狀態(tài)荔睹;epsilon轉(zhuǎn)換則不需要有輸入,就從一個(gè)狀態(tài)轉(zhuǎn)換至另一個(gè)狀態(tài)言蛇。

正則表達(dá)式中的各種運(yùn)算僻他,可以通過(guò)組合上述兩種轉(zhuǎn)換實(shí)現(xiàn):

  • 組合運(yùn)算 RS
RS
  • 替換運(yùn)算 R|S

    R|S
  • 重復(fù)運(yùn)算 R*

    R*

上面圖中的 R、S 是有開始狀態(tài)和結(jié)束狀態(tài)的 NFA腊尚。

以正則表達(dá)式 ab|c 為例吨拗,包括兩個(gè)運(yùn)算:

  • ab 組合
  • ab 的結(jié)果,與 c 替換

這樣我們把正則表達(dá)式視為一系列輸入和運(yùn)算,進(jìn)行分解丢胚、組合,就可以得到最終的 NFA受扳。

首先携龟,我們要把正則表達(dá)式轉(zhuǎn)換為方便記錄輸入、運(yùn)算的方式勘高。

正則表達(dá)式 -> 后綴表達(dá)式

后綴表達(dá)式是一種方便記錄輸入峡蟋、運(yùn)算的表達(dá)式,本身已包含了運(yùn)算符的優(yōu)先級(jí)华望,也稱為 逆波蘭表示法(Reverse Polish Notation蕊蝗,簡(jiǎn)寫為 RPN)。

為方便記錄運(yùn)算赖舟,我們?yōu)檎齽t表達(dá)式中的組合運(yùn)算也創(chuàng)建一個(gè)運(yùn)算符“.”(本文只涉及最簡(jiǎn)單的正則表達(dá)式形式蓬戚,這里的“.”不是用于匹配任意字符的特殊符號(hào))。

正則表達(dá)式ab|c對(duì)應(yīng)的后綴表達(dá)式為 ab.c|宾抓。

這樣子漩,通過(guò)逐個(gè)掃描后綴表達(dá)式,并識(shí)別其中的運(yùn)算符來(lái)執(zhí)行石洗,就可以對(duì)后綴表達(dá)式進(jìn)行求解幢泼。對(duì)于正則表達(dá)式來(lái)說(shuō),則是在將其變?yōu)楹缶Y表達(dá)式后讲衫,通過(guò)“求值”的過(guò)程來(lái)進(jìn)一步構(gòu)建并得到最終的 NFA缕棵。

用于創(chuàng)建后綴表達(dá)式的是 調(diào)度場(chǎng)算法

對(duì)于這里的正則表達(dá)式處理的場(chǎng)景涉兽,算法的大致描述如下:

代碼在:regex2post() | nfa.js#L14 - luobotang/nfa

- 創(chuàng)建輸出隊(duì)列 output 和運(yùn)算符棧 ops
- 依次讀取輸入字符串中每一個(gè)字符 ch
  - 如果 ch 是普通字符招驴,追加到 output
  - 如果 ch 是運(yùn)算符,只要 ops 棧頂?shù)倪\(yùn)算符優(yōu)先級(jí)不低于 ch花椭,依次出棧并追加到 output忽匈,最后將 ch 入棧 ops
  - 如果 ch 是“(”,入棧 ops
  - 如果 ch 是“)”矿辽,只要 ops 棧頂不是“(”丹允,依次出棧并追加到 output
- 將 ops 中運(yùn)算符依次出棧追加到 output
- 返回 output

具體處理過(guò)程中,由于原始正則表達(dá)式中并沒(méi)有組合運(yùn)算符袋倔,所以需要自行判斷合理的插入位置雕蔽。

運(yùn)算符優(yōu)先級(jí)如下(由高到低):

  • * ? +
  • .
  • |
  • (

后綴表達(dá)式 -> NFA

基于后綴表達(dá)式創(chuàng)建 NFA,是一個(gè)由簡(jiǎn)單的 NFA 進(jìn)行不斷組合得到復(fù)雜 NFA 的過(guò)程宾娜。

用于表示狀態(tài) State 的數(shù)據(jù)結(jié)構(gòu)為:

// State
{
  id: String,
  type: String, // 'n' - normal, 'e' - epsilon, 'end'
  symbol: String, // 普通狀態(tài)對(duì)應(yīng)的輸入字符
  out: State, // 允許的下一個(gè)狀態(tài)
  out1: State // 允許的下一個(gè)狀態(tài)
}

每個(gè)狀態(tài)可以對(duì)應(yīng)最多兩個(gè) out 狀態(tài)批狐,像 a|b|c 的表達(dá)式,會(huì)被分解為 (a|b)|c,每次運(yùn)算符“|”都只處理兩個(gè)(子)表達(dá)式嚣艇。

在構(gòu)造最終 NFA 過(guò)程中承冰,每次會(huì)創(chuàng)建 NFA 的片段 Fragment:

// Fragment
{
  start: State,
  out: State
}

不管 NFA 片段內(nèi)部是怎樣復(fù)雜,它都只有一個(gè)入口(開始狀態(tài))食零,一個(gè)出口(最終狀態(tài))困乒。

這一部分代碼在:post2nfa() | nfa.js#L90 - luobotang/nfa

處理的過(guò)程大致為:

- 創(chuàng)建用于記錄 NFA 片段的棧 stack
- 依次讀取輸入的后綴表達(dá)式的每個(gè)字符 ch
  - 如果 ch 是運(yùn)算符,從 stack 出棧所需數(shù)目的 NFA 片段贰谣,構(gòu)建新的 NFA 片段后入棧 stack
  - 如果 ch 是普通字符娜搂,創(chuàng)建新的狀態(tài),并構(gòu)建只包含此狀態(tài)的 NFA 片段入棧 stack
- 返回 stack 棧頂?shù)?NFA 片段吱抚,即最終結(jié)果

以對(duì)組合運(yùn)算的處理為例:

const e2 = stack.pop()
const e1 = stack.pop()
e1.out.out = e2.start
stack.push(new Fragment(e1.start, e2.out))

從 stack 出棧兩個(gè) NFA 片段百宇,然后將其首尾相連后構(gòu)建新的 NFA 片段再入棧。

其他處理過(guò)程就不詳細(xì)介紹了秘豹,感興趣可以看下代碼携御。

NFA 的執(zhí)行

NFA 的執(zhí)行過(guò)程就是用當(dāng)前狀態(tài)來(lái)比對(duì)字符串的當(dāng)前字符,如果匹配就繼續(xù)比對(duì)下一個(gè)狀態(tài)和下一個(gè)字符既绕,否則匹配失敗因痛。

不過(guò)由于 NFA 的不確定性,所以可能會(huì)同時(shí)有多個(gè)匹配的狀態(tài)岸更。

我這里就簡(jiǎn)單粗暴了鸵膏,直接讓當(dāng)前所有的狀態(tài)都進(jìn)行比對(duì),仍然滿足條件的下一個(gè)狀態(tài)再繼續(xù)參與下一輪比對(duì)怎炊。一次只跟蹤一條路徑谭企,匹配失敗后再回溯肯定也是可以的,不過(guò)就要復(fù)雜很多了评肆。

代碼在:simulator.js - luobotang/nfa

總結(jié)

綜上债查,正則表達(dá)式的執(zhí)行,可以通過(guò)構(gòu)建等價(jià)的 NFA瓜挽,然后執(zhí)行 NFA 來(lái)匹配輸入的字符串盹廷。真實(shí)的 JavaScript 中的正則表達(dá)式擁有更多的特性,其正則表達(dá)式引擎也更加復(fù)雜久橙。

希望通過(guò)我的介紹俄占,能夠讓你對(duì)正則表達(dá)式有了更多的了解。當(dāng)然淆衷,水平有限缸榄,講得不當(dāng)?shù)牡胤皆谒y免,歡迎指正祝拯。

最后甚带,感謝閱讀!

參考資料

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市鹰贵,隨后出現(xiàn)的幾起案子晴氨,更是在濱河造成了極大的恐慌,老刑警劉巖碉输,帶你破解...
    沈念sama閱讀 216,324評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件瑞筐,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡腊瑟,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門块蚌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)闰非,“玉大人,你說(shuō)我怎么就攤上這事峭范〔扑桑” “怎么了?”我有些...
    開封第一講書人閱讀 162,328評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵纱控,是天一觀的道長(zhǎng)辆毡。 經(jīng)常有香客問(wèn)我,道長(zhǎng)甜害,這世上最難降的妖魔是什么舶掖? 我笑而不...
    開封第一講書人閱讀 58,147評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮尔店,結(jié)果婚禮上眨攘,老公的妹妹穿的比我還像新娘。我一直安慰自己嚣州,他們只是感情好鲫售,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,160評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著该肴,像睡著了一般情竹。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上匀哄,一...
    開封第一講書人閱讀 51,115評(píng)論 1 296
  • 那天秦效,我揣著相機(jī)與錄音,去河邊找鬼涎嚼。 笑死棉安,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的铸抑。 我是一名探鬼主播贡耽,決...
    沈念sama閱讀 40,025評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了蒲赂?” 一聲冷哼從身側(cè)響起阱冶,我...
    開封第一講書人閱讀 38,867評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎滥嘴,沒(méi)想到半個(gè)月后木蹬,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,307評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡若皱,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,528評(píng)論 2 332
  • 正文 我和宋清朗相戀三年镊叁,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片走触。...
    茶點(diǎn)故事閱讀 39,688評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡晦譬,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出互广,到底是詐尸還是另有隱情敛腌,我是刑警寧澤,帶...
    沈念sama閱讀 35,409評(píng)論 5 343
  • 正文 年R本政府宣布惫皱,位于F島的核電站像樊,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏旅敷。R本人自食惡果不足惜生棍,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,001評(píng)論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望媳谁。 院中可真熱鬧足绅,春花似錦、人聲如沸韩脑。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)段多。三九已至首量,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間进苍,已是汗流浹背加缘。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留觉啊,地道東北人拣宏。 一個(gè)月前我還...
    沈念sama閱讀 47,685評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像杠人,于是被迫代替她去往敵國(guó)和親勋乾。 傳聞我的和親對(duì)象是個(gè)殘疾皇子宋下,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,573評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容