懒惰和贪婪-正则回溯
需要一定的正則基礎,并且是基于JS寫的文章。
正則表達式是從左往右匹配的。在使用正則表達式的時候我們知道/.*/可以匹配一個字字符串中所有的字符,/.*?/卻一個字符都匹配不到。/(.*)\d/中的.\*可以匹配除了最后一位數字的所有字符,但是之前說的/.*/不是匹配了所有字符嗎為什么后面的\d還可以匹配到一個數字字符?
首先我們要知道對于貪婪模式在進行匹配的時候會首先嘗試匹配。意思就是/.*/匹配”abcd”的時候可以選擇匹配a和不匹配a都是可以的,但是因為是貪婪模式所以選擇了匹配a,b和c和d是同樣的道理,到最后匹配完了abcd正則表達式匹配完成并且匹配成功。
對于懶惰模式在進行匹配的時候會首先嘗試跳過。就是/.*?/匹配”abcd”字符串的時候首先嘗試跳過a的匹配,再跳過b的匹配,直到最后正則表達式匹配完成,射門都沒匹配到。
通過上面的描述可以看出貪婪和懶惰只是在每一個字符是否匹配上做的選擇不同,相同的是在匹配和跳過的選擇中正則表達式都會記住我在這里做了選擇,這里還要其他選擇。記住這些選擇的作用就是當前選擇如果走不通了,那么還可以回退到這里選擇記錄下來的另一條路,這就是回溯。 開始回溯的會選擇離當前位置最近的一次選擇,就是一個后入先出的棧的模式。
例1:/(.*)\d/匹配”abcd1”
第一步:因為.*是貪婪模式,所以會匹配字符a(并記住也可以不匹配a),往后匹配字符b(也可不匹配)一直往后匹配了字符c,字符d,字符1。
第二步:\d匹配的時候匹配不到字符,整個正則需要回溯,到選擇是否匹配字符1的時候,之前選擇了匹配現在要選擇不匹配,讓出了字符1。
第三步:\d匹配字符1,完成整個正則的匹配。
var reg = /(.*)\d/ var str = "abcd1" var res = str.match(reg) // ?["abcd1", "abcd"] 可以看到分組(.*)匹配到了abcd并不包括1例2:/(.*)\d/匹配”ab1cd”
第一步:因為.*是貪婪模式,所以會匹配字符a(并記住也可以不匹配a),往后匹配字符b(也可不匹配)一直往后匹配了字符c,字符d,字符1。
第二步:\d匹配的時候匹配不到字符,整個正則需要回溯,到選擇是否匹配字符d的時候,之前選擇了匹配現在要選擇不匹配d,讓出了字符d。
第三步:\d匹配字符d,匹配失敗。繼續回溯.*讓出字符c。
第四步:\d繼續匹配字符c,匹配失敗。還要繼續回溯.*讓出字符1.
第五步:\d匹配字符1,完成整個正則的匹配。
var reg = /(.*)\d/ var str = "ab1cd" var res = str.match(reg) // ?["ab1", "ab"] 可以看到分組(.*)匹配到了ab并不包括1例3:/(.*?)\d/匹配”abcd1”
第一步:因為.*?是懶惰模式,所以不會匹配字符a(并記住可以匹配a)。
第二步:\d匹配字符a,匹配失敗。回溯.*?重新選擇匹配字符a,并繼續放棄了字符b(記住可以匹配字符b)。
第二步:\d匹配字符b,匹配失敗。回溯.*?重新選擇匹配字符b,并繼續放棄了字符c(記住可以匹配字符c)。
第三步:\d匹配字符c,匹配繼續失敗。繼續回溯.*?重新選擇匹配了字符c,繼續并放棄了匹配字符d(記住可以匹配字符d)。
第四步:\d繼續匹配字符d,匹配失敗。還要繼續回溯.*?重新選擇匹配字符d,還是放棄了字符1(記住可以匹配字符1)。
第五步:\d匹配字符1,完成整個正則的匹配。
var reg = /(.*?)\d/ var str = "abcd1" var res = str.match(reg) // ?["abcd1", "abcd"] 可以看到分組(.*)匹配到了abcd并不包括1小結
對比例1和例3可以發現懶惰和貪婪模式匹配的結果是相同的,但是這并不意味著這兩種匹配模式匹配結果是無差別的。對于兩種模式匹配的字符串如果只有一個真確的匹配結果那么確是匹配得到的結果是一樣的,但是一步一步檢查過來就會知道雖然匹配結果一樣但是經過的步驟是不同的。
如果匹配的結果不止一種可能,那么這兩種模式匹配得到的結果就不一樣了。例如,將字符串“abcd1”換成”abcd11”,那么這兩種模式匹配到的結果就一樣了。
var reg = /(.*?)\d/ var reg2 = /(.*)\d/ var str = "abcd11"str.match(reg) // ["abcd1", "abcd"] str.match(reg2) // ["abcd11", "abcd1"]例4:/\w?(\w?)1/匹配“a1”
第一步:\w?匹配字符a(記住可以不匹配字符a)
第二步:(\w?)匹配字符1(記住可不匹配字符1)
第三步:1匹配不到任意字符,返回之前做選擇的地方(\w?)從新選擇放棄了字符1,什么都沒有匹配
第四步:1匹配了字符1,完成整個正則表達式的匹配
var reg = /\w?(\w?)1/ var str = "a1"str.match(reg) // ["a1", ""] 數組的第二個值也就是分組1(\w?)什么都沒有匹配到通過上面的步驟可以看出當需要回溯的時候會選擇當前位置最近的一次選擇,在重新開始,而不是從整個表達式的第一次選擇開始重新選擇。這是一個后進先出的模式就像棧一樣。
斷言/環視中的回溯
首先說結論,斷言中的備用狀態在斷言匹配結束后會被丟棄,整個斷言只能當做一個整體存在,回溯的時候不會進入斷言中的備用狀態。
例5:/(?=(.*))\11/匹配”11111”
這個正則什么都匹配不到。
第一步:(?=(.*))\1匹配了11111
第二步:1并不能匹配到任何字符串,并且準備回溯的時候發現前面并沒有可回溯的狀態,匹配失敗
第三步:以為這就結束了嗎?還沒有會從第二個1再開始匹配,雖然得到的結果是一樣的,直到最后一個1完成匹配,匹配失敗,什么都沒有匹配到
var reg = /(?=(.*))\11/ var str = "11111"str.match(reg) // null注:這是固化分組的一種模擬,固化分組指的是放棄分組中的備用狀態,語法是(?…)。
分支中的回溯
首先多選分支是從左到右匹配的,并不會因為某個分支的或長或短就優先匹配,這樣就會造成分支順序不正確就導致某些分支永遠不會被匹配到。
var reg = /abc|ab|abcd/ var str = "abcd"str.match(reg) // ["abc"]匹配結果是abc并不是ab,如果將分支調換位置那么得到的結果又將不一樣。
var reg = /abcd|abc|ab/ var str = "abcd"str.match(reg) // ["abcd"] var reg = /ab|abcd|abc/ var str = "abcd"str.match(reg) // ["ab"]從上面的例子中可以看出多選分支的匹配是從左到右來選擇分支來匹配的,并且在分支之間選擇的時候會記住備用的選擇,以備無法匹配的時候回溯到這里選擇另一個分支。
例6:/((aa?)|(aa))a/匹配”aa”
第一步:選擇分支(aa?)并且記住可以選擇分支(aa),開始匹配字符aa
第二步:a匹配字符a
第三步:a? 匹配第二個a(記住可以不匹配這個a)
第四步:正則中最后一個a匹配的時候發現,無字符可匹配
第五步:回溯到第三步選擇不匹配a字符
第六步:正則中最后一個a完成了匹配字符串中的a,整個正則匹配完成
var reg = /((aa?)|(aa))a/ var str = "aa"str.match(reg) // ["aa", "a", "a", undefined]例7:/((aa)|(aa?))a/匹配”aa”
第一步:選擇分支(aa)并且記住可以選擇分支(aa?),開始匹配字符aa
第二步:a匹配字符a
第三步:aa 匹配第二個a
第四步:正則中最后一個a匹配的時候發現,無字符可匹配
第五步:回溯到第一步選擇分支(aa?)
第六步:這時就會重復例6的步驟,直到整個正則匹配成功
var reg = /((aa)|(aa?))a/ var str = "aa"str.match(reg) // ["aa", "a", undefined, "a"]注:雙引號”表示的是字符串,并不是字符串的一部分
參考
精通正則表達式(第三版)
總結
以上是生活随笔為你收集整理的懒惰和贪婪-正则回溯的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: web安全之CSRF
- 下一篇: Generator执行步骤浅析