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

正則表示式零寬斷言詳解

本文嚴重參考:正則表示式零寬斷言詳解

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

正則表示式零寬斷言:

零寬斷言還有其他的名稱,例如“環視”或者“預搜尋”等等。

1.基本概念

零寬斷言正如它的名字一樣,是一種零寬度的匹配,它匹配到的內容不會儲存到匹配結果中去,最終匹配結果只是一個位置而已。

作用是給指定位置新增一個限定條件,用來規定此位置之前或者之後的字元必須滿足限定條件才能使正則中的字表達式匹配成功。

注意:這裡所說的子表示式並非只有用小括號括起來的表示式,而是正則表示式中的任意匹配單元。

javascript只支援零寬先行斷言,而零寬先行斷言又可以分為正向零寬先行斷言,和負向零寬先行斷言。

正向零寬先行斷言——程式碼例項一如下:

var str = 'abZWab000abAAA863'
var reg = /ab(?=[A-Z])/
console.log(str.match(reg)) // ["ab", index: 0, input: "abZWab000abAAA863", groups: undefined] —— 返回第一個符合條件的ab

在以上程式碼中,正則表示式的語義是:匹配後面跟隨任意一個大寫字母的字串"ab"。最終匹配結果是"ab",因為零寬斷言 (?=[A-Z])

並不匹配任何字元,只是用來規定當前位置的後面必須是一個大寫字母。

var str = 'abZWab0000--863'
var reg = /ab(?![A-Z])/
console.log(str.match(reg)) // ["ab", index: 4, input: "abZWab0000--863", groups: undefined]

在以上程式碼中,正則表示式的語義是:匹配"ab"子串,並且“ab”串之後的必須不能是大寫字母。

2.匹配原理

上面程式碼只是用概念的方式介紹了零寬斷言是如何匹配的。

下面就以匹配原理的方式分別介紹一下正向零寬斷言和負向零寬斷言是如何匹配的。

1.正向零寬斷言:

var str="<div>antzone";
var reg=/^(?=<)<[^>]+>/; // (?=<) 並不匹配任何字元,表示首位必須是<,(?=<)之後的<才表示第一個
console.log(str.match(reg)); // ["<div>antzone", index: 0, input: "<div>antzone", groups: undefined]

匹配過程如下:

首先由正則表示式中的""獲取控制權,首先由位置0開始進行匹配,它匹配開始位置0,匹配成功,然後控制權轉交給"(?=<)",,由於""是零寬的,所以"(?=<)"也是從位置0處開始匹配,它要求所在的位置右側必須是字元"<",位置0的右側恰好是字元"<",匹配成功,然後控制權轉交個"<",由於"(?=<)"也是零寬的,所以它也是從位置0處開始匹配,於是匹配成功,後面的匹配過程就不介紹了。

2.負向零寬斷言:

var str = 'abZW863ab88'
var reg = /ab(?![A-Z])/ // (?![A-Z])匹配ab之後必須是非大寫字母
console.log(str.match(reg)) // ["ab", index: 7, input: "abZW863ab88", groups: undefined]

匹配過程如下:

首先由正則表示式的字元"a"獲取控制權,從位置0處開始匹配,匹配字元"a"成功,然後控制權轉交給"b",從位置1處開始匹配,配字元"b"成功,然後控制權轉交給"(?![A-Z])",它從位置2處開始匹配,它要求所在位置的右邊不能夠是任意一個大寫字母,而位置的右邊是大寫字母"Z",匹配失敗,然後控制權又重新交給字元"a",並從位置1處開始嘗試,匹配失敗,然後控制權再次交給字元"a",從位置2處開始嘗試匹配,依然失敗,如此往復嘗試,直到從位置7處開始嘗試匹配成功,然後將控制權轉交給"b",然後從位置8處開始嘗試匹配,匹配成功,然後再將控制權轉交給"(?![A-Z])",它從位置9處開始嘗試匹配,它規定它所在的位置右邊不能夠是大寫字母,匹配成功,但是它並不會真正匹配字元,所以最終匹配結果是"ab"。

3.補充

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

3.1.定義解釋

零寬斷言是正則表示式中的一種方法:

正則表示式在電腦科學中,是指一個用來描述或者匹配一系列符合某個句法規則的字串的單個字串。在很多文字編輯器或其他工具裡,正則表示式通常被用來檢索和/或替換那些符合某個模式的文字內容。許多程式設計語言都支援利用正則表示式進行字串操作。例如,在Perl中就內建了一個功能強大的正則表示式引擎。正則表示式這個概念最初是由Unix中的工具軟體(例如sed和grep)普及開的。正則表示式通常縮寫成“regex”,單數有regexp、regex,複數有regexps、regexes、regexen。

3.2.零寬斷言

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

?=exp 也叫零寬度正預測先行斷言,它斷言自身出現的位置的後面能匹配表示式exp。比如\b(?=re)\w+\b,匹配以re開頭的單詞的後面部分(除了re以外的部分),如查詢reading a book.時,它會匹配ading。

var str="i am reading a book";
var reg=/\b(?=re)\w+\b/;
console.log(str.match(reg));

?<=exp 也叫零寬度正回顧後發斷言,它斷言自身出現的位置的前面能匹配表示式exp。比如\b\w+(?<=ing\b)會匹配以ing結尾的單詞的前半部分(除了ing以外的部分),例如在查詢I am reading a book.時,它匹配read。

var str="i am reading a book";
var reg=/\b\w+(?<=ing\b)/; // 倆個\b分別是表示單詞的左右邊界
console.log(str.match(reg));

3.3.負向零寬斷言

前面我們提到過怎麼查詢不是某個字元或不在某個字元類裡的字元的方法(反義)。但是如果我們只是想要確保某個字元沒有出現,但並不想去匹配它時怎麼辦?

例如,如果我們想查詢這樣的單詞--它裡面出現了字母q,但是q後面跟的不是字母u,我們可以嘗試這樣:

var str="queue aqi";
var reg=/\b\w*q[^u]\w*\b/; // 倆個\b分別是表示單詞的左右邊界
console.log(str.match(reg));

\b\w*q[^u]\w*\b 匹配包含後面不是字母u的字母q的單詞。但是如果多做測試(或者你思維足夠敏銳,直接就觀察出來了),你會發現,如果q出現在單詞的結尾的話,像Iraq,Benq,這個表示式就會出錯。

var str="queue aiq qww ad";
var reg=/\b\w*q[^u]\w*\b/; // 倆個\b分別是表示單詞的左右邊界
console.log(str.match(reg)); // aiq qww

這是因為 [^\u] 總要匹配一個字元,所以如果q是單詞的最後一個字元的話,後面的 [^u] 將會匹配q後面的單詞分隔符(可能是空格,或者是句號或其它的什麼),後面的 \w*\b 將會匹配下一個單詞,於是 \b\w*q[^u]\w*\b 就能匹配整個Iraq fighting。負向零寬斷言能解決這樣的問題,因為它只匹配一個位置,並不消費任何字元。現在,我們可以這樣來解決這個問題: \b\w*q(?!u)\w*\b

var str="12ab23 999l";
var reg=/\d{3}(?!\d)/; // 倆個\b分別是表示單詞的左右邊界
console.log(str.match(reg));

var str="abc123 qwe123";
var reg=/\b((?!abc)\w)+\b/; // 倆個\b分別是表示單詞的左右邊界
console.log(str.match(reg));

同理,我們可以用 ?<!exp ,零寬度負回顧後發斷言來斷言此位置的前面不能匹配表示式exp:(?<![a-z])\d{7} 匹配前面不是小寫字母的七位數字。

var str="a1234567 A7654321";
var reg=/(?<![a-z])\d{7}/; // 倆個\b分別是表示單詞的左右邊界
console.log(str.match(reg));

// 升級
var str="a1234567 A7654321";
var reg=/\b(?<![a-z])[A-Z]\d{7}\b/; // 倆個\b分別是表示單詞的左右邊界
console.log(str.match(reg))

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

var str="<h1>哈哈哈</h1>";
var reg=/(?<=<(\w+)>).*(?=<\/\1>)/; // 倆個\b分別是表示單詞的左右邊界
console.log(str.match(reg));

上面的看了有點傷腦筋啊。下面來點補充一

斷言用來宣告一個應該為真的事實。正則表示式中只有當斷言為真時才會繼續進行匹配。

接下來的四個用於查詢在某些內容(但並不包括這些內容)之前或之後的東西,也就是說它們像\b,^,$那樣用於指定一個位置,這個位置應該滿足一定的條件(即斷言),因此它們也被稱為零寬斷言。最好還是拿例子來說明吧:

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

var str = 'I\'m singing while you\'re dancing.';
var reg = /\b(\w+(?=ing\b))/g;
console.log(str.match(reg)); // ["sing", "danc"]

(?<=exp) 也叫零寬度正回顧後發斷言,它斷言自身出現的位置的前面能匹配表示式exp。比如(?<=\bre)\w+\b會匹配以re開頭的單詞的後半部分(除了re以外的部分),例如在查詢reading a book時,它匹配ading。

var str = 'reading a book;
var reg = /(?<=\bre)\w+\b/g;
console.log(str.match(reg)); // ["ading"]

假如你想要給一個很長的數字中每三位間加一個逗號(當然是從右邊加起了),你可以這樣查詢需要在前面和裡面新增逗號的部分:((?<=\d)\d{3})*\b,用它對1234567890進行查詢時結果是234567890。

var str = '1234567890';
var reg = /((?<=\d)\d{3})*\b/g;
console.log(str.match(reg)); //  ["", "234567890", ""]

下面這個例子同時使用了這兩種斷言:(?<=\s)\d+(?=\s)匹配以空白符間隔的數字(再次強調,不包括這些空白符)。(例子在哪裡了????)

補充二:

(?= 子表示式)(零寬度正預測先行斷言。)僅當子表示式在此位置的右側匹配時才繼續匹配。例如,\w+(?=\d) 與後跟數字的單詞匹配,而不與該數字匹配。

經典的例子:某單詞以ing結尾,要獲取ing前面的內容:

var str = 'reading';
var reg = /\w+(?=ing)/g;
console.log(str.match(reg)); // ["read"]

補充三:

(?=exp) :零寬度正預測先行斷言,它斷言自身出現的位置的後面能匹配表示式exp。

(?<=exp) :零寬度正回顧後發斷言,它斷言自身出現的位置的前面能匹配表示式exp。

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

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

其他?相關

// 1.貪婪匹配 和 惰性匹配
// 檢視 標題4

// 2. ?: —— 忽略()分組
var pattern = /(ab)\w+(ba)/
console.log('abcba123'.replace(pattern, '$1')) // abcba 被 $1(ab) 替換
/* 
 * 結果"ab123";匹配到的字元被第一個分組(ab)替換
 */
var pattern2 = /(?:ab)\w+(ba)/
console.log('abcba123'.replace(pattern2, '$1'))
/*
 * 結果"ba123";第一次分組內加入了?:,產生的是一個沒有編號的分組,所以$1匹配的字元是第二個分組,
 * 也就是第一個編號分組(ba)相匹配的文字內容
 */

標準格式化金額

console.log('$ 12345678'.replace(/\B(?=(\d{3})+(?!\d))/g, ','))
console.log('$ 12,345,678'.replace(/\$\s?|(,*)/g, ''))