正則表達式的一些探索(偏JavaScript)

簡單的探索下正則表達式的相關知識,首先先了解下正則表達式的引擎和匹配過程區別,再試着掌握如何在場景中編寫正則表達式,再然後探索下根據上文已知的原理和編寫過程怎麼去優化正則表達式,最後給出一些js里正則相關的小擴展。

基礎及原理簡單介紹

瞭解一下正則表達式的正則引擎(正則表達式使用的理論模型是有窮自動機,具體實現稱爲正則引擎)。

正則引擎分有DFA(確定型有窮自動機)的和NFA(非確定型有窮自動機)的實現,根據編譯相關知識的描述,兩者是可以等價轉換的。NFA又分傳統型和POSIX標準,下面是三者一個簡單的對照表:

-/-         回溯     忽略優先量詞     捕獲型括號       應用算法
DFA          N          N              N           文本主導
傳統型NFA     Y    ️      Y       ️       Y           表達式主導   
POSIX NFA    Y          N           ️   Y           表達式主導

回溯指的是:一個類似枚舉的搜索嘗試過程,在搜索嘗試過程中尋找問題的解,當在分歧點選擇一條路徑,記住另一/多條路徑供之後使用,在選擇路徑後的探索過程中發現不滿足求解條件時,就返回到分歧點,嘗試其他路徑(回溯遵循後進先出原則/LIFO-last in first out)。

忽略優先量詞指的是:支持儘可能少的匹配結果。

捕獲型括號指的是:匹配並捕獲結果,獲取組號。

文本主導指的是:匹配搜索過程中的每個字符都對匹配過程進行控制(這樣的結果就是:雖然我不能回溯,但我速度快呀 - DFA的傲嬌...)。

表達式主導指的是:表達式中的控制權在不同的表達式元素之間轉換,然後與文本進行匹配。

三者還有一些其他的差異,如編譯階段的處理及匹配最左最長原則等,由於文章裏以js的正則爲準,就不做更多區別的介紹,詳情可參考書籍《精通正則表達式》,不過值得注意的是本書中的內容不一定完全適合各個語言或平臺中的正則表達式...

正則匹配的過程規則總結:

1. 優先選擇最左端的匹配結果(匹配是從左到右開始的)

  匹配先從需要查找的字符串的第一個字符嘗試匹配;

  如果匹配到結果,則返回結果;

  如果匹配不到結果,則就需要從字符串的第二個字符位置開始重新匹配;

  以此類推,直到嘗試過所有的字符串位置作爲匹配起始點進行匹配後,返回匹配失敗結果。

  如 forever 中匹配是否存在 ever:

  第一輪:從f開始匹配,fore不匹配ever;

  第二輪:從o開始匹配,orev不匹配ever;

  第三輪:從r開始匹配,reve不匹配ever;

  第四輪:從e開始匹配,ever匹配ever,獲得ever匹配起始點(此處爲3)的索引和匹配結果。

2. 標準量詞總是匹配優先的(*、+、?、{m,n})

  匹配優先:儘可能多的匹配;忽略優先:儘可能少的匹配。

  標準量詞總是匹配優先的,如:foo 針對表達式取 fo|foo(下面匹配基於JavaScript)

  匹配優先結果:foo,/f[o]{1,2}/

  忽略優先結果:fo,/f[o]{1,2}?/,加?後表示忽略優先量詞

更多原理和匹配過程詳解可以參考文章"淺析正則表達式—(原理篇)"

在需求場景中構建正則表達式

要構建正則表達式,得先了解一些正則表達式的一些普通字符和元字符。這些可以參照"百度百科-正則表達式_符號",該表對於正則使用生疏的同學可以作字典參考,這裏對於這些字符就不多做贅述。

以幾個例子來說明下在有正則需求的場景中該如何來寫表達式。

例子一:查詢句子中某些單詞出現的次數。

首先分析情況得出:先給出個大致形狀,/pattern/g ,經測試發現:

"the weather's often cold in the north and windy in the east".match(/the/g).length
// output 4

輸出4,明顯不對,這時候還需要考慮單詞的邊界情況,需要在pattern兩邊加上限制\b,並且注意不需要匹配單詞邊界,於是得出:

/\bthe\b/g

再測一下:

"the weather's often cold in the north and windy in the east".match(/\bthe\b/g).length
// output 3

正確。

例子二:比如需要給數字加 ",(逗號)" 形成貨幣格式的場景,如1234567,變成1,234,567。

首先分析情況得出:逗號應該加在"左邊存在數字,右邊的數字個數是3的倍數"。

這裏用到正則的"非獲取匹配,反向肯定預查 : (?<=pattern)" 和 "非獲取匹配,正向肯定預查 : (?=pattern)",非獲取匹配指的是匹配但不獲取匹配值,正/反向肯定預查指的是正/反向匹配時在任何匹配條件的字符串開始處匹配查找字符串。

先反向肯定預查"左邊存在數字"的組合,給出(?<=\d);再正向肯定預查"右邊數字個數是3的倍數的組合",給出(?=(\d{3})+$);結合兩者,加上全局的匹配 g,得出:

/(?<=\d)(?=(\d{3})+$)/g

測試一下效果:

'123456789'.replace(/(?<=\d)(?=(\d{3})+$)/g,',')
// output  123,456,789
'23456789'.replace(/(?<=\d)(?=(\d{3})+$)/g,',')
// output  23,456,789
'1234'.replace(/(?<=\d)(?=(\d{3})+$)/g,',')
// output  1,234

例子三:比如想把雷老闆的Are you ok 歌詞處理一下。

首先不想替換掉"...",其次注意轉義字符,最後結合需要取的是引號內包含且非"的字符,於是得出:

 /\"([^\"\.]*)\"/g 

測試一下:

var str = `
Are you ok?
Auther Mr.Lei
1. "Thank you!"
2. "Are you ok?"
3. "Hello"
4. "Thank you"
5. "Thank you every much"
6. "How are you Indian Mi fans?"
7. "Do you like Mi 4i?"
8. "..."
`;

var reg = /\"([^\"\.]*)\"/g; 
var replaceStr = str.replace(reg,'\"???\"');
// output 
// Are you ok?
// Auther Mr.Lei
// 1. "???"
// 2. "???"
// 3. "???"
// 4. "???"
// 5. "???"
// 6. "???"
// 7. "???"
// 8. "..."

如何寫正則表達式的總結:先寫出明確的匹配條件,再根據需求去組合或添加細節。

正則表達式優化的一些探索

《精通正則表達式》書中和網上查找了不少資料,並且根據對NFA的理解,列出了一些符合理論的優化建議(不排除更多可能):

  1. 減少或者優化判斷分支;

  2. 精確匹配條件(字符和量詞);

  3. 限制匹配優先的作用範圍,如+和*的區別,+減少了多選結構的回溯次數;

  4. 節省引用(使用非獲取匹配);

  5. 將複雜的多條件正則拆分成多個簡單的單條件正則;

  6. 錨點的優化,^(a|b)而非(^a|^b);

  7. 量詞等價轉換的效率差異(因語言而異);

  8. 使用固化分組(在支持的情況下,js並不支持);

  9. 使用變量存儲正則表達式(減少正則編譯過程);

  10. 還有更多細節...

《精通正則表達式》的第五及第六章都有涉及這些優化的介紹,網上google/baidu也是不少資料和案例。

根據其主要做的優化過程大致可總結出以下幾點(不排除更多可能):

  1. 減少匹配的回溯次數,減少時間;

  2. 節省引用(使用非獲取匹配),減少空間;

  3. 正則表達式自身優化以達到編譯最優;

  4. 也許更多吧...

以上是書籍和網上給出的優化建議,給出的案例大多是針對的perl和PHP的,在JavaScript中並不適用,爲了驗證js裏的正則如何優化,下面是我給出的一些測試結果...

基於JavaScript的正則表達式...這一小節給不出完整的測試代碼,因爲在多個瀏覽器和nodejs端跑了很多測試代碼,給定的情況是不同的...

但單獨某個端或者瀏覽器給出的結果是穩定的,以下是給出Chrome和Firefox瀏覽器和Nodejs各跑100000次(原先是1W長度字符串跑1000次,後面爲了效果更明顯最終增加爲10W長度字符串跑10W次)測試的結果總結...還有Safari瀏覽器、360瀏覽器、QQ瀏覽器的測試結果也統一不了.

Chrome瀏覽器下:
    使用.、*這類元字符或範圍比精確查找條件的性能要優;
    非貪婪模式和貪婪模式的效率差別大,貪婪模式性能優;
    獲取匹配和非獲取匹配效率差別不大,按平均值來看獲取匹配性能優;
    優化判斷分支(將最可能匹配的結果放前面)後性能更差;

Firefox瀏覽器下:
    精確查找條件比使用.、*這類元字符或者範圍的性能優;
    貪婪模式比非貪婪模式性能優;
    獲取匹配比非獲取匹配性能優;
    優化判斷分支(將最可能匹配的結果放前面)後性能更差;

Nodejs環境下:
    使用.、*這類元字符或範圍比精確查找條件的性能優;
    貪婪模式比非貪婪模式性能優;
    獲取匹配比非獲取匹配性能優;
    優化判斷分支(將最可能匹配的結果放前面)性能優;

以上的測試結果並不能給出肯定的答案,所以建議在JavaScript裏使用正則的時候先測一下性能以免導致不愉快的意外,至於JavaScript中正則表達式的實現過程更是需要看V8的代碼了...

JavaScript RegExp 小擴展

lastIndex 和 test 方法的愛恨情仇

var str = 'abcabc';
var regexp = /a/g;

console.log(regexp.test(str));  // output true 
console.log(regexp.lastIndex);  // output 1 

console.log(regexp.test(str));  // output true 
console.log(regexp.lastIndex);  // output 4

console.log(regexp.test(str));  // output false 
console.log(regexp.lastIndex);  // output 0

test 方法在執行後將 lastIndex 屬性值置爲匹配到的結果索引值,並返回匹配結果;下一次執行 test 方法時從 lastIndex 位置開始匹配,並返回匹配結果;以此類推;最後一次執行 test 方法,索引重置爲0,匹配結果爲false。

RegExp vs indexOf 誰纔是快男

在固定匹配值和"只匹配一次"的條件下,針對判斷字符串是否包含某個"固定的"字符串,indexOf 優於 regexp,性能將近一半。

var str = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36";

console.time('reg');
/Mac/.test(str);
console.timeEnd('reg');

console.time('indexOf');
str.indexOf('Mac');
console.timeEnd('indexOf');

經過多次測試  reg 的平均值 是 indexOf 的近2倍(環境:chrome & nodejs);然而在其他匹配條件下,RegExp 更靈活,更方便,更全面。

關於String.prototype.indexOf方法內部代碼暫時不知道怎麼去探究,根據以下文檔有解釋,但未明確的理解,參考文檔:

[ECMAScript 2015 (6th Edition, ECMA-262)]

[ECMAScript 1st Edition (ECMA-262)](初始定義)

注意:本文只是一次知識探討,僅供做參考作用,請勿以之爲準,如果需要更進一步的學習請以書籍以及自我實踐所得爲準。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章