【正規表示式系列】貪婪與非貪婪模式

撒網要見魚發表於2017-12-18

前言

貪婪模式和非貪婪模式是正則匹配中的重要特性
在理解貪婪和非貪婪的區別時,可以根據例項,一步一步的循序漸進

大綱

  • 匹配規則簡介
  • 貪婪模式與非貪婪模式快速理解
  • 例項練習
  • 回溯現象與匹配失敗

匹配規則簡介

var str=`aabcab`;
var reg=/ab/;
var res=str.match(reg);

// ab index 為 1
console.log(res);

要快速理解正則的匹配規則,可以先嚐試理解上述的例子

匹配步驟是這樣的:

  • 初始index=0,匹配到了字元a
  • 接下來匹配下一個字元a,但是由於aa/ab/不匹配,因此此次匹配失敗
  • index挪到下一個,從1開始,又重新匹配了a
  • 接下來匹配下一個字元b,剛好和/ab/匹配,因此此次匹配成功,返回了abindex=1
  • 如果正則的匹配後面有g這種關鍵字,則會繼續開始下一組的匹配(但是本例中沒有g,因此只有一組結果)

要點

  • 最先開始的匹配擁有最高的優先權

這一個要點的詳細解釋是: 例如第一個匹配的字元是a,假設之後的匹配沒有出現匹配失敗的情況。則它將一直匹配下去,直到匹配完成,也就是說index=0不會變,直到匹配完成(如果出現匹配失敗並且無法回溯,index才會繼續往下挪)

這一點適用於下面的貪婪模式與非貪婪模式中(並且優先順序高於它們),因此請謹記

貪婪模式與非貪婪模式快速理解

貪婪匹配模式

定義

正規表示式去匹配時,會盡量多的匹配符合條件的內容

識別符號

+?*{n}{n,}{n,m}

匹配時,如果遇到上述識別符號,代表是貪婪匹配,會盡可能多的去匹配內容

示例

var str=`aacbacbc`;
var reg=/a.*b/;
var res=str.match(reg);

// aacbacb index為0
console.log(res);

上例中,匹配到第一個a後,開始匹配.*,由於是貪婪模式,它會一直往後匹配,直到最後一個滿足條件的b為止,因此匹配結果是aacbacb

示例2

var str=`aacbacbc`;
var reg=/ac.*b/;
var res=str.match(reg);

// acbacb index為1
console.log(res);

第一個匹配的是a,然後再匹配下一個字元a時,和正則不匹配,因此匹配失敗,index挪到1,接下來匹配成功了ac,繼續往下匹配,由於是貪婪模式,儘可能多的去匹配結果,直到最後一個符合要求的b為止,因此匹配結果是acbacb

非貪婪匹配模式

定義

正規表示式去匹配時,會盡量少的匹配符合條件的內容
也就是說,一旦發現匹配符合要求,立馬就匹配成功,而不會繼續匹配下去(除非有g,開啟下一組匹配)

識別符號

+???*?{n}?{n,}?{n,m}?

可以看到,非貪婪模式的識別符號很有規律,就是貪婪模式的識別符號後面加上一個?

示例

var str=`aacbacbc`;
var reg=/a.*?b/;
var res=str.match(reg);

// aacb index為0
console.log(res);

上例中,匹配到第一個a後,開始匹配.*?,由於是非貪婪模式,它在匹配到了第一個b後,就匹配成功了,因此匹配結果是aacb

為什麼是aacb而不是acb呢?
因為前面有提到過一個正在匹配的優先規則: 最先開始的匹配擁有最高的優先權
第一個a匹配到了,只要之後沒有發生匹配失敗的情況,它就會一直匹配下去,直到匹配成功

示例2

var str=`aacbacbc`;
var reg=/ac.*?b/;
var res=str.match(reg);

// acb index為1
console.log(res);

先匹配的a,接下來匹配第二個a時,匹配失敗了index變為1,繼續匹配ac成功,繼續匹配b,由於是非貪婪模式,此時acb已經滿足了正則的最低要求了,因此匹配成功,結果為acb

示例3

var str=`aacbacbc`;
var reg=/a.*?/;
var res=str.match(reg);

// a index為0
console.log(res);

var reg2=/a.*/;
var res2=str.match(reg2);

// aacbacbc index為0
console.log(res2);

這一個例子則是對示例1的補充,可以發現,當後面沒有b時,由於是非貪婪模式,匹配到第一個a就直接匹配成功了
而後面一個貪婪模式的匹配則是會匹配所有

例項練習

在初步理解了貪婪模式與非貪婪模式後,可以通過練習加深理解

提取HTML中的Div標籤

給出一個HTML字串,如下

其它元素
<div><span>使用者:<span/><span>張三<span/></div>
<div><span>密碼:<span/><span>123456<span/></div>
其它元素

__需求__: 提取出div包裹的內容(包括div標籤本身),並將各個結果存入陣列

__程式碼__: 通過非貪婪模式的全域性匹配來完成,如下

var reg=/<div>.*?</div>/g;
var res=str.match(reg);

// ["<div><span>使用者:<span/><span>張三<span/></div>", "<div><span>密碼:<span/><span>123456<span/></div>"]
console.log(res);

__詳解__: 用到了兩個知識點,.*?的非貪婪模式匹配以及g全域性匹配

  • <div>.*?</div>代表每次只會匹配一次div,這樣可以確保每一個div不會越界
  • 最後的g代表全域性匹配,即第一次匹配成功後,會將匹配結果放入陣列,然後從下一個index重新開始匹配新的結果

__另外__: 假設使用了/<div>.*</div>/g進行貪婪模式的匹配,結果則是

["<div><span>使用者:<span/><span>張三<span/></div><div><span>密碼:<span/><span>123456<span/></div>"]

因為貪婪模式匹配了第一個<div>後會無限貪婪的匹配接下來的字元,直到最後一個符合條件的</div>為止,導致了將中間所有的div標籤都一起匹配上了

提取兩個""中的子串,其中不能再包含""

示例引用自: 正規表示式之 貪婪與非貪婪模式詳解

"The phrase "regular expression" is called "Regex" for short"

__需求__: 提取兩個引號之間的子串,其中不能再包括引號,例如上述的提取結果應該是: "regular expression""Regex"(每一個結束的"後面都接空格)

__錯誤解法__: 通過如下的非貪婪匹配(請注意空格)

var str=`"The phrase "regular expression" is called "Regex" for short"`;
var reg=/".*?" /g;
var res=str.match(reg);

// [`"The phrase "regular expression"  `, `"Regex"  `]
console.log(res);

可以看到,上述的匹配完全就是匹配錯誤了,這個正則匹配到第一個符合條件的"+空格後就自動停下來了

__正確解法__: 使用貪婪模式進行匹配

var reg=/"[^"]*" /g;
var res=str.match(reg);

// [`"regular expression" `, `"Regex" `]
console.log(res);

這個匹配中

  • 從第一個"開始匹配,接下來到12位時("r"),不滿足[^"],也不滿足之後的"+空格,因此匹配失敗了,index挪到下一個,開始下一次匹配
  • 第二個匹配從"r"開始,一直匹配到n"空格空格,這一組剛剛好匹配成功(因為最後符合了正則的"空格),匹配好了"regular expression"空格
  • 第三個匹配匹配到了"Regex"空格(過程不再複述)
  • 到最後時,僅剩一個"直接匹配失敗(因為首先得符合"才能開始掙扎匹配)
  • 至此,正則匹配結束,匹配成功,並且符合預期

__最後__: 這個例子相對來說複雜一點,如要更好的理解,可以參考引用來源中的文章,裡面有就原理進行介紹
另外,參考文章中還有對非貪婪模式的匹配失敗,回溯影響效能等特性進行原理分析與講解

回溯現象與匹配失敗

你真的已經理解了貪婪模式和非貪婪模式麼?

回溯現象

不知道對上面最後例子中提到的回溯這詞有沒有概念?
這裡仍然以上例引用來源中的示例來分析

原字串

"Regex" 

貪婪匹配過程分析

".*" 
  • 第一個"取得控制權,匹配正則中的",匹配成功,控制權交給.*
  • .*取得控制權後,匹配接下來的字元,.代表匹配任何字元,*代表可匹配可不匹配,這屬於貪婪模式的識別符號,會優先嚐試匹配,於是接下來從1位置處的R開始匹配,依次成功匹配了Regex,接著繼續匹配最後一個字元",匹配成功,這時候已經匹配到了字串的結尾,所以.*匹配結束,將控制符交給正則式中最後的"
  • "取得控制權後,由於已經是到了字串的結尾,因此匹配失敗,向前查詢可供回溯的狀態,控制權交給.*.*讓出一個字元",再把控制權交給",此時剛好匹配成功
  • 至此,整個正規表示式匹配完畢,匹配結果為"Regex",匹配過程中回溯了1

非貪婪匹配表示式

".*?" 
  • 第一個"取得控制權,匹配正則中的",匹配成功,控制權交給.*?
  • .*?取得控制權後,由於這是非貪婪模式下的識別符號,因此在可匹配可不匹配的情況下會優先不匹配,因此嘗試不匹配任何內容,將控制權交給",此時index1處(R字元處)
  • "取得控制權後,開始匹配1處的R,匹配失敗,向前查詢可供回溯的狀態,控制權交給.*?.*?吃進一個字元,index到了2處,再把控制權交給"
  • "取得控制權後,開始匹配2處的e,匹配失敗,重複上述的回溯過程,直到.*?吃進了x字元,再將控制權交給
  • "取得控制權後,開始匹配6處的",匹配成功
  • 至此,整個正規表示式匹配完畢,匹配結果為”Regex”,匹配過程中回溯了5

優化去除回溯

上述的貪婪匹配中,出現了一次回溯現象,其實也可以通過優化表示式來防止回溯的,比如

"[^"]*"

這個表示式中構建了一個子表示式-[]中的^",它的作用是排除"匹配,這樣*的貪婪匹配就不會主動吃進",這樣最後就直接是"匹配",匹配成功,不會進行回溯

總結

上述的分析中可以看出,在匹配成功的情況下,貪婪模式進行了更少的回溯(可以自行通過更多的實驗進行驗證),因此在應用中,在對正則掌握不是很精通的情況下,可以優先考慮貪婪模式的匹配,這樣可以避免很多效能上的問題

匹配失敗的情況

上述的回溯分析都是基於匹配成功的情況,那如果是匹配失敗呢?

var str = `"Regex`
var reg = /"[^"]*"/g;

這個原字元中,沒有最後的",因此匹配是會失敗的,它的過程大致如下

  • "匹配",接著[]^"*匹配Regex
  • 接著到了最後,"獲取控制權,由於到了最後,開始回溯
  • 依次回溯的結果是*讓出xegeR,直到*已經無法再讓出字元,第一輪匹配失敗
  • 接著index開始往下挪,依次用"匹配Regex都失敗了,一直到最後也沒有再匹配到結果,因此此次正規表示式的匹配失敗,沒有匹配到結果(或者返回null)

那非貪婪模式呢?

/"[^"]*?"/g
  • "匹配",接著*嘗試不匹配,"匹配R,失敗,然後回溯,*吃進R
  • 接下來類似於上一步,*依次回溯吃進egex,一直到最後,*再次回溯想吃進時,已經到了字串結尾了,無法繼續,因此第一輪匹配失敗
  • 接著index開始往下挪,依次用"匹配Regex都失敗了,返回null

總結

通過匹配失敗的例子可以看出貪婪和非貪婪的模式區別。貪婪是先吃進,回溯再讓出,非貪婪是先忽略,回溯再吃進

而且,在匹配失敗的情況下,貪婪模式也會進行不少的回溯(非貪婪當然一直都很多回溯)

但是,實際情況中是可以通過子表示式優化的,比如構建^xxx,可以當匹配到不符合條件的時候提前匹配失敗,這樣就會少很多回溯

var str = `"cccccc`
var reg = /"[^"c]*"/g;

這個由於直接排除了c,因此*不會吃進它,直接就匹配失敗了,減少了很多回溯(當然,上述只是最簡單的例子,實際情況要更復雜)

寫在最後的話

正則匹配中,貪婪模式與非貪婪模式乍看之下一看便知,很容易理解,但是真正的深入理解需要掌握正則的原理才行,並且,真正理解它們後,就不僅僅只是寫出普通的正規表示式,而是高效能的正規表示式了,比如理解非貪婪模式中的回溯特性後更容易寫出高效能的表示式

本文也只是做一些淺顯的分析與引導,更多是起到拋磚引玉的作用,要深入理解還請去了解正則的原理

附錄

部落格

初次釋出2017.07.06於個人部落格

http://www.dailichun.com/2017/07/06/regularExpressionGreedyAndLazy.html

參考資料

相關文章