有時我們只需要為一個模式找到那些在另一個模式之后或之前的匹配項。
有一種特殊的語法,稱為“前瞻斷言(lookahead)”和“后瞻斷言(lookbehind)”。
首先,讓我們從字符串中查找價格,例如 1 turkey costs 30€
。即:一個數(shù)字,后跟€
符號。
前瞻斷言
語法為:x(?=y)
,它表示“僅在后面是 Y
時匹配 X
”。這里的 X
和 Y
可以是任何模式。
那么對于一個后面跟著 €
的整數(shù),正則表達式應(yīng)該為:\d+(?=€)
。
let str = "1 turkey costs 30€";
alert( str.match(/\d+(?=€)/) ); // 30,數(shù)字 1 被忽略了,因為它后面沒有 €
請注意:前瞻斷言只是一個測試,括號 (?=...)
中的內(nèi)容不包含在匹配結(jié)果 30
中。
當(dāng)我們查找 X(?=Y)
時,正則表達式引擎會找到 X
,然后檢查其后是否有 Y
。如果沒有,則跳過潛在匹配,并繼續(xù)搜索。
更復(fù)雜的測試也是可能的,例如 X(?=Y)(?=Z)
表示:
- 尋找 ?
X
?。
- 檢查 ?
Y
? 是否緊跟在 ?X
? 之后(如果不是則跳過)。
- 檢查 ?
Z
? 是否也在 ?X
? 之后(如果不是則跳過)。
- 如果兩個測試都通過了,那么 ?
X
? 是匹配的,否則繼續(xù)搜索。
換句話說,這樣的模式意味著我們同時在尋找 X
后跟 Y
和 Z
。
這只有在模式 Y
和 Z
不是互斥的情況下才可行。
例如,\d+(?=\s)(?=.*30)
查找后跟著空格 (?=\s)
的 \d+
,并且有 30
在它之后的某個地方 (?=.*30)
:
let str = "1 turkey costs 30€";
alert( str.match(/\d+(?=\s)(?=.*30)/) ); // 1
在我們給出的字符串中,與數(shù)字 1
完全匹配。
否定的前瞻斷言
假設(shè)我們想要一個數(shù)量,而不是來自同一字符串的價格。那是一個數(shù)字 \d+
,后面不是 €
。
為此,我們可以使用否定的前瞻斷言。
語法是:X(?!Y)
,意思是“搜索 X
,但前提是后面沒有 Y
”。
let str = "2 turkeys cost 60€";
alert( str.match(/\d+\b(?!€)/g) ); // 2(價格不匹配)
后瞻斷言
后瞻斷言的瀏覽器兼容情況
請注意:非 V8 引擎的瀏覽器不支持后瞻斷言,例如 Safari、Internet Explorer。
前瞻斷言允許添加一個“后面要跟著什么”的條件判斷。
后瞻斷言也類似,只不過它是在相反的方向上進行條件判斷。也就是說,它只允許匹配前面有特定字符串的模式。
語法為如下:
- 肯定的后瞻斷言:
(?<=Y)X
,匹配 X
,僅在前面是 Y
的情況下。
- 否定的后瞻斷言:
(?<!Y)X
,匹配 X
,僅在前面不是 Y
的情況下。
例如,讓我們把價格換成美元。美元符號通常在數(shù)字前面,所以要查找 $30
我們將使用 (?<=\$)\d+
—— 一個前面帶 $
的數(shù)值:
let str = "1 turkey costs $30";
// 美元符號被轉(zhuǎn)義 \$
alert( str.match(/(?<=\$)\d+/) ); // 30(跳過了僅僅是數(shù)字的值)
如果我們需要找到量詞 —— 一個前面不帶 $
的數(shù)字,我們可以使用否定的后瞻斷言:(?<!\$)\d+
let str = "2 turkeys cost $60";
alert( str.match(/(?<!\$)\b\d+/g) ); // 2(價格不匹配)
捕獲組
一般來說,前瞻斷言和后瞻斷言括號中的內(nèi)容不會成為結(jié)果的一部分。
例如,在模式 \d+(?!€)
中,€
符號就不會出現(xiàn)在匹配結(jié)果中。這是很自然的事:我們尋找一個數(shù)字 \d+
,而 (?=€)
只是一個測試,表示要匹配的數(shù)字后面應(yīng)該緊跟著 €
字符。
但在某些情況下,我們可能還想捕獲前瞻斷言和后瞻斷言所匹配的內(nèi)容,或者部分內(nèi)容。這也是可行的。只需要將該部分包裝在額外的括號中。
在下面的示例中,貨幣符號 (€|kr)
和金額一起被捕獲了:
let str = "1 turkey costs 30€";
let regexp = /\d+(?=(€|kr))/; // €|kr 兩側(cè)有額外的括號
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)容的時候,前瞻斷言和后瞻斷言(通常被稱為“環(huán)視斷言”)很有用。
對于簡單的正則表達式,我們可以手動執(zhí)行類似的操作。即:不管上下文,匹配所有可匹配的內(nèi)容,然后在循環(huán)中根據(jù)上下文進行過濾。
請記住,str.match
(沒有修飾符 g
)和 str.matchAll
(總是)將匹配項作為具有 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ù)
這里有一個由整數(shù)組成的字符串。
創(chuàng)建一個正則表達式來找出所有的非負(fù)整數(shù)(包括 0)。
使用示例:
let regexp = /你的正則表達式/g;
let str = "0 12 -5 123 -18";
alert( str.match(regexp) ); // 0, 12, 123
解決方案
整數(shù)的正則表達式是 \d+
。
我們可以通過在它前面加上否定的后瞻斷言來排除負(fù)數(shù):(?<!-)\d+
。
盡管如此,如果我們現(xiàn)在嘗試使用上面的正則表達式,會發(fā)現(xiàn)有一個“例外”情況:
let regexp = /(?<!-)\d+/g;
let str = "0 12 -5 123 -18";
console.log( str.match(regexp) ); // 0, 12, 123, 8
正如我們所看到的,它從 -18
中配到了 8
。要排除這種情況,我們需要確保正則表達式要從一個數(shù)的開頭開始匹配數(shù)字,而不是從另一個(不匹配的)數(shù)字的中間開始進行匹配。
我們可以通過指定另一個否定的后瞻斷言來實現(xiàn)這一點:(?<!-)(?<!\d)\d+
?,F(xiàn)在 (?<!\d)
確保匹配不會從另一個數(shù)字之后開始進行匹配了,這正是我們所需要的。
我們也可以把它們合并成一個后瞻斷言:
let regexp = /(?<![-\d])\d+/g;
let str = "0 12 -5 123 -18";
alert( str.match(regexp) ); // 0, 12, 123
在標(biāo)簽頭后插入
我們有一個帶有 HTML 文檔的字符串。
編寫一個正則表達式,在 <body>
標(biāo)簽之后立即插入 <h1>Hello</h1>
。標(biāo)簽可能具有特性(attribute)。
例如:
let regexp = /你的正則表達式/;
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)容,我們必須先找到它。我們可以使用正則表達式 <body.*?>
來實現(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.*?>
相對應(yīng)的部分。它會被它自身加上 <h1>Hello</h1>
替換。
另一種方法是使用后瞻斷言:
let str = '...<body style="...">...';
str = str.replace(/(?<=<body.*?>)/, `<h1>Hello</h1>`);
alert(str); // ...<body style="..."><h1>Hello</h1>...
正如你所看到的,這個正則表達式中只有后瞻斷言部分。
它的工作原理如下:
- 在文本的每個位置。
- 檢查它前面是否有 ?
<body.*?>
?。 - 如果有,就匹配該位置。
標(biāo)簽 <body.*?>
不會被作為結(jié)果返回。這個正則表達式的結(jié)果實際上是一個空字符串,但它只匹配前面緊挨著 <body.*?>
的位置。
因此,它將緊挨著 <body.*?>
的“空位置”替換為了 <h1>Hello</h1>
。這樣就在 <body>
之后插入了內(nèi)容。
P.S. 正則表達式中的修飾符,例如 s
和 i
也很有用:/<body.*?>/si
。這里修飾符 s
使得 .
可以匹配換行符,而修飾符 i
使 <body>
大小寫不敏感,可以匹配 <BODY>
。
更多建議: