在JavaScript正則表達式(2)中,我們一起學習了正則表達式的入門進階功能,比如反向引用,分組匹配,環視,一丟丟引擎的概念,NFA,DFA,以及2個基本原則。下面我們將更加深入了解正則表達式
NFA,“表達式主導”引擎###
2種引擎的根本差異來自于他們各自有著不同的應用算法。NFA
被稱為“表達式主導”引擎,而DFA
被成為“文本主導引擎”。什么叫表達式主導呢?請看如下代碼:
var reg = /a(cat|dog|deer|lion)/;
var text = '1234adog';
當reg
去匹配text
的時候,正則表達式從a
開始,每次只檢查一部分(由引擎查看表達式的一部分),同時檢查text
是否匹配表達式的當前部分。如果是,則繼續表達式的下一部分,如此繼續下去,一直到匹配完所有的正則表達式,那么整個表達式就算是匹配成功。
上述正則的匹配過程是,正則從a
開始,去匹配text
里的1
,失敗,然后text
往后移,匹配2
······一直到匹配到a
,滿足條件,然后是cat|dog|deer|lion
,它也是會一個一個嘗試,先是cat
中的c
,發現不匹配,跳到下一種可能,dog
中的d
,匹配,o
匹配,g
匹配。匹配完成,退出匹配并返回結果。像這種,正則表達式的控制權在不同元素之間來回轉換,我們稱之為“表達式主導”。因為控制權在正則表達式本身,而不是文本,所以,可以通過不同的寫法,讓匹配的過程變得更簡潔,更快捷。正因為如此,NFA
的這種特性給我們提供了豐富的創造性思維空間。一個好的正則表達式能帶來許多收益,而一個不好的,可能會帶來嚴重的后果。
回溯###
回溯是NFC
一個相當重要的概念,它的作用是當引擎在處理各個子表達式或者元素時,遇到需要在2個或者多個之間選擇一個的時候,會選擇一個,并記錄下另外一個。你可以理解為游戲存檔,在有多個選擇的時候,存一下檔,進入其中一個,如果失敗之后,選擇最近的一次回檔,并選擇另外一個。
需要作出選擇的情形包括量詞(決定是否嘗試另一個匹配)和多選結構(決定選擇哪一個多選分支)
下面我們將討論有多種選擇時,哪種選擇優先,匹配失敗時,應該回溯到什么狀態。
如果需要在進行嘗試和跳過嘗試之間選擇,對于匹配優先量詞,引擎會選擇嘗試匹配,而忽略優先量詞會選擇跳過匹配。
回溯到哪里?
距離當前最近存儲的選項就是當本地失敗強制回溯時返回的。使用的原則是后進先出(LIFO)。
總結正則中一些樸實而實用的技巧#####
前略,相信大家已經了解并掌握的正則的基本知識,下面讓我們帶著這些知識,在實戰中來處理更加復雜的問題。正則中的平衡法則:
- 只匹配我們期望的,不匹配我們不期望的文本。
- 易于控制和理解。
- 要保證效率,如果能匹配,必須很快返回結果,如果不能匹配,應該盡快報告匹配失敗。
先看下面一個例子:
var text = 'myName=moonburn . \moonburn';//需要匹配這個字符串
var reg = /^\w+=.*\\\w*/gi;
reg.test(text)//false
不要吃驚,是的,匹配失敗了,但是講道理不應該失敗的,對嗎?讓我們仔細分析一波,這個正則的問題在于\,很突兀,對不對。不信?我們來證明一下:
var text = 'myName=moonburn . moonburn';//去掉了\
var reg = /^\w+=.*\w*/gi;
reg.test(text)//true
匹配成功,果然是\的問題。
下面來說一下\的坑....
在JavaScript中,\和其他的語言是不太一樣的,舉個例子:
var text = '\abc';
console.log(text)//abc
\不見了!再看下一個例子:
'\a' === 'a'//true
解析的時候,就把\自動忽略了?不太完整。再看下面一個例子:
'\n' === 'n'//false
說明并不是忽略,是能轉義的時候轉義,不能轉義的時候忽略!。
所以,回到之前的例子:
var text = 'myName=moonburn . \moonburn';//需要匹配這個字符串
var reg = /^\w+=.*(\\)\w*/gi;
RegExp.$1//''
括號捕獲失敗,因為根本就沒有\,被忽略掉了。所以,應該這樣:
var text = 'myName=moonburn . \\moonburn';//需要匹配這個字符串
var reg = /^\w+=.*\\\w*/gi;
reg.test(text)//true
當然,我們發現,在使用.*
去匹配的時候,因為是匹配優先,會匹配全部,然后在通過回溯,退回到\,在進行下一部分的匹配。這樣明顯不太符合我們說的第三點,沒有保證效率。所以我們可以用[^\\\]*
去代替.*
,這樣,一旦匹配到了\,就會停下來,進行下一部匹配,沒有回溯,效率自然高了。代碼如下:
var text = 'myName=moonburn . \\moonburn';//需要匹配這個字符串
var reg = /^\w+=[^\\]*\\\w*/gi;
reg.test(text)//true
最后,得出第一條結論:盡量不要用.*
去匹配,而是用[^···]
去替換,如果選擇項比較少,也可以使用(··|··)
的形式,選擇項太多使用(··|··)
就得不償失了。
下面再看一個例子,如何匹配一個IP地址:
一般情況下,IP地址都是由小于3位的數字加上.
號組合而成,就像這樣000.001.002.003
。首先我們想到的,應該是這樣的形式進行匹配^[0-9][0-9][0-9]\.[0-9][0-9][0-9]\.[0-9][0-9][0-9]\.[0-9][0-9][0-9]$
:
var ip = '000.001.002.003';
var reg =/^[0-9][0-9][0-9]\.[0-9][0-9][0-9]\.[0-9][0-9][0-9]\.[0-9][0-9][0-9]$/;
reg.test(ip)//true
覺得寫法太臃腫?可以把[0-9]
替換成\d
,雖然對于引擎來說本質上沒有區別。而且,像這樣的寫法一定要求3位,顯得太死板了,一些ip不一定滿足3位,我們也應該匹配通過,所以我們通過如下去匹配:
var ip = '1.21.34.211';
var reg =/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
reg.test(ip)//true
嗯,差不多已經很接近了,不過稍微了解一點IP常識的話,就會知道,最大3位數不會超過255
,而我們這樣的匹配法,可以一直匹配到999
,不符合第一條規定,所以我們要縮小范圍。應該如何縮小范圍呢?首先想到了使用(··|··)
的形式,當然,不會是(0|1|2···|225)
這樣子,這樣太慢了。下面我們在仔細分析一下這樣的結構,首先可以使單個的數字所以(\d|···)
,然后可以是2位數,也是沒有限制的,所以變成(\d|\d\d|···)
,只有3位數的時候,會有限制,255,所以當第一位數是0,1的時候,也是沒有任何限制的,也就是(\d|\d\d|[01]\d\d|···)
,最后,我們只剩下,3位數,并且第一位數是2的情況,繼續分析第二位數,只要比5小,都是沒有限制的,所以可以分成(\d|\d\d|[01]\d\d|2[0-4]\d|···)
分析到了這里,最后的情況也明朗了,最終的版本,也就是(\d|\d\d|[01]\d\d|2[0-4]\d|25[0-5])
。具體代碼如下:
var ip = '1.21.34.211';
var reg =/^((\d|\d\d|[01]\d\d|2[0-4]\d|25[0-5])\.){3}(\d|\d\d|[01]\d\d|2[0-4]\d|25[0-5])$/;
reg.test(ip);//true
還能再簡單點嗎?###
能...
如果使用?
,上述的正則表達式還能更加簡略一點,變為([01]?\d?\d|2[0-4]\d|25[0-5])
。代碼如下:
var ip = '1.21.34.241';
var reg =/^(([01]?\d?\d|2[0-4]\d|25[0-5])\.){3}([01]?\d?\d|2[0-4]\d|25[0-5])$/;
reg.test(ip);//true
這個例子本身不難,需要掌握的是分析的過程,一層一層分析,最后在修改,優化。
當然,有人會問,為什么不使用環視呢?只需要環視.
之后是否滿足條件就ok啊,我也想過,首先JavaScript沒有反向環視,只能lookahead,所以第一個.
之前的數字要自己判斷,環視的寫法也是類似與(?=([01]?\d?\d|2[0-4]\d|25[0-5]))
并沒有優化,而且環視的價值在于判斷字符不用占位符,這里明顯是不需要這樣做的。所以不考慮了。
JavaScript 正則表達式(1)
JavaScript 正則表達式(2)
JavaScript 正則表達式(3)
JavaScript 正則表達式(4)