2022 年 6 月 7 日

前瞻和後瞻

有時我們只需要找出符合某種模式的配對,而這些配對後面或前面接著另一種模式。

這方面有特殊的語法,稱為「前瞻」和「後瞻」,合稱為「環顧」。

首先,讓我們從 1 turkey costs 30€ 這樣的字串中找出價格。也就是:一個數字,後面接著 符號。

前瞻

語法為:X(?=Y),意為「尋找 X,但僅當後接 Y 時才匹配」。XY 可以是任何模式。

對於後接 的整數,正規表示式將為 \d+(?=€)

let str = "1 turkey costs 30€";

alert( str.match(/\d+(?=€)/) ); // 30, the number 1 is ignored, as it's not followed by €

請注意:前瞻僅為測試,括號 (?=...) 中的內容不包含在結果 30 中。

當我們尋找 X(?=Y) 時,正規表示式引擎會找到 X,然後檢查其後方是否緊接著 Y。如果不是,則會略過潛在的匹配,並繼續搜尋。

可以進行更複雜的測試,例如 X(?=Y)(?=Z) 表示

  1. 尋找 X
  2. 檢查 Y 是否緊接在 X 之後(如果不是,則略過)。
  3. 檢查 Z 是否也緊接在 X 之後(如果不是,則略過)。
  4. 如果兩個測試都通過,則 X 為匹配,否則繼續搜尋。

換句話說,此類模式表示我們同時尋找後接 YZX

這僅在模式 YZ 互不排斥時才有可能。

例如,\d+(?=\s)(?=.*30) 尋找後接空格 (?=\s),且其後方某處有 30\d+ (?=.*30)

let str = "1 turkey costs 30€";

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

在我們的字串中,完全匹配數字 1

負前瞻

假設我們想要同一個字串中的數量,而不是價格。這是一個數字 \d+,後不接

為此,可以套用負前瞻。

語法為:X(?!Y),意為「搜尋 X,但僅當後不接 Y 時」。

let str = "2 turkeys cost 60€";

alert( str.match(/\d+\b(?!€)/g) ); // 2 (the price is not matched)

後瞻

後瞻瀏覽器相容性

請注意:非 V8 瀏覽器(例如 Safari、Internet Explorer)不支援後瞻。

前瞻允許為「後續內容」新增條件。

回顧式類似,但它往後看。也就是說,它允許在前面有東西的情況下才比對樣式。

語法為

  • 正向回顧式:(?<=Y)X,比對 X,但前提是前面有 Y
  • 負向回顧式:(?<!Y)X,比對 X,但前提是前面沒有 Y

例如,我們將價格改為美金。美元符號通常在數字前面,因此要尋找 $30,我們將使用 (?<=\$)\d+,也就是一個前面有 $ 的金額

let str = "1 turkey costs $30";

// the dollar sign is escaped \$
alert( str.match(/(?<=\$)\d+/) ); // 30 (skipped the sole number)

而且,如果我們需要數量,也就是一個數字,且前面沒有 $,那麼我們可以使用負向回顧式 (?<!\$)\d+

let str = "2 turkeys cost $60";

alert( str.match(/(?<!\$)\b\d+/g) ); // 2 (the price is not matched)

擷取群組

一般來說,環顧括號內的內容不會成為結果的一部分。

例如,在樣式 \d+(?=€) 中, 符號不會被擷取為比對的一部分。這是很自然的:我們尋找數字 \d+,而 (?=€) 只是測試它後面應該接

但在某些情況下,我們可能也想擷取環顧式,或其一部分。這是可能的。只要將該部分包在額外的括號中即可。

在下面的範例中,貨幣符號 (€|kr) 與金額一起被擷取

let str = "1 turkey costs 30€";
let regexp = /\d+(?=(€|kr))/; // extra parentheses around €|kr

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

以下是回顧式的相同範例

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

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

摘要

當我們想比對依賴於前後文的情況時,前瞻式和回顧式(通常稱為「環顧式」)很有用。

對於簡單的正規表示式,我們可以手動執行類似的操作。也就是說:比對任何內容,在任何上下文中,然後在迴圈中依據上下文進行篩選。

請記住,str.match(沒有標記 g)和 str.matchAll(總是)會將比對結果傳回為具有 index 屬性的陣列,因此我們知道它在文字中的確切位置,並可以檢查上下文。

但一般來說,環顧式比較方便。

環顧式類型

樣式 類型 比對
X(?=Y) 正向前瞻式 X 如果後面接 Y
X(?!Y) 負前瞻 X 如果後面沒有接 Y
(?<=Y)X 正向後方參照 X 如果在 Y 之後
(?<!Y)X 負向後方參照 X 如果不在 Y 之後

任務

有一串整數數字。

建立一個正規表示式,只尋找非負整數(允許零)。

使用範例

let regexp = /your regexp/g;

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

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

整數數字的正規表示式為 \d+

我們可以透過在前面加上負向後方參照來排除負數:(?<!-)\d+

不過,如果我們現在試用看看,可能會注意到多了一個「額外」的結果

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

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

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

如你所見,它配對到 8,來自 -18。為了排除它,我們需要確保正規表示式從另一個(不配對)數字的開頭開始配對,而不是從中間。

我們可以透過指定另一個負向後方參照來做到:(?<!-)(?<!\d)\d+。現在 (?<!\d) 確保配對不會從另一個數字之後開始,這正是我們需要的。

我們也可以將它們合併成一個後方參照

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

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

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

我們有一個包含 HTML 文件的字串。

寫一個正規表示式,在 <body> 標籤之後立即插入 <h1>Hello</h1>。該標籤可能具有屬性。

例如

let regexp = /your regular expression/;

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

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

之後 str 的值應該是

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

為了在 <body> 標籤之後插入,我們必須先找到它。我們可以使用正規表示式模式 <body.*?> 來做到這一點。

在這個任務中,我們不需要修改 <body> 標籤。我們只需要在它之後新增文字。

以下是如何做到這一點

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

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

在替換字串中 $& 表示配對本身,也就是原始文字中對應到 <body.*?> 的部分。它會被它自己加上 <h1>Hello</h1> 取代。

另一種方法是使用後方參照

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

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

如你所見,這個正規表示式中只有後方參照部分。

它的運作方式如下

  • 在文字中的每個位置。
  • 檢查它前面是否有 <body.*?>
  • 如果有的話,那麼我們就有了配對。

標籤 <body.*?> 沒有被回傳。這個正規表示式的結果是一個空字串,但它只會配對在 <body.*?> 之前的位置。

因此,它會將在 <body.*?> 之前,且為「空行」的部分替換成 <h1>Hello</h1>。這就是 <body> 之後的插入。

P.S. Regexp 旗標,例如 si 也可以很有用:/<body.*?>/sis 旗標讓點 . 符合換行字元,而 i 旗標讓 <body> 也會符合 <BODY>,不分大小寫。

教學課程地圖

留言

留言前請先閱讀…
  • 如果你有建議要如何改進 - 請 提交 GitHub 議題 或提出 pull request,而不是留言。
  • 如果你看不懂文章中的某個部分 - 請詳細說明。
  • 要插入幾行程式碼,請使用 <code> 標籤,要插入多行,請用 <pre> 標籤包起來,要插入超過 10 行,請使用沙盒 (plnkrjsbincodepen…)