想寫出效率更高的正則表達式?試試固化分組和佔有優先匹配吧

20200719 (1).png

上次咱們講解了正則表達式量詞匹配方式的貪婪匹配和懶惰匹配以後,一些同窗給個人公衆號留言說但願可以快點把量詞匹配方式的下篇也寫了。那麼,此次咱們就來學習一下量詞的另一種匹配方式,那就是佔有優先的匹配方式。固然咱們這篇文章還講解了跟佔有優先匹配功能同樣的固化分組,以及使用確定的順序環視來模擬佔有優先的匹配方式以及固化分組。準備好了嗎,快來給本身的技能樹上再添加一些技能吧。javascript

咱們若是能夠掌握這種匹配方式的原理的話,那麼咱們就有能力寫出效率更高的正則表達式。在進行深刻的學習以前,但願你至少對正則表達式的貪婪匹配有所瞭解,若是還不怎麼了解的話,能夠花費幾分鐘的時間看一下我上一篇關於貪婪匹配和懶惰匹配的文章。php

佔有優先的匹配方式:(表達式)*+

首先,佔有優先的匹配方式跟貪婪匹配的方式很像。它們之間的區別就是佔有優先的匹配方式,不會歸還已經匹配的字符,這點很重要。這也是爲何佔有優先的匹配方式效率比較高的緣由。由於它做用的表達式在匹配結束以後,不會保留備用的狀態,也就是不會進行回溯。可是,貪婪匹配會保留備用的狀態,若是以前的匹配沒有成功的話,它會回退到最近一次的保留狀態,也就是進行回溯,以便正則表達式總體可以匹配成功
那麼佔有優先的表示方式是怎樣的?佔有優先匹配就是在原來的量詞的後面添加一個+,像下面展現的這樣。html

.?+
.*+
.++
.{3, 6}+
.{3,}+

由於正則表達式有不少流派,有一些流派是不支持佔有優先這種匹配方式的,好比JavaScript就不支持這種匹配的方式(前端同窗表示不是很開心😂),可是咱們可使用確定的順序環視來模擬佔有優先的匹配。PHP就支持這種匹配方式。因此接下來咱們一些正則的演示,就會選擇使用PHP流派進行演示。前端

咱們來寫一個簡單的例子來加深一下你們對於佔有優先匹配方式的瞭解。有這麼一個需求,你須要寫一個正則表達式來匹配以數字9結尾的數字。你會怎麼寫呢?固然,對於已經有正則表達式基礎的同窗來講,這應該是很容易的事情。咱們會寫出這麼一個正則表達式\d*9,這樣就知足了上面所說的需求了。java

d*9

讓咱們把上面的貪婪匹配方式修改成佔有優先的匹配方式,你以爲咱們還可以匹配相同的結果嗎?來讓咱們看一下修改後的匹配結果。node

d*+9

答案是不能,你也許會好奇,爲何就不能夠了。讓我來好好給你們解釋一下爲何不可以匹配了。git

咱們知道正則表達式是從左向右匹配的,對於\d*+這個總體,咱們在進行匹配的時候能夠先把\d*+看做是\d*進行匹配。對於\d*+這部分表達式來講它在開始匹配的時候會匹配儘量多的數字,對於咱們給出的測試用例,\d*+都是能夠匹配的,因此\d*+直接匹配到了每一行數字的結尾處。而後由於\d*+是一個總體,表示佔有優先的匹配。因此\d*+匹配完成以後,這個總體便再也不歸還已經匹配的字符了。可是咱們正則表達式的後面還須要匹配一個字符9,可是前面已經匹配到字符串的結尾了,再沒有字符給9去匹配,因此上面的測試用例都匹配失敗了。github

在開始匹配的過程當中咱們能夠把佔有優先當作貪婪匹配來進行匹配,可是一旦匹配完成就跟貪婪匹配不同了,由於它再也不歸還匹配到的字符。因此對於佔有優先的匹配方式,咱們只須要牢記佔有優先匹配方式匹配到的字符再也不歸還就能夠了。正則表達式

固化分組:(?>表達式)

咱們瞭解了佔有優先的匹配以後,再來看看跟佔有優先匹配做用同樣的固化分組。那什麼是固化分組呢?固化分組的意思是這樣的,當固化分組裏面的表達式匹配完成以後,再也不歸還已經匹配到的字符。固話分組的表示方式是(?>表達式),其中裏面的表達式就是咱們要進行匹配的表達式。好比(?>\d*)裏面的表達式就是\d*,表示的意思就是當\d*這部分匹配完成以後,再也不歸還\d*已經匹配到的字符。express

因此,對於\d*+來講,咱們若是使用固化分組的話能夠表示爲(?>\d*)。其實,佔有優先固化分組的一種簡便的表示方式,若是固化分組裏面的表達式是一個很簡單的表達式的話。那麼使用佔有優先量詞,比使用固化分組更加的直觀。

咱們將上面使用佔有優先的表達式替換爲使用固化分組的方式表示,下面兩張圖片展現了使用固化分組後的匹配結果。

(?>d*)

(?>d*)9

還有一些須要注意的是,支持佔有優先量詞的正則流派也支持固化分組,可是對於支持固化分組的正則流派來講,不必定支持佔有優先量詞。因此在使用佔有優先量詞的時候,要確保你使用的那個流派是支持的。

使用確定順序環視模擬固化分組:(?=(表達式))1

對於不支持固化分組的流派來講,若是這些流派支持確定的順序環視捕獲的括號的話,咱們可使用確定的順序環視來模擬固化分組。若是對於正則表達式的環視還不熟悉的話,能夠花幾分鐘的時間看一下我以前寫的這篇文章距離弄懂正則的環視,你只差這一篇文章,保證你能夠快速的理解正則的環視。

看到這裏,你可能要問,爲何確定的順序環視能夠模擬固化分組呢?咱們要知道固化分組的特性就是匹配完成以後,丟棄了固化分組內表達式的備用狀態,而後不會進行回溯。又由於環視一旦匹配成功以後也是不會進行回溯的,因此咱們能夠利用確定的順序環視來模擬固化分組。

咱們可使用(?=(表達式))\1這個表達式來模擬固化分組(?>表達式)。我來解釋一下上面這個模擬的正則表達式,首先是一個確定的順序環視,須要在當前位置的後面找到知足表達式的匹配,若是找到的話,接下來\1會匹配環視中已經匹配到的那部分字符。由於在順序環視中的正則表達式不會受到順序環視後面表達式的影響,因此順序環視內部的表達式在匹配完成以後不會進行回溯。而後後面的\1再次匹配環視裏面表達式匹配到的內容,這樣就模擬了一個固化分組。

咱們再將上面的表達式替換爲使用模擬固化分組的方式表示,下面兩張圖片展現了使用模擬固化分組後的匹配結果。

(?=(d*))1

(?=(d*))19

模擬的固化分組在效率上要比真正的固化分組慢一些,由於\1的匹配也是須要花費時間的。不過對於貪婪匹配所形成的的回溯來講,這點匹配的時間通常仍是很短的。

貪婪匹配和佔有優先效率的比較

咱們上面說過,由於佔有優先不會回溯,因此在一些狀況下,使用佔有優先的匹配要比使用匹配優先的匹配效率高不少。那麼下面咱們就使用代碼來驗證一下貪婪匹配和佔有優先匹配的效率是怎樣的。

代碼以下所示:

// 匹配優先(貪婪匹配)匹配一行中的數字,後面緊跟着字符b
const greedy_reg = /\d*b/;
// 佔有優先(使用確定順序環視模擬)
const possessive_reg = /(?=(\d*))\1b/;
// 測試的字符串 000...(共有1000個0)...000a
const str = `${new Array(1000).fill(0).join('')}a`;

console.time('匹配優先');
greedy_reg.test(str);
console.timeEnd('匹配優先');

console.time('模擬的佔有優先');
possessive_reg.test(str);
console.timeEnd('模擬的佔有優先');

在上面的測試代碼中,咱們生成了一個長度爲1001的字符串,最後一位是一個小寫字母a。由於貪婪匹配在匹配到最後一個數字後,發現最後一個字符是a,不可以知足b的匹配,因此開始進行回溯。雖然咱們知道就算進行了回溯也不會匹配成功了,可是運行的程序是不知道的,因此程序會不斷的回溯,一直回溯到\d*什麼也不匹配,而後再次檢查b,發現仍是不能夠匹配。最終報告匹配失敗。中間進行了大量的回溯,因此匹配的效率下降了。

對於佔有優先的匹配,在第一次\d*匹配成功後,發現後面的a不可以知足b的匹配,因此當即報告失敗,匹配效率比較高。可是由於JavaScript不支持佔有優先固化分組,因此咱們使用了確定的順序環視來替代,可是由於\1須要進行接下來的匹配,也會消耗一些時間。因此這個測試的結果不可以嚴格意義上代表佔有優先貪婪匹配在這種狀況下的效率高,可是若是模擬的佔有優先消耗的時間比較短,那就能夠說明佔有優先確實比貪婪匹配在這種狀況下的效率高。

我首先在node.js環境中運行,我本地的node.js版本爲v12.16.1,系統爲macOS。程序運行的結果以下:

匹配優先: 1.080ms
模擬的佔有優先: 0.702ms

這個結果只是其中一次的運行結果,運行不少次後發現匹配優先的耗時要比咱們模擬的佔有優先多一些,但也有幾回的運行時間是小於模擬的佔有優先的。我把相同的代碼也放在了Chrome瀏覽器中運行了屢次,發現匹配優先的耗時有時比模擬佔有優先高,有時比模擬佔有優先低,不是很好作判斷。

JavaScript中不可以很好地反應這兩種匹配方式的效率高低,因此咱們須要在PHP中再次進行試驗。由於PHP是原生的支持佔有優先匹配的,因此比較的結果是有說服力的。咱們使用PHP的代碼實現上面相同的邏輯,代碼以下:

// 貪婪匹配
$greedy_reg     = '/\d*b/';
// 佔有優先
$possessive_reg = '/\d*+b/';

// 待測試字符串
$str = implode(array_fill(0, 1000, 0)) . 'a';

// 計算貪婪匹配花費的時間
$t1 = microtime(true);
preg_match($greedy_reg, $str);
$t2 = microtime(true);
echo '貪婪匹配運行的時間:' . ($t2 - $t1) * 1e3 . 'ms';

echo PHP_EOL;

// 計算佔有優先匹配花費的時間
$t3 = microtime(true);
preg_match($possessive_reg, $str);
$t4 = microtime(true);
echo '佔有優先匹配運行的時間:' . ($t4 - $t3) * 1e3 . 'ms';

能夠看到運行的結果以下:

貪婪匹配運行的時間:0.025033950805664ms
佔有優先匹配運行的時間:0.0071525573730469ms

若是你將這段代碼運行屢次的話,能夠看到佔有優先匹配所花費的時間的確是比貪婪匹配要少一些的,因此上面的代碼能夠說明,在這種狀況下佔有優先匹配的效率是比貪婪匹配的效率高的。

關於正則表達式的佔有優先匹配和固化分組的講解到這裏就結束啦,若是你們有什麼疑問和建議均可以在這裏提出來。歡迎你們關注個人公衆號關山不難越,咱們一塊兒學習更多有用的正則知識,一塊兒進步。

參考資料:

相關文章
相關標籤/搜索