Javascript 捕獲組

2023-02-17 11:01 更新

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

這有兩個影響:

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

示例

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

示例:gogogo

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

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

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

示例:域名

讓我們做些更復(fù)雜的事 —— 搜索域名的正則表達(dá)式。

例如:

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

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

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

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

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

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

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

示例:電子郵件

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

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

模式:

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

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

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

匹配中的括號的內(nèi)容

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

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

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

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

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

現(xiàn)在,我們在結(jié)果數(shù)組中得到了標(biāo)簽的整體 <h1> 及其內(nèi)容 h1

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

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

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

嵌套組

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

例如,在搜索標(biāo)簽 <span class="my"> 時,我們可能會對以下內(nèi)容感興趣:

  1. 整個標(biāo)簽的內(nèi)容:span class="my"。
  2. 標(biāo)簽名稱:span。
  3. 標(biāo)簽特性:class="my"

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

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


驗(yàn)證:

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 中始終保存的是正則表達(dá)式的完整匹配項。

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

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

字符串中每個組的內(nèi)容:


可選組

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

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

如果我們在單個字母的字符串上運(yùn)行 a,則結(jié)果為:

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

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

數(shù)組的長度為 3,但所有組均為空。

對字符串 ac 的匹配會更復(fù)雜:

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

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

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

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

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

舊的瀏覽器不支持 matchAll。

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

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

例如,讓我們查找字符串中的所有標(biāo)簽:

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

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

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

結(jié)果是一個匹配數(shù)組,但沒有每個匹配項的詳細(xì)信息。但是實(shí)際上,我們通常需要在結(jié)果中獲取捕獲組的內(nèi)容。

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

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

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

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

例如:

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

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

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

results = Array.from(results); // 讓我們將其轉(zhuǎn)換為數(shù)組

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

我們可以看到,第一個區(qū)別非常重要,如 (*) 行所示。我們無法獲得 results[0] 的匹配項,因?yàn)樵搶ο蟛⒉皇莻螖?shù)組。我們可以使用 Array.from 把它變成一個真正的 Array。在 Iterable object(可迭代對象) 一文中有關(guān)于偽數(shù)組和可迭代對象的更多詳細(xì)內(nèi)容。

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

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

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

……或使用解構(gòu):

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

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

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? 的結(jié)果是可迭代對象而不是數(shù)組?

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

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

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

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

命名組

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

在左括號后緊跟著放置 ?<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èi)容。這使用 $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

非捕獲組 ?:

有時我們需要用括號才能正確應(yīng)用量詞,但我們不希望它們的內(nèi)容出現(xiàn)在結(jié)果中。

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

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

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

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(在數(shù)組中沒有其他數(shù)組項)

總結(jié)

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

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

可以在結(jié)果中獲得按組匹配的內(nèi)容:

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

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

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

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

任務(wù)


檢查 MAC 地址

網(wǎng)絡(luò)接口的 MAC 地址 由 6 個以冒號分隔的兩位十六進(jìn)制數(shù)字組成。

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

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

用例:

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

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 個數(shù)字,必須為 6 個)

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

解決方案

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

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

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

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

最終:

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 個數(shù)字,必須為 6 個)

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

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

編寫一個匹配 #abc 或 #abcdef 格式的顏色值的正則表達(dá)式。即:# 后跟著 3 個或 6 個十六進(jìn)制的數(shù)字。

用例:

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

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

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

P.S. 必須只匹配 3 位或 6 位十六進(jìn)制數(shù)字的顏色值。不應(yīng)該匹配 4 位數(shù)字的值,例如 #abcd


解決方案

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

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

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

這里將模式 [a-f0-9]{3} 用括號括起來,以在其外面應(yīng)用量詞 {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

找出所有數(shù)字

編寫一個正則表達(dá)式,找出所有十進(jìn)制數(shù)字,包括整數(shù)、浮點(diǎn)數(shù)和負(fù)數(shù)。

用例:

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

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

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

解決方案

帶有可選小數(shù)部分的正數(shù):\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

解析表達(dá)式

一個算術(shù)表達(dá)式由 2 個數(shù)字和一個它們之間的運(yùn)算符組成,例如:

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

運(yùn)算符為 "+""-"、"*" 或 "/" 中之一。

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

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

  1. 第一個數(shù)字
  2. 運(yùn)算符
  3. 第二個數(shù)字

用例:

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

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

解決方案

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

我們可以使用 [-+*/] 匹配運(yùn)算符。連字符 - 在方括號中的最前面,因?yàn)樵谥虚g它表示字符范圍,而我們只想讓其表示字符 -

在 JavaScript 正則表達(dá)式 /.../ 中,我們應(yīng)該對 / 進(jìn)行轉(zhuǎn)義,稍后我們會對其進(jìn)行處理。

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

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

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

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

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

使用示例:

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

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

結(jié)果包括:

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

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

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

可以通過在開頭添加 ?: 來排除包含小數(shù)部分(數(shù)字 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


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

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號