前言
貪婪模式和非貪婪模式是正則匹配中的重要特性
在理解貪婪和非貪婪的區別時,可以根據例項,一步一步的循序漸進
大綱
- 匹配規則簡介
- 貪婪模式與非貪婪模式快速理解
- 例項練習
- 回溯現象與匹配失敗
匹配規則簡介
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/
匹配,因此此次匹配成功,返回了ab
,index=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
開始匹配,依次成功匹配了R
,e
,g
,e
,x
,接著繼續匹配最後一個字元"
,匹配成功,這時候已經匹配到了字串的結尾,所以.*
匹配結束,將控制符交給正則式中最後的"
-
"
取得控制權後,由於已經是到了字串的結尾,因此匹配失敗,向前查詢可供回溯的狀態,控制權交給.*
,.*
讓出一個字元"
,再把控制權交給"
,此時剛好匹配成功 - 至此,整個正規表示式匹配完畢,匹配結果為
"Regex"
,匹配過程中回溯了1
次
非貪婪匹配表示式
".*?"
- 第一個
"
取得控制權,匹配正則中的"
,匹配成功,控制權交給.*?
-
.*?
取得控制權後,由於這是非貪婪模式下的識別符號,因此在可匹配可不匹配的情況下會優先不匹配,因此嘗試不匹配任何內容,將控制權交給"
,此時index
在1
處(R
字元處) -
"
取得控制權後,開始匹配1
處的R
,匹配失敗,向前查詢可供回溯的狀態,控制權交給.*?
,.*?
吃進一個字元,index
到了2
處,再把控制權交給"
-
"
取得控制權後,開始匹配2
處的e
,匹配失敗,重複上述的回溯過程,直到.*?
吃進了x
字元,再將控制權交給”
-
"
取得控制權後,開始匹配6
處的"
,匹配成功 - 至此,整個正規表示式匹配完畢,匹配結果為”Regex”,匹配過程中回溯了
5
次
優化去除回溯
上述的貪婪匹配中,出現了一次回溯現象,其實也可以通過優化表示式來防止回溯的,比如
"[^"]*"
這個表示式中構建了一個子表示式-[]
中的^"
,它的作用是排除"
匹配,這樣*
的貪婪匹配就不會主動吃進"
,這樣最後就直接是"
匹配"
,匹配成功,不會進行回溯
總結
上述的分析中可以看出,在匹配成功的情況下,貪婪模式進行了更少的回溯(可以自行通過更多的實驗進行驗證),因此在應用中,在對正則掌握不是很精通的情況下,可以優先考慮貪婪模式的匹配,這樣可以避免很多效能上的問題
匹配失敗的情況
上述的回溯分析都是基於匹配成功的情況,那如果是匹配失敗呢?
var str = `"Regex`
var reg = /"[^"]*"/g;
這個原字元中,沒有最後的"
,因此匹配是會失敗的,它的過程大致如下
-
"
匹配"
,接著[]
的^"
與*
匹配R
,e
,g
,e
,x
- 接著到了最後,
"
獲取控制權,由於到了最後,開始回溯 - 依次回溯的結果是
*
讓出x
,e
,g
,e
,R
,直到*
已經無法再讓出字元,第一輪匹配失敗 - 接著
index
開始往下挪,依次用"
匹配R
,e
,g
,e
,x
都失敗了,一直到最後也沒有再匹配到結果,因此此次正規表示式的匹配失敗,沒有匹配到結果(或者返回null
)
那非貪婪模式呢?
/"[^"]*?"/g
-
"
匹配"
,接著*
嘗試不匹配,"
匹配R
,失敗,然後回溯,*
吃進R
- 接下來類似於上一步,
*
依次回溯吃進e
,g
,e
,x
,一直到最後,*
再次回溯想吃進時,已經到了字串結尾了,無法繼續,因此第一輪匹配失敗 - 接著
index
開始往下挪,依次用"
匹配R
,e
,g
,e
,x
都失敗了,返回null
總結
通過匹配失敗的例子可以看出貪婪和非貪婪的模式區別。貪婪是先吃進,回溯再讓出
,非貪婪是先忽略,回溯再吃進
而且,在匹配失敗的情況下,貪婪模式也會進行不少的回溯(非貪婪當然一直都很多回溯)
但是,實際情況中是可以通過子表示式優化的,比如構建^xxx
,可以當匹配到不符合條件的時候提前匹配失敗,這樣就會少很多回溯
var str = `"cccccc`
var reg = /"[^"c]*"/g;
這個由於直接排除了c
,因此*
不會吃進它,直接就匹配失敗了,減少了很多回溯(當然,上述只是最簡單的例子,實際情況要更復雜)
寫在最後的話
正則匹配中,貪婪模式與非貪婪模式乍看之下一看便知,很容易理解,但是真正的深入理解需要掌握正則的原理才行,並且,真正理解它們後,就不僅僅只是寫出普通的正規表示式,而是高效能的正規表示式了,比如理解非貪婪模式中的回溯特性後更容易寫出高效能的表示式
本文也只是做一些淺顯的分析與引導,更多是起到拋磚引玉的作用,要深入理解還請去了解正則的原理
附錄
部落格
初次釋出2017.07.06
於個人部落格
http://www.dailichun.com/2017/07/06/regularExpressionGreedyAndLazy.html