1. 程式人生 > >正則表示式零寬斷言詳解

正則表示式零寬斷言詳解

在使用正則表示式時,有時我們需要捕獲的內容前後必須是特定內容,但又不捕獲這些特定內容的時候,零寬斷言就起到作用了。

一.基本概念:

零寬斷言正如它的名字一樣,是一種零寬度的匹配,它匹配到的內容不會儲存到匹配結果中去,最終匹配結果只是一個位置而已。
作用是給指定位置新增一個限定條件,用來規定此位置之前或者之後的字元必須滿足限定條件才能使正則中的字表達式匹配成功。
注意:這裡所說的子表示式並非只有用小括號括起來的表示式,而是正則表示式中的任意匹配單元。
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",匹配失敗,
  1. 然後控制權又重新交給字元"a",並從位置1處開始嘗試,匹配失敗,
  2. 然後控制權再次交給字元"a",從位置2處開始嘗試匹配,依然失敗,
  3. 如此往復嘗試,直到從位置7處開始嘗試匹配成功,然後將控制權轉交給"b",然後從位置8處開始嘗試匹配,匹配成功,然後再將控制權轉交給"(?![A-Z])",它從位置9處開始嘗試匹配,它規定它所在的位置右邊不能夠是大寫字母,匹配成功,但是它並不會真正匹配字元,所以最終匹配結果是"ab"。

說明

零寬斷言是正則表示式中的一種方法,正則表示式在電腦科學中,是指一個用來描述或者匹配一系列符合某個句法規則的字串的單個字串。

定義解釋

零寬斷言是正則表示式中的一種方法正則表示式在電腦科學中,是指一個用來描述或者匹配一系列符合某個句法規則的字串的單個字串。在很多文字編輯器或其他工具裡,正則表示式通常被用來檢索和/或替換那些符合某個模式的文字內容。許多程式設計語言都支援利用正則表示式進行字串操作

零寬斷言

用於查詢在某些內容(但並不包括這些內容)之前或之後的東西,也就是說它們像\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\w*q[^u]\w*\b匹配包含後面不是字母u的字母q的單詞。但是如果多做測試(或者你思維足夠敏銳,直接就觀察出來了),你會發現,如果q出現在單詞的結尾的話,像Iraq,Benq,這個表示式就會出錯。這是因為[u]總要匹配一個字元,所以如果q是單詞的最後一個字元的話,後面的[u]將會匹配q後面的單詞分隔符(可能是空格,或者是句號或其它的什麼),後面的\w*\b將會匹配下一個單詞,於是\b\w*q[^u]\w*\b就能匹配整個Iraq fighting。負向零寬斷言能解決這樣的問題,因為它只匹配一個位置,並不消費任何字元。現在,我們可以這樣來解決這個問題:\b\w*q(?!u)\w*\b

零寬度負預測先行斷言(?!exp),斷言此位置的後面不能匹配表示式exp。例如:\d{3}(?!\d)匹配三位數字,而且這三位數字的後面不能是數字;\b((?!abc)\w)+\b匹配不包含連續字串abc的單詞。
同理,我們可以用(?<!exp),零寬度負回顧後發斷言來斷言此位置的前面不能匹配表示式exp:(?<![a-z])\d{7}匹配前面不是小寫字母的七位數字。

一個更復雜的例子:

(?<=<(\w+)>).*(?=<\/\1>)匹配不包含屬性的簡單HTML標籤內裡的內容。(<?=(\w+)>)指定了這樣的字首:被尖括號括起來的單詞(比如可能是<b>),然後是.*(任意的字串),最後是一個字尾(?=<\/\1>)。注意字尾裡的/,它用到了前面提過的字元轉義;\1則是一個反向引用,引用的正是捕獲的第一組,前面的(\w+)匹配的內容,這樣如果字首實際上是<b>的話,字尾就是</b>了。整個表示式匹配的是<b>和</b>之間的內容(再次提醒,不包括字首和字尾本身)。

上面的看了有點傷腦筋啊。

補充一:

斷言用來宣告一個應該為真的事實。正則表示式中只有當斷言為真時才會繼續進行匹配。
接下來的四個用於查詢在某些內容(但並不包括這些內容)之前或之後的東西,也就是說它們像\b,^,$那樣用於指定一個位置,這個位置應該滿足一定的條件(即斷言),因此它們也被稱為零寬斷言。最好還是拿例子來說明吧:

1、(?=exp)也叫零寬度正預測先行斷言,它斷言自身出現的位置的後面能匹配表示式exp。比如\b\w+(?=ing\b),匹配以ing結尾的單詞的前面部分(除了ing以外的部分),如查詢I'm singing while you're dancing.時,它會匹配sing和danc。
2、(?<=exp)也叫零寬度正回顧後發斷言,它斷言自身出現的位置的前面能匹配表示式exp。比如(?<=\bre)\w+\b會匹配以re開頭的單詞的後半部分(除了re以外的部分),例如在查詢reading a book時,它匹配ading。

假如你想要給一個很長的數字中每三位間加一個逗號(當然是從右邊加起了),你可以這樣查詢需要在前面和裡面新增逗號的部分:((?<=\d)\d{3})*\b,用它對1234567890進行查詢時結果是234567890。
下面這個例子同時使用了這兩種斷言:(?<=\s)\d+(?=\s)匹配以空白符間隔的數字(再次強調,不包括這些空白符)。

補充二:

最近為了對html檔案進行原始碼處理,需要進行正則查詢並替換。於是藉著這個機會把正則系統地學一下,雖然以前也用過正則,但每次都是臨時學一下混過關的。在學習的過程中還是遇到不少問題的,特別是零寬斷言(這裡還要吐槽下,網上到處都是都複製貼上的內容,遇到個問題查看了不少重複的東西,汗!!!),所以在這裡把自己的理解寫下來,方便以後查閱!

  零寬度正預測先行斷言是什麼呢,看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)/ #wangfei

(?!exp):零寬度負預測先行斷言,斷言此位置的後面不能匹配表示式exp。

匹配後面不是_path

'product_path'.scan /(product)(?!_path)/ #nil

匹配後面不是_url

'product_path'.scan /(product)(?!_url)/ #product

(?<!exp):零寬度負回顧後發斷言來斷言此位置的前面不能匹配表示式exp

匹配前面不是name:

'name:angelica'.scan /(?<!name:)(angelica)/ #nil

匹配前面不是nick_name:

'name:angelica'.scan /(?<!nick_name:)(angelica)/#angelica

'123456789'.match(/(?=(\d{3})+$)/)

(?=(\d{3})+$) 單位匹配,每次都需要匹配到結尾,才算成功

所謂g,就是從0的位置,開始遍歷整個字串

@123 456 789 ok 3個3位結尾
[email protected] 567 89
[email protected] 679 9
[email protected] 789 ok 2個3位結尾

[email protected] 89

[email protected] 9

[email protected] ok 1個3位結尾

/\B(?=(\d{3})+$)/g

\B的意思是,匹配前,檢測當前位置左右是否同類型字元,如果不是,則停止匹配,繼續下一次迴圈,

對於 123456789 而言:

第一次,0位置匹配時,左邊是空格,右邊是數字,因此是單詞邊界,失敗, 也就是從1開始匹配。

‘123456789’.replace(/\B(?=(\d{3})+$)/g,','); // 123,456,789