正規表示式系列之中級進階篇

黃Java發表於2018-07-09

概述

本文主要通過介紹正規表示式中的一些進階內容,讓讀者瞭解正規表示式在日常使用中用到的比較少但是又比較重要的一部分內容,從而讓大家對正規表示式有一個更加深刻的認識。

本文的主要內容為:

  • 正規表示式回溯法原理
  • 正規表示式操作符優先順序

本文不介紹相關正規表示式的基本用法,如果對正規表示式的基本使用方法還不瞭解的同學,可以閱讀我的上一篇部落格——正規表示式系列之初級入門篇

回溯法原理

回溯是影響正規表示式效率的一個非常重要的原因,我們在進行正規表示式匹配時,一定要儘可能的避免回溯。

很多人可能只是對聽說過“回溯法”,並不瞭解其中具體內容和原理,接下來就先讓我們看下什麼是回溯法。

回溯法的定義

回溯法就是指正規表示式從頭開始依次進行匹配,如果匹配到某個特定的情況下時,發現無法繼續進行匹配,需要回退到之前匹配的結果,選擇另一個分支繼續進行匹配中的現象。這個描述可能有點抽象,我們舉一個簡單的例子,讓大家能夠更加明確的理解回溯法:

const reg = /ab{1,3}c/;

const str = 'abbc';

// 第1步:匹配/a/,得到'a'
// 第2步:匹配/ab{1}/,得到'ab'
// 第3步:匹配/ab{2}/,得到'abb'
// 第4步:匹配/ab{3}/,匹配失敗,需要進行回溯
// 第5步:回溯到/ab{2}/,繼續匹配/ab{2}c/,得到'abbc'
// 第6步:正規表示式匹配完成,得到'abbc'
複製程式碼

如果我們把正規表示式的各個分支都整理成一棵樹的話,正規表示式的匹配其實就是一個深度優先搜尋演算法。而回溯其實就是在進行深度優先匹配失敗後的後退正常操作邏輯。

如果退回到了根節點仍然無法匹配的話,就會將index向後移動一位,重新構建匹配數。即/bc/'abc'時,由於第一個字元'a'無法匹配,則移動到'b'開始匹配。

回溯法產生場景

理解了回溯法和回溯操作,接下來我們來看下什麼場景下會出現回溯。出現回溯的場景主要有以下幾種:

  1. 貪婪量詞(貪婪匹配)
  2. 惰性量詞(非貪婪匹配)
  3. 分支結構(分支匹配)

接下來,讓我們一個一個來看下這些場景是如何出現回溯的。

貪婪量詞(貪婪匹配)

const reg = /ab{1,3}c/;

const str = 'abbc';

// 第1步:匹配/a/,得到'a'
// 第2步:匹配/ab{1}/,得到'ab'
// 第3步:匹配/ab{2}/,得到'abb'
// 第4步:匹配/ab{3}/,匹配失敗,需要進行回溯
// 第5步:回溯到/ab{2}/,繼續匹配/ab{2}c/,得到'abbc'
// 第6步:正規表示式匹配完成,得到'abbc'
複製程式碼

最開始的例子其實就是一個貪婪匹配的示例,通過儘可能多的匹配b從而導致了回溯。

惰性量詞(非貪婪匹配)

const reg = /ab{1,3}?c/;

const str = 'abbc';

// 第1步:匹配/a/,得到'a'
// 第2步:匹配/ab{1}/,得到'ab'
// 第3步:匹配/ab{1}c/,匹配失敗,需要進行回溯
// 第4步:回溯到/ab{1}/,繼續匹配/ab{2}/,得到'abb'
// 第5步:匹配/ab{2}c/,得到'abbc'
// 第6步:正規表示式匹配完成,得到'abbc'
複製程式碼

與貪婪匹配類似,非貪婪匹配雖然每次都是去最小匹配數目,但是也會出現回溯的情況。

分支結構(分支匹配)

const reg = /(ab|abc)d/;

const str = 'abcd';

// 第1步:匹配/ab/,得到'ab'
// 第2步:匹配/abd/,匹配失敗,需要進行回溯
// 第3步:回溯到//,繼續匹配/abc/,得到'abc'
// 第4步:匹配/abcd/,得到'abcd'
// 第5步:正規表示式匹配完成,得到'abcd'
複製程式碼

通過上面的示例我們可以看到,分支結構在出現兩個分支情況類似的時候,也會出現回溯的情況,在這種情況下,如果一個分支無法匹配,則會回到這個分支的最初情況來重新進行匹配。

正規表示式操作符優先順序

看完了回溯法,下面我們來了解下關於正規表示式操作符的優先順序。

我們直接看結論,然後再根據結論來給大家提供示例進行理解。

操作符描述 操作符 優先順序
轉移符 \ 1
小括號和中括號 (…)、(?:…)、(?=…)、(?!…)、[…] 2
量詞限定符 {m}、{m,n}、{m,}、?、*、+ 3
位置和序列 ^、$、\元字元、一般字元 4
管道符 | 5

通過操作符的優先順序,我們能夠知道如何來讀一個正規表示式。以下面這個正規表示式為例,我們來介紹一下按照優先順序進行分析的方法:

const reg = /ab?(c|de*)+|fg/;

// 第一步,根據優先順序先考慮(c|de*)+,再根據優先順序拆分得到c de*,即匹配c或者de*(注意,位置和序列的優先順序高於管道符|,所以是c或de*而不是c或d和e*)
// 第二步,得到ab?,根據優先順序拆分得到a和b?
// 第三步,得到fg,這個內容和第一步+第二步的結果為或的關係
複製程式碼

最終,我們得到的效果如下:

image.png

通過這個圖,大家就能夠理解我們的分析思路:先找括號,括號中的一定為一個整體(轉移符只做轉義,不分割正則,因此可以認為第一優先順序其實是括號),沒有括號後再從左到右按照優先順序進行分析。量詞限定符則看做是正則的一個整體。

注:如果大家需要話類似的正規表示式流程圖,可以使用此網站

根據上面的優先順序,我們就能夠避免在正規表示式的理解中出現歸類錯誤的情況。

總結

本文通過介紹在正規表示式中容易被忽略的兩個內容:回溯法操作優先順序,讓大家能夠在進行正則的閱讀和書寫過程中避免踩到相關的坑。

參考內容

  1. 《JavaScript正規表示式迷你書》——老姚 V1.1
  2. 《JavaScript權威指南》

相關文章