Javascript 前瞻斷言與后瞻斷言

2023-02-17 11:02 更新

有時(shí)我們只需要為一個(gè)模式找到那些在另一個(gè)模式之后或之前的匹配項(xiàng)。

有一種特殊的語法,稱為“前瞻斷言(lookahead)”和“后瞻斷言(lookbehind)”。

首先,讓我們從字符串中查找價(jià)格,例如 1 turkey costs 30€。即:一個(gè)數(shù)字,后跟符號(hào)。

前瞻斷言

語法為:x(?=y),它表示“僅在后面是 Y 時(shí)匹配 X”。這里的 X 和 Y 可以是任何模式。

那么對(duì)于一個(gè)后面跟著  的整數(shù),正則表達(dá)式應(yīng)該為:\d+(?=€)。

let str = "1 turkey costs 30€";

alert( str.match(/\d+(?=€)/) ); // 30,數(shù)字 1 被忽略了,因?yàn)樗竺鏇]有 €

請(qǐng)注意:前瞻斷言只是一個(gè)測(cè)試,括號(hào) (?=...) 中的內(nèi)容不包含在匹配結(jié)果 30 中。

當(dāng)我們查找 X(?=Y) 時(shí),正則表達(dá)式引擎會(huì)找到 X,然后檢查其后是否有 Y。如果沒有,則跳過潛在匹配,并繼續(xù)搜索。

更復(fù)雜的測(cè)試也是可能的,例如 X(?=Y)(?=Z) 表示:

  1. 尋找 ?X?。
  2. 檢查 ?Y? 是否緊跟在 ?X? 之后(如果不是則跳過)。
  3. 檢查 ?Z? 是否也在 ?X? 之后(如果不是則跳過)。
  4. 如果兩個(gè)測(cè)試都通過了,那么 ?X? 是匹配的,否則繼續(xù)搜索。

換句話說,這樣的模式意味著我們同時(shí)在尋找 X 后跟 Y 和 Z

這只有在模式 Y 和 Z 不是互斥的情況下才可行。

例如,\d+(?=\s)(?=.*30) 查找后跟著空格 (?=\s) 的 \d+,并且有 30 在它之后的某個(gè)地方 (?=.*30)

let str = "1 turkey costs 30€";

alert( str.match(/\d+(?=\s)(?=.*30)/) ); // 1

在我們給出的字符串中,與數(shù)字 1 完全匹配。

否定的前瞻斷言

假設(shè)我們想要一個(gè)數(shù)量,而不是來自同一字符串的價(jià)格。那是一個(gè)數(shù)字 \d+,后面不是 。

為此,我們可以使用否定的前瞻斷言。

語法是:X(?!Y),意思是“搜索 X,但前提是后面沒有 Y”。

let str = "2 turkeys cost 60€";

alert( str.match(/\d+\b(?!€)/g) ); // 2(價(jià)格不匹配)

后瞻斷言

后瞻斷言的瀏覽器兼容情況

請(qǐng)注意:非 V8 引擎的瀏覽器不支持后瞻斷言,例如 Safari、Internet Explorer。

前瞻斷言允許添加一個(gè)“后面要跟著什么”的條件判斷。

后瞻斷言也類似,只不過它是在相反的方向上進(jìn)行條件判斷。也就是說,它只允許匹配前面有特定字符串的模式。

語法為如下:

  • 肯定的后瞻斷言:(?<=Y)X,匹配 X,僅在前面是 Y 的情況下。
  • 否定的后瞻斷言:(?<!Y)X,匹配 X,僅在前面不是 Y 的情況下。

例如,讓我們把價(jià)格換成美元。美元符號(hào)通常在數(shù)字前面,所以要查找 $30 我們將使用 (?<=\$)\d+ —— 一個(gè)前面帶 $ 的數(shù)值:

let str = "1 turkey costs $30";

// 美元符號(hào)被轉(zhuǎn)義 \$
alert( str.match(/(?<=\$)\d+/) ); // 30(跳過了僅僅是數(shù)字的值)

如果我們需要找到量詞 —— 一個(gè)前面不帶 $ 的數(shù)字,我們可以使用否定的后瞻斷言:(?<!\$)\d+

let str = "2 turkeys cost $60";

alert( str.match(/(?<!\$)\b\d+/g) ); // 2(價(jià)格不匹配)

捕獲組

一般來說,前瞻斷言和后瞻斷言括號(hào)中的內(nèi)容不會(huì)成為結(jié)果的一部分。

例如,在模式 \d+(?!€) 中, 符號(hào)就不會(huì)出現(xiàn)在匹配結(jié)果中。這是很自然的事:我們尋找一個(gè)數(shù)字 \d+,而 (?=€) 只是一個(gè)測(cè)試,表示要匹配的數(shù)字后面應(yīng)該緊跟著  字符。

但在某些情況下,我們可能還想捕獲前瞻斷言和后瞻斷言所匹配的內(nèi)容,或者部分內(nèi)容。這也是可行的。只需要將該部分包裝在額外的括號(hào)中。

在下面的示例中,貨幣符號(hào) (€|kr) 和金額一起被捕獲了:

let str = "1 turkey costs 30€";
let regexp = /\d+(?=(€|kr))/; // €|kr 兩側(cè)有額外的括號(hào)

alert( str.match(regexp) ); // 30, €

后瞻斷言也一樣:

let str = "1 turkey costs $30";
let regexp = /(?<=(\$|£))\d+/;

alert( str.match(regexp) ); // 30, $

總結(jié)

當(dāng)我們想根據(jù)前面/后面的上下文匹配某些內(nèi)容的時(shí)候,前瞻斷言和后瞻斷言(通常被稱為“環(huán)視斷言”)很有用。

對(duì)于簡單的正則表達(dá)式,我們可以手動(dòng)執(zhí)行類似的操作。即:不管上下文,匹配所有可匹配的內(nèi)容,然后在循環(huán)中根據(jù)上下文進(jìn)行過濾。

請(qǐng)記住,str.match(沒有修飾符 g)和 str.matchAll(總是)將匹配項(xiàng)作為具有 index 屬性的數(shù)組返回,因此我們知道它在文本中的確切位置,并且可以檢查上下文。

但通常環(huán)視斷言更方便。

環(huán)視斷言類型:

模式 類型 匹配
X(?=Y) 肯定的前瞻斷言 X 后緊跟著 Y
X(?!Y) 否定的前瞻斷言 X 后沒緊跟著 Y
(?<=Y)X 肯定的后瞻斷言 X 緊跟在 Y 后面
(?<!Y)X 否定的后瞻斷言 X 沒緊跟在 Y 后面

任務(wù)


找到非負(fù)整數(shù)

這里有一個(gè)由整數(shù)組成的字符串。

創(chuàng)建一個(gè)正則表達(dá)式來找出所有的非負(fù)整數(shù)(包括 0)。

使用示例:

let regexp = /你的正則表達(dá)式/g;

let str = "0 12 -5 123 -18";

alert( str.match(regexp) ); // 0, 12, 123

解決方案

整數(shù)的正則表達(dá)式是 \d+ 。

我們可以通過在它前面加上否定的后瞻斷言來排除負(fù)數(shù):(?<!-)\d+。

盡管如此,如果我們現(xiàn)在嘗試使用上面的正則表達(dá)式,會(huì)發(fā)現(xiàn)有一個(gè)“例外”情況:

let regexp = /(?<!-)\d+/g;

let str = "0 12 -5 123 -18";

console.log( str.match(regexp) ); // 0, 12, 123, 8

正如我們所看到的,它從 -18 中配到了 8。要排除這種情況,我們需要確保正則表達(dá)式要從一個(gè)數(shù)的開頭開始匹配數(shù)字,而不是從另一個(gè)(不匹配的)數(shù)字的中間開始進(jìn)行匹配。

我們可以通過指定另一個(gè)否定的后瞻斷言來實(shí)現(xiàn)這一點(diǎn):(?<!-)(?<!\d)\d+。現(xiàn)在 (?<!\d) 確保匹配不會(huì)從另一個(gè)數(shù)字之后開始進(jìn)行匹配了,這正是我們所需要的。

我們也可以把它們合并成一個(gè)后瞻斷言:

let regexp = /(?<![-\d])\d+/g;

let str = "0 12 -5 123 -18";

alert( str.match(regexp) ); // 0, 12, 123

在標(biāo)簽頭后插入

我們有一個(gè)帶有 HTML 文檔的字符串。

編寫一個(gè)正則表達(dá)式,在 <body> 標(biāo)簽之后立即插入 <h1>Hello</h1>。標(biāo)簽可能具有特性(attribute)。

例如:

let regexp = /你的正則表達(dá)式/;

let str = `
<html>
  <body style="height: 200px">
  ...
  </body>
</html>
`;

str = str.replace(regexp, `<h1>Hello</h1>`);

之后,str 的值應(yīng)該為:

<html>
  <body style="height: 200px"><h1>Hello</h1>
  ...
  </body>
</html>

解決方案

為了在 <body> 標(biāo)簽后面插入內(nèi)容,我們必須先找到它。我們可以使用正則表達(dá)式 <body.*?> 來實(shí)現(xiàn)。

在本任務(wù)中,我們不需要修改 <body> 標(biāo)簽。我們只需要在它后面添加文本。

我們可以這樣做:

let str = '...<body style="...">...';
str = str.replace(/<body.*?>/, '{#content}amp;<h1>Hello</h1>');

alert(str); // ...<body style="..."><h1>Hello</h1>...

在替換字符串中,$& 表示匹配本身,即源文本中與 <body.*?> 相對(duì)應(yīng)的部分。它會(huì)被它自身加上 <h1>Hello</h1> 替換。

另一種方法是使用后瞻斷言:

let str = '...<body style="...">...';
str = str.replace(/(?<=<body.*?>)/, `<h1>Hello</h1>`);

alert(str); // ...<body style="..."><h1>Hello</h1>...

正如你所看到的,這個(gè)正則表達(dá)式中只有后瞻斷言部分。

它的工作原理如下:

  • 在文本的每個(gè)位置。
  • 檢查它前面是否有 ?<body.*?>?。
  • 如果有,就匹配該位置。

標(biāo)簽 <body.*?> 不會(huì)被作為結(jié)果返回。這個(gè)正則表達(dá)式的結(jié)果實(shí)際上是一個(gè)空字符串,但它只匹配前面緊挨著 <body.*?> 的位置。

因此,它將緊挨著 <body.*?> 的“空位置”替換為了 <h1>Hello</h1>。這樣就在 <body> 之后插入了內(nèi)容。

P.S. 正則表達(dá)式中的修飾符,例如 s 和 i 也很有用:/<body.*?>/si。這里修飾符 s 使得 . 可以匹配換行符,而修飾符 i 使 <body> 大小寫不敏感,可以匹配 <BODY>。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)