https://www.cnblogs.com/macq/p/6597366.html
在使用正則表達式時,有時我們需要捕獲的內容前后必須是特定內容,但又不捕獲這些特定內容的時候,零寬斷言就起到作用了
正則表達式零寬斷言:
零寬斷言是正則表達式中的難點,所以重點從匹配原理方面進行分析。零寬斷言還有其他的名稱,例如"環視"或者"預搜索"等等,不過這些都不是我們關注的重點。
一.基本概念:
零寬斷言正如它的名字一樣,是一種零寬度的匹配,它匹配到的內容不會保存到匹配結果中去,最終匹配結果只是一個位置而已。
作用是給指定位置添加一個限定條件,用來規定此位置之前或者之后的字符必須滿足限定條件才能使正則中的字表達式匹配成功。
注意:這里所說的子表達式並非只有用小括號括起來的表達式,而是正則表達式中的任意匹配單元。
javascript只支持零寬先行斷言,而零寬先行斷言又可以分為正向零寬先行斷言,和負向零寬先行斷言。
代碼實例如下:
實例代碼一:
var str="abZW863"; var reg=/ab(?=[A-Z])/; console.log(str.match(reg));
在以上代碼中,正則表達式的語義是:匹配后面跟隨任意一個大寫字母的字符串"ab"。最終匹配結果是"ab",因為零寬斷言"(?=[A-Z])"並不匹配任何字符,只是用來規定當前位置的后面必須是一個大寫字母。
實例代碼二:
var str="abZW863"; var reg=/ab(?[A-Z])/; console.log(str.match(reg));
以上代碼中,正則表達式的語義是:匹配后面不跟隨任意一個大寫字母的字符串"ab"。正則表達式沒能匹配任何字符,因為在字符串中,ab的后面跟隨有大寫字母。
二.匹配原理:
上面代碼只是用概念的方式介紹了零寬斷言是如何匹配的。
下面就以匹配原理的方式分別介紹一下正向零寬斷言和負向零寬斷言是如何匹配的。
1.正向零寬斷言:
代碼實例如下:
var str="<div>antzone"; var reg=/^(?=<)<[^>]+>\w+/; console.log(str.match(reg));
匹配過程如下:
首先由正則表達式中的"^"獲取控制權,首先由位置0開始進行匹配,它匹配開始位置0,匹配成功,然后控制權轉交給"(?=<)",由於"^"是零寬的,所以"(?=<)"也是從位置0處開始匹配,它要求所在的位置右側必須是字符"<",位置0的右側恰好是字符"<",匹配成功,然后控制權轉交個"<",由於"(?=<)"也是零寬的,所以它也是從位置0處開始匹配,於是匹配成功,后面的匹配過程就不介紹了。
2.負向零寬斷言:
代碼實例如下:
var str="abZW863ab88"; var reg=/ab(?[A-Z])/g; console.log(str.match(reg));
匹配過程如下:
首先由正則表達式的字符"a"獲取控制權,從位置0處開始匹配,匹配字符"a"成功,然后控制權轉交給"b",從位置1處開始匹配,配字符"b"成功,然后控制權轉交給"(?[A-Z])",它從位置2處開始匹配,它要求所在位置的右邊不能夠是任意一個大寫字母,而位置的右邊是大寫字母"Z",匹配失敗,然后控制權又重新交給字符"a",並從位置1處開始嘗試,匹配失敗,然后控制權再次交給字符"a",從位置2處開始嘗試匹配,依然失敗,如此往復嘗試,直到從位置7處開始嘗試匹配成功,然后將控制權轉交給"b",然后從位置8處開始嘗試匹配,匹配成功,然后再將控制權轉交給"(?[A-Z])",它從位置9處開始嘗試匹配,它規定它所在的位置右邊不能夠是大寫字母,匹配成功,但是它並不會真正匹配ab后面的字符,所以最終匹配結果是"ab"。
下面補充有重復,可能斷言方法名字有所不同,理解意思最重要,可以以補充三中的斷言名為准。直接看補充三:沒有長篇大論的補充三
三、補充
零寬斷言是正則表達式中的一種方法,正則表達式在計算機科學中,是指一個用來描述或者匹配一系列符合某個句法規則的字符串的單個字符串。
定義解釋
零寬斷言是正則表達式中的一種方法
正則表達式在計算機科學中,是指一個用來描述或者匹配一系列符合某個句法規則的字符串的單個字符串。在很多文本編輯器或其他工具里,正則表達式通常被用來檢索和/或替換那些符合某個模式的文本內容。許多程序設計語言都支持利用正則表達式進行字符串操作。例如,在Perl中就內建了一個功能強大的正則表達式引擎。正則表達式這個概念最初是由Unix中的工具軟件(例如sed和grep)普及開的。正則表達式通常縮寫成“regex”,單數有regexp、regex,復數有regexps、regexes、regexen。
零寬斷言
用於查找在某些內容(但並不包括這些內容)之前或之后的東西,也就是說它們像\b,^,$那樣用於指定一個位置,這個位置應該滿足一定的條件(即斷言),因此它們也被稱為零寬斷言。最好還是拿例子來說明吧: 斷言用來聲明一個應該為真的事實。正則表達式中只有當斷言為真時才會繼續進行匹配。
(?=exp)也叫零寬度正預測先行斷言,它斷言自身出現的位置的后面能匹配表達式exp。比如\b(?=re)\w+\b
,匹配以re開頭的單詞的后面部分(除了re以外的部分),如查找reading a book.時,它會匹配ading。
var reg = new Regex(@"\w+(?=ing)"); var str = "muing"; Console.WriteLine(reg.Match(str).Value);//返回mu
(?<=exp)也叫零寬度正回顧后發斷言,它斷言自身出現的位置的前面能匹配表達式exp。比如\b\w+(?<=ing\b)
會匹配以ing結尾的單詞的前半部分(除了ing以外的部分),例如在查找I am reading.時,它匹配read。
假如你想要給一個很長的數字中每三位間加一個逗號(當然是從右邊加起了),你可以這樣查找需要在前面和里面添加逗號的部分:((?=\d)\d{3})+\b
,用它對1234567890進行查找時結果是234567890。
下面這個例子同時使用了這兩種斷言:(?<=\s)\d+(?=\s)
匹配以空白符間隔的數字(再次強調,不包括這些空白符)。
前面我們提到過怎么查找不是某個字符或不在某個字符類里的字符的方法(反義)。但是如果我們只是想要確保某個字符沒有出現,但並不想去匹配它時怎么辦?
例如,如果我們想查找這樣的單詞--它里面出現了字母q,但是q后面跟的不是字母u,我們可以嘗試這樣:\b\wq[^u]\w\b
匹配包含后面不是字母u的字母q的單詞。但是如果多做測試(或者你思維足夠敏銳,直接就觀察出來了),你會發現,如果q出現在單詞的結尾的話,像Iraq,Benq,這個表達式就會出錯。這是因為[^u]總要匹配一個字符,所以如果q是單詞的最后一個字符的話,后面的[^u]將會匹配q后面的單詞分隔符(可能是空格,或者是句號或其它的什么),后面的\w\b將會匹配下一個單詞,於是 \b\wq[^u]\w\b 就能匹配整個Iraq fighting。\b\wq[^u]\w\b不會匹配Iraq fighting,只能匹配Iraq fighting中的 aq f
。而\b\w+q[^u]\w+\b才能匹配整個Iraq fighting。(2017-10-20修正,感謝RussellJX指正)
負向零寬斷言能解決這樣的問題,因為它只匹配一個位置,並不消費任何字符。現在,我們可以這樣來解決這個問題:\b\wq(?!u)\w\b
。
零寬度負預測先行斷言(?!exp),斷言此位置的后面不能匹配表達式exp。
例如:\d{3}(?!\d)
匹配三位數字,而且這三位數字的后面不能是數字;\b((?!abc)\w)+\b
匹配不包含連續字符串abc的單詞。
同理,我們可以用(?<!exp),零寬度負回顧后發斷言來斷言此位置的前面不能匹配表達式exp:(?<[a-z])\d{7}
匹配前面不是小寫字母的七位數字。
一個更復雜的例子:(?<=<(\w+)>).(?=<\/\1>)
匹配不包含屬性的簡單HTML標簽內里的內容。(<?=(\w+)>)
指定了這樣的前綴:被尖括號括起來的單詞(比如可能是),然后是.*(任意的字符串),最后是一個后綴(?=<\/\1>)
。注意后綴里的\/
,它用到了前面提過的字符轉義;\1則是一個反向引用,引用的正是捕獲的第一組,前面的(\w+)匹配的內容,這樣如果前綴實際上是的話,后綴就是了。整個表達式匹配的是和之間的內容(再次提醒,不包括前綴和后綴本身)。
上面的看了有點傷腦筋啊。下面來點補充:
補充一:(復習正預測,正回顧,已經理解可以跳過)
斷言用來聲明一個應該為真的事實。正則表達式中只有當斷言為真時才會繼續進行匹配。
接下來的四個用於查找在某些內容(但並不包括這些內容)之前或之后的東西,也就是說它們像\b,^,$那樣用於指定一個位置,這個位置應該滿足一定的條件(即斷言),因此它們也被稱為零寬斷言。最好還是拿例子來說明吧:
(?=exp)也叫零寬度正預測先行斷言,它斷言自身出現的位置的后面能匹配表達式exp。
比如\b\w+(?=ing\b)
,匹配以ing結尾的單詞的前面部分(除了ing以外的部分),如查找I'm singing while you're dancing.時,它會匹配sing和danc。
(?<=exp)也叫零寬度正回顧后發斷言,它斷言自身出現的位置的前面能匹配表達式exp。
比如(?<=\bre)\w+\b
會匹配以re開頭的單詞的后半部分(除了re以外的部分),例如在查找reading a book時,它匹配ading。
假如你想要給一個很長的數字中每三位間加一個逗號(當然是從右邊加起了),你可以這樣查找需要在前面和里面添加逗號的部分:((?<=\d)\d{3})*\b
,用它對1234567890進行查找時結果是234567890。
這個正則同時使用了這兩種斷言:(?<=\s)\d+(?=\s)
匹配以空白符間隔的數字(再次強調,不包括這些空白符)。
零寬度正預測先行斷言是什么呢,看msdn上的官方解釋定義
(?= 子表達式)
零寬度正預測先行斷言僅當子表達式在此位置的右側匹配時才繼續匹配。
例如,\w+(?=\d) 與后跟數字的單詞匹配,而不與該數字匹配。
經典的例子:某單詞以ing結尾,要獲取ing前面的內容
var reg = new Regex(@"\w+(?=ing)"); var str = "muing"; Console.WriteLine(reg.Match(str).Value);//返回mu
以上是網上到處可見的例子,到這里或許你明白了,原來就是返回了exp表達式前面的內容。
再看下面的的代碼
var reg = new Regex(@"a(?=b)c"); var str = "abc"; Console.WriteLine(reg.IsMatch(str));//返回false
為什么會返回false?
其實msdn官方定義已經說了,只是它說得很官方而已。這里需要我們注意一個關鍵點:此位置。沒錯,是位置而不是字符。
那么結合官方定義和第一個例子來理解第二個例子:
因為a后面是b,則此時返回了匹配內容a(由第一個例子知道,只返回a不返回exp匹配的內容),此時a(?=b)c
中的a(?=b)
部分已經解決了,接下來要解決c的匹配問題了,此時匹配c要從字符串abc哪里開始呢,結合官方定義,就知道是從子表達的位置向右開始的,那么就是從b的位置開始,但b又不匹配a(?=b)c
剩余部分的c,所以abc就不匹配a(?=b)c
了。
那么如果要上面的進行匹配,正則應該如何寫呢?
答案是:a(?=b)bc
當然,有人會說直接abc就匹配上了,還要這么折騰嗎?當然不用這么折騰,只是為了說明零寬度正預測先行斷言到底是怎么一回事?關於其它的零寬斷言也是同一原理!
(?=exp):零寬度正預測先行斷言,它斷言自身出現的位置的后面能匹配表達式exp。
匹配后面為_path,結果為product
'product_path'.scan
/(product)(?=_path)/
(?<=exp):零寬度正回顧后發斷言,它斷言自身出現的位置的前面能匹配表達式exp
匹配前面為name:,結果為wangfei
'name:wangfei'.scan
/(?<=name:)(wangfei)/
(?!exp):零寬度負預測先行斷言,斷言此位置的后面不能匹配表達式exp。
匹配后面不是_path
'product_path'.scan
/(product)(?!_path)/
匹配后面不是_url
'product_path'.scan
/(product)(?!_url)/
(?<!exp):零寬度負回顧后發斷言來斷言此位置的前面不能匹配表達式exp
匹配前面不是name:
'name:angelica'.scan
/(?<!name:)(angelica)/
匹配前面不是nick_name:
'name:angelica'.scan
/(?<!nick_name:)(angelica)/
再次提醒,如果由於同樣表達式在不同地方的斷言方法名(斷言表達式叫法)不一致引起不適的話請以補充三中的為准。溜了溜了。
謝謝大家的閱讀。