Javascript 捕獲組

2023-02-17 11:01 更新

模式的一部分可以用括號括起來 ?(...)?。這被稱為“捕獲組(capturing group)”。

這有兩個影響:

  1. 它允許將匹配的一部分作為結果數組中的單獨項。
  2. 如果我們將量詞放在括號后,則它將括號視為一個整體。

示例

讓我們看看在示例中的括號是如何工作的。

示例:gogogo

不帶括號,模式 go+ 表示 g 字符,其后 o 重復一次或多次。例如 goooo 或 gooooooooo

括號將字符組合,所以 (go)+ 匹配 go,gogogogogo等。

alert( 'Gogogo now!'.match(/(go)+/i) ); // "Gogogo"

示例:域名

讓我們做些更復雜的事 —— 搜索域名的正則表達式。

例如:

mail.com
users.mail.com
smith.users.mail.com

正如我們所看到的,一個域名由重復的單詞組成,每個單詞后面有一個點,除了最后一個單詞。

在正則表達式中是 (\w+\.)+\w+

let regexp = /(\w+\.)+\w+/g;

alert( "site.com my.site.com".match(regexp) ); // site.com,my.site.com

搜索有效,但該模式無法匹配帶有連字符的域名,例如 my-site.com,因為連字符不屬于 \w 類。

我們可以通過用 [\w-] 替換 \w 來匹配除最后一個單詞以外的每個單詞:([\w-]+\.)+\w+。

示例:電子郵件

擴展一下上面這個示例。我們可以基于它為電子郵件創(chuàng)建一個正則表達式。

電子郵件的格式為:name@domain。名稱可以是任何單詞,允許使用連字符和點。在正則表達式中為 [-.\w]+。

模式:

let regexp = /[-.\w]+@([\w-]+\.)+[\w-]+/g;

alert("my@mail.com @ his@site.com.uk".match(regexp)); // my@mail.com, his@site.com.uk

該正則表達式并不完美的,但多數情況下都能正確匹配,并且有助于修復輸入郵箱時的意外錯誤輸入。唯一真正可靠的電子郵件檢查只能通過發(fā)送電子郵件來完成。

匹配中的括號的內容

括號被從左到右編號。正則引擎會記住它們各自匹配的內容,并允許在結果中獲取它。

方法 str.match(regexp),如果 regexp 沒有修飾符 g,將查找第一個匹配項,并將它作為數組返回:

  1. 在索引 0 處:完整的匹配項。
  2. 在索引 1 處:第一個括號的內容。
  3. 在索引 2 處:第二個括號的內容。
  4. ……等等……

例如,我們想找到 HTML 標簽 <.*?> 并處理它們。將標簽內容(尖括號內的內容)放在單獨的變量中會很方便。

讓我們將內部內容包裝在括號中,像這樣:<(.*?)>

現在,我們在結果數組中得到了標簽的整體 <h1> 及其內容 h1

let str = '<h1>Hello, world!</h1>';

let tag = str.match(/<(.*?)>/);

alert( tag[0] ); // <h1>
alert( tag[1] ); // h1

嵌套組

括號可以嵌套。在這種情況下,編號也從左到右。

例如,在搜索標簽 <span class="my"> 時,我們可能會對以下內容感興趣:

  1. 整個標簽的內容:span class="my"
  2. 標簽名稱:span。
  3. 標簽特性:class="my"。

讓我們?yōu)樗鼈兲砑永ㄌ枺?code><(([a-z]+)\s*([^>]*))>。

這是它們的編號方式(根據左括號從左到右):


驗證:

let str = '<span class="my">';

let regexp = /<(([a-z]+)\s*([^>]*))>/;

let result = str.match(regexp);
alert(result[0]); // <span class="my">
alert(result[1]); // span class="my"
alert(result[2]); // span
alert(result[3]); // class="my"

result 的索引 0 中始終保存的是正則表達式的完整匹配項。

然后是按左括號從左到右編號的組。第一組返回為 result[1]。它包含了整個標簽內容。

然后是 result[2],從第二個左括號開始分組 ([a-z]+) —— 標簽名稱,然后在 result[3] 中:([^>]*)。

字符串中每個組的內容:


可選組

即使組是可選的并且在匹配項中不存在(例如,具有量詞 (...)?),也存在相應的 result 數組項,并且等于 undefined。

例如,讓我們考慮正則表達式 a(z)?(c)?。它查找 "a",后面是可選的 "z",然后是可選的 "c"。

如果我們在單個字母的字符串上運行 a,則結果為:

let match = 'a'.match(/a(z)?(c)?/);

alert( match.length ); // 3
alert( match[0] ); // a(完整的匹配項)
alert( match[1] ); // undefined
alert( match[2] ); // undefined

數組的長度為 3,但所有組均為空。

對字符串 ac 的匹配會更復雜:

let match = 'ac'.match(/a(z)?(c)?/)

alert( match.length ); // 3
alert( match[0] ); // ac(完整的匹配項)
alert( match[1] ); // undefined, 因為沒有 (z)? 的匹配項
alert( match[2] ); // c

數組長度依然是:3。但沒有組 (z)? 的匹配項,所以結果是 ["ac", undefined, "c"]。

帶有組搜索所有匹配項:matchAll

?matchAll? 是一個新方法,可能需要使用 polyfill

舊的瀏覽器不支持 matchAll。

可能需要進行 polyfill,例如 https://github.com/ljharb/String.prototype.matchAll.

當我們搜索所有匹配項(修飾符 g)時,match 方法不會返回組的內容。

例如,讓我們查找字符串中的所有標簽:

let str = '<h1> <h2>';

let tags = str.match(/<(.*?)>/g);

alert( tags ); // <h1>,<h2>

結果是一個匹配數組,但沒有每個匹配項的詳細信息。但是實際上,我們通常需要在結果中獲取捕獲組的內容。

要獲取它們,我們應該使用方法 str.matchAll(regexp) 進行搜索。

在使用 match 很長一段時間后,它才被作為“新的改進版本”被加入到 JavaScript 中。

就像 match 一樣,它尋找匹配項,但有 3 個區(qū)別:

  1. 它返回的不是數組,而是一個可迭代的對象。
  2. 當存在修飾符 ?g? 時,它將每個匹配項以包含組的數組的形式返回。
  3. 如果沒有匹配項,則返回的不是 ?null?,而是一個空的可迭代對象。

例如:

let results = '<h1> <h2>'.matchAll(/<(.*?)>/gi);

// results —— 不是數組,而是一個迭代對象
alert(results); // [object RegExp String Iterator]

alert(results[0]); // undefined (*)

results = Array.from(results); // 讓我們將其轉換為數組

alert(results[0]); // <h1>,h1(第一個標簽)
alert(results[1]); // <h2>,h2(第二個標簽)

我們可以看到,第一個區(qū)別非常重要,如 (*) 行所示。我們無法獲得 results[0] 的匹配項,因為該對象并不是偽數組。我們可以使用 Array.from 把它變成一個真正的 Array。在 Iterable object(可迭代對象) 一文中有關于偽數組和可迭代對象的更多詳細內容。

如果我們只需要遍歷結果,則 Array.from 沒有必要:

let results = '<h1> <h2>'.matchAll(/<(.*?)>/gi);

for(let result of results) {
  alert(result);
  // 第一個 alert:<h1>,h1
  // 第二個:<h2>,h2
}

……或使用解構:

let [tag1, tag2] = '<h1> <h2>'.matchAll(/<(.*?)>/gi);

matchAll 返回的每個匹配項,與不帶修飾符 g 的 match 所返回的格式相同:具有額外 index(字符串中的匹配索引)屬性和 input(源字符串)的數組:

let results = '<h1> <h2>'.matchAll(/<(.*?)>/gi);

let [tag1, tag2] = results;

alert( tag1[0] ); // <h1>
alert( tag1[1] ); // h1
alert( tag1.index ); // 0
alert( tag1.input ); // <h1> <h2>

為什么 ?matchAll? 的結果是可迭代對象而不是數組?

為什么這個方法這樣設計?原因很簡單 —— 為了優(yōu)化。

調用 matchAll 不會執(zhí)行搜索。相反,它返回一個可迭代對象,最初沒有結果。每次我們迭代它時才會執(zhí)行搜索,例如在循環(huán)中。

因此,這將根據需要找出盡可能多的結果,而不是全部。

例如,文本中可能有 100 個匹配項,但在一個 for..of 循環(huán)中,我們找到了 5 個匹配項,然后覺得足夠了并做出一個 break。這時引擎就不會花時間查找其他 95 個匹配。

命名組

用數字記錄組很困難。對于簡單的模式,它是可行的,但對于更復雜的模式,計算括號很不方便。我們有一個更好的選擇:給括號命名。

在左括號后緊跟著放置 ?<name> 即可完成對括號的命名。

例如,讓我們查找 “year-month-day” 格式的日期:

let dateRegexp = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/;
let str = "2019-04-30";

let groups = str.match(dateRegexp).groups;

alert(groups.year); // 2019
alert(groups.month); // 04
alert(groups.day); // 30

正如你所看到的,匹配的組在 .groups 屬性中。

要查找所有日期,我們可以添加修飾符 g。

我們還需要 matchAll 以獲取完整的組匹配:

let dateRegexp = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/g;

let str = "2019-10-30 2020-01-01";

let results = str.matchAll(dateRegexp);

for(let result of results) {
  let {year, month, day} = result.groups;

  alert(`${day}.${month}.${year}`);
  // 第一個 alert:30.10.2019
  // 第二個:01.01.2020
}

替換中的捕獲組

讓我們能夠替換 str 中 regexp 的所有匹配項的方法 str.replace(regexp, replacement) 允許我們在 replacement 字符串中使用括號中的內容。這使用 $n 來完成,其中 n 是組號。

例如,

let str = "John Bull";
let regexp = /(\w+) (\w+)/;

alert( str.replace(regexp, '$2, $1') ); // Bull, John

對于命名的括號,引用為 $<name>。

例如,讓我們將日期格式從 “year-month-day” 更改為 “day.month.year”:

let regexp = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/g;

let str = "2019-10-30, 2020-01-01";

alert( str.replace(regexp, '{#content}lt;day>.{#content}lt;month>.{#content}lt;year>') );
// 30.10.2019, 01.01.2020

非捕獲組 ?:

有時我們需要用括號才能正確應用量詞,但我們不希望它們的內容出現在結果中。

可以通過在開頭添加 ?: 來排除組。

例如,如果我們要查找 (go)+,但不希望括號內容(go)作為一個單獨的數組項,則可以編寫:(?:go)+。

在下面的示例中,我們僅將名稱 John 作為匹配項的單獨成員:

let str = "Gogogo John!";

// ?: 從捕獲組中排除 'go'
let regexp = /(?:go)+ (\w+)/i;

let result = str.match(regexp);

alert( result[0] ); // Gogogo John(完整的匹配項)
alert( result[1] ); // John
alert( result.length ); // 2(在數組中沒有其他數組項)

總結

括號將正則表達式中的一部分組合在一起,以便量詞可以整體應用。

括號組從左到右編號,可以選擇用 (?<name>...) 命名。

可以在結果中獲得按組匹配的內容:

  • 方法 ?str.match? 僅當不帶修飾符 ?g? 時返回捕獲組。
  • 方法 ?str.matchAll? 始終返回捕獲組。

如果括號沒有名稱,則匹配數組按編號提供其內容。命名括號還可使用屬性 groups

我們還可以在 str.replace 的替換字符串中使用括號內容:通過數字 $n 或者名稱 $<name>。

可以通過在組的開頭添加 ?: 來排除編號。當我們需要對整個組應用量詞,但不希望將其作為結果數組中的單獨項時這很有用。我們也不能在替換字符串中引用這樣的括號。

任務


檢查 MAC 地址

網絡接口的 MAC 地址 由 6 個以冒號分隔的兩位十六進制數字組成。

例如:'01:32:54:67:89:AB'。

編寫一個檢查字符串是否為 MAC 地址的正則表達式。

用例:

let regexp = /你的正則表達式/;

alert( regexp.test('01:32:54:67:89:AB') ); // true

alert( regexp.test('0132546789AB') ); // false (沒有冒號分隔)

alert( regexp.test('01:32:54:67:89') ); // false (5 個數字,必須為 6 個)

alert( regexp.test('01:32:54:67:89:ZZ') ) // false (尾部為 ZZ)

解決方案

一個兩位的十六進制數可以用 [0-9a-f]{2}(假設已設定修飾符 i)進行匹配。

我們需要匹配數字 NN,然后再重復 5 次 :NN(匹配更多數字);

所以正則表達式為:[0-9a-f]{2}(:[0-9a-f]{2}){5}

現在讓我們驗證一下此匹配規(guī)則可以捕獲整個文本:從開頭開始,在結尾結束。這是通過將模式包裝在 ^...$ 中實現的。

最終:

let regexp = /^[0-9a-f]{2}(:[0-9a-f]{2}){5}$/i;

alert( regexp.test('01:32:54:67:89:AB') ); // true

alert( regexp.test('0132546789AB') ); // false (沒有分號分隔)

alert( regexp.test('01:32:54:67:89') ); // false (5 個數字,必須為 6 個)

alert( regexp.test('01:32:54:67:89:ZZ') ) // false (尾部為 ZZ)

找出形如 #abc 或 #abcdef 的顏色值

編寫一個匹配 #abc 或 #abcdef 格式的顏色值的正則表達式。即:# 后跟著 3 個或 6 個十六進制的數字。

用例:

let regexp = /你的正則表達式/g;

let str = "color: #3f3; background-color: #AA00ef; and: #abcd";

alert( str.match(regexp) ); // #3f3 #AA00ef

P.S. 必須只匹配 3 位或 6 位十六進制數字的顏色值。不應該匹配 4 位數字的值,例如 #abcd。


解決方案

查找 # 號后跟著 3 位十六進制數的顏色值 #abc 的正則表達式:/#[a-f0-9]{3}/i。

我們可以再添加 3 位可選的十六進制數字。這樣剛好,不多不少。只匹配 # 號后跟著 3 位或 6 位十六進制數字的顏色值。

我們使用量詞 {1,2} 來實現:所以正則表達式為 /#([a-f0-9]{3}){1,2}/i。

這里將模式 [a-f0-9]{3} 用括號括起來,以在其外面應用量詞 {1,2}。

用例:

let regexp = /#([a-f0-9]{3}){1,2}/gi;

let str = "color: #3f3; background-color: #AA00ef; and: #abcd";

alert( str.match(regexp) ); // #3f3 #AA00ef #abc

這里存在一個小問題:上面的模式會匹配 #abcd 中的 #abc。為避免這一問題,我們可以在最后添加 \b。

let regexp = /#([a-f0-9]{3}){1,2}\b/gi;

let str = "color: #3f3; background-color: #AA00ef; and: #abcd";

alert( str.match(regexp) ); // #3f3 #AA00ef

找出所有數字

編寫一個正則表達式,找出所有十進制數字,包括整數、浮點數和負數。

用例:

let regexp = /你的正則表達式/g;

let str = "-1.5 0 2 -123.4.";

alert( str.match(regexp) ); // -1.5, 0, 2, -123.4

解決方案

帶有可選小數部分的正數:\d+(\.\d+)?

讓我們在開頭加上可選的 -

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

let str = "-1.5 0 2 -123.4.";

alert( str.match(regexp) );   // -1.5, 0, 2, -123.4

解析表達式

一個算術表達式由 2 個數字和一個它們之間的運算符組成,例如:

  • 1 + 2
  • 1.2 * 3.4
  • -3 / -6
  • -2 - 2

運算符為 "+"、"-"、"*" 或 "/" 中之一。

在開頭、之間的部分或末尾可能有額外的空格。

創(chuàng)建一個函數 parse(expr),它接受一個表達式作為參數,并返回一個包含 3 個元素的數組:

  1. 第一個數字
  2. 運算符
  3. 第二個數字

用例:

let [a, op, b] = parse("1.2 * 3.4");

alert(a); // 1.2
alert(op); // *
alert(b); // 3.4

解決方案

匹配數字的正則表達式:-?\d+(\.\d+)?。我們在上一題創(chuàng)建了這個表達式。

我們可以使用 [-+*/] 匹配運算符。連字符 - 在方括號中的最前面,因為在中間它表示字符范圍,而我們只想讓其表示字符 -

在 JavaScript 正則表達式 /.../ 中,我們應該對 / 進行轉義,稍后我們會對其進行處理。

我們需要一個數字、一個運算符以及另一個數字。其間可能會有空格。

完整的正則表達式為:-?\d+(\.\d+)?\s*[-+*/]\s*-?\d+(\.\d+)?。

它包含 3 個部分,以 \s* 分隔:

  1. -?\d+(\.\d+)? —— 第一個數字,
  2. [-+*/] —— 運算符,
  3. -?\d+(\.\d+)? —— 第二個數字。

為了使這里的每一部分成為結果數組中的單獨元素,所以我們把它們括在括號里:(-?\d+(\.\d+)?)\s*([-+*/])\s*(-?\d+(\.\d+)?)。

使用示例:

let regexp = /(-?\d+(\.\d+)?)\s*([-+*\/])\s*(-?\d+(\.\d+)?)/;

alert( "1.2 + 12".match(regexp) );

結果包括:

  • result[0] == "1.2 + 12" (完整的匹配項)
  • result[1] == "1.2" (第一組 (-?\d+(\.\d+)?) —— 第一個數字,包括小數部分)
  • result[2] == ".2" (第二組 (\.\d+)? —— 第一個數字的小數部分)
  • result[3] == "+" (第三組 ([-+*\/]) —— 運算符)
  • result[4] == "12" (第四組 (-?\d+(\.\d+)?) —— 第二個數字)
  • result[5] == undefined(第五組 (\.\d+)? —— 第二個數字的小數部分不存在,所以這里是 undefined)

我們只想要數字和運算符,不需要完全匹配的以及小數部分結果,所以讓我們稍微“清理”一下結果。

我們可以使用數組的 shift 方法 result.shift() 來刪去完全匹配的結果(數組的第一項)。

可以通過在開頭添加 ?: 來排除包含小數部分(數字 2 和 4)(.\d+) 的組:(?:\.\d+)?。

最終的解決方案:

function parse(expr) {
  let regexp = /(-?\d+(?:\.\d+)?)\s*([-+*\/])\s*(-?\d+(?:\.\d+)?)/;

  let result = expr.match(regexp);

  if (!result) return [];
  result.shift();

  return result;
}

alert( parse("-1.23 * 3.45") );  // -1.23, *, 3.45


以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號