2019年12月19日

キャプチャグループ

パターンの一部を丸括弧 (...)で囲むことができます。これは “キャプチャグループ” と呼ばれています。

これには2つの効果があります:

  1. 結果の配列の中で、マッチした部分を別々の項目として取得することができます。
  2. 丸括弧の後の量指定子がある場合、最後の文字ではなく全体に丸括弧が適用されます。

例でどのように丸括弧が動作するか見てみましょう:

例: gogogo

括弧なしだと、パターン /go+/g と、それに続けて1回以上の o の繰り返しを意味します、例えば、goooogooooooooo です。

括弧は文字をグループ化するので、(go)+go, gogo, gogogo etc を意味します:

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

例: domain

もっと複雑なもの – web サイトのドメインを探す正規表現を作りましょう。

例:

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-] に置き換えることで対処できます。

例: email

前の例が拡張できます。前の例をベースにして正規表現を作りましょう。

email のフォーマットは name@domain です。任意の文字が name になれ、ハイフンとドットが許可されます。正規表現では [-.\w]+ となります。

パターン:

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

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

この正規表現は完璧ではありませんが、ほとんどの場合機能し偶発的なミスを修正するのに役立ちます。なお、メールアドレスの唯一の信頼できるチェックは送信することによってのみ行うことができます。

括弧の中身

丸括弧は左から右へ番号付けされます。検索エンジンはそれぞれの中身を覚えており、パターンまたは置換文字列の中で内容を参照することができます。

メソッド str.match(regexp) では、regexpg フラグがなければ最初の一致を探し、それを配列として返します:

  1. インデックス 0: 完全な一致
  2. インデックス 1: 最初の丸括弧の中身
  3. インデックス 2: 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".

これらのための括弧を追加しましょう: <(([a-z]+)\s*([^>]*))>

番号の付け方は次の通りです(左から右に)

イメージ "/article/regexp-groups/regexp-nested-groups-pattern.svg" が見つかりませんでした
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] では2つ目の開始括弧 ([a-z]+) にいき、タグ名を返します。result[3]([^>]*) に対応するものです。

文字列中のすべてのグループの中身です:

イメージ "/article/regexp-groups/regexp-nested-groups-matches.svg" が見つかりませんでした

オプションのグループ

たとえグループがオプションであり、マッチに存在しない場合(例. 量指定子 (...)? がある)でも、対応する result の配列項目は存在し undefined と等価です。

例えば、正規表現 a(z)?(c)? を考えてみましょう。これは "a" に任意の "z"が続き, それに任意の "c" が続くパターンを探します。

もし1文字 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 は古いブラウザではサポートされていません。

https://github.com/ljharb/String.prototype.matchAll のような polyfill が必要な場合があります。

すべての一致を検索する場合(フラグ g)、match メソッドはグループの中身を返却しません。

例えば、文字列中のすべてのタグを探してみましょう。:

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

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

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

結果は一致したものの配列ですが、それぞれに関する詳細は含まれていません。しかし、実際には、通常は結果としてキャプチャグループの中身が必要です。

それらを取得するには、str.matchAll(regexp) メソッドを使用して検索する必要があります。

これは、“新しく改良されたバージョン” として、match のずっと後に JavaScript 言語に追加されました。

match のように一致を探しますが、3つの違いがあります:

  1. 配列ではなく反復可能(iterable)オブジェクトを返します。
  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 (1つ目のタグ)
alert(results[1]); // <h2>,h2 (2つ目のタグ)

ご覧の通り、行 (*) で示しているように1つ目の違いは非常に重要です。オブジェクトは疑似配列ではないため、results[0] で一致したものを取得することはできません。Array.from を使用して本当の Array にすることができます。疑似配列と反復可能についてのより詳細な内容に関しては、反復可能なオブジェクト を参照してください。

結果をループする場合は、Array.from は必要ありません。:

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

for(let result of results) {
  alert(result);
  // первый вывод: <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 の結果は配列ではなく反復可能オブジェクトなのでしょう?

なぜこのようにメソッドが設計されたのでしょう? 理由はシンプルです – 最適化のためです。

matchAll の呼び出しは検索を実行しません。代わりに、最初に結果なしの反復可能オブジェクトを返します。検索はループなど、それをイテレートするたびに実行されます。

そのため、必要なだけ結果が見つかります。

例. テキストに100個の一致がある可能性がありますが、for..of ループではそのうち5つを見つけ、それで十分と判断し、break します。すると、エンジンは他の 95 個の一致を探すために時間を費やさずにすみます。

名前付きグループ(Named groups)

番号でグループを覚えておくのは難しいです。簡単なパターンであれば問題ありませんが、より複雑なパターンの場合、括弧を数えるのには不便です。そのためのより良いオプションがあります: 括弧に名前をつけます。

開始括弧の直後に?<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}`);
  // first alert: 30.10.2019
  // second: 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, '$<day>.$<month>.$<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.matchg がない場合のみキャプチャグループを返します。
  • メソッド str.matchAll は常にキャプチャグループを返します。

括弧に名前がない場合、それらの中身は番号によって一致した配列から取り出すことができます。名前付きの場合はプロパティ groups でも利用できます。

str.replace では、置換文字列中に括弧の中身を使用することもできます。: 番号 $n あるいは名前 $<name>

グループは先頭に ?: を追加することで番号付けから外すことができます。これはグループ全体に対して量指定子を適用するが、結果の配列に別の項目としては不要なときに使われます。このような括弧は置換文字列でも参照できません。

タスク

ネットワークインターフェースの MAC アドレス はコロンで区切られた6つの2桁の16進数から構成されます。

例: '01:32:54:67:89:AB'.

文字列が MAC アドレスかをチエックする正規表現を書いてください。

使用方法:

let regexp = /your 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)

2桁の16進数は [0-9a-f]{2} (フラグ i がセットされる想定)です。

数値 NN、その後5回続く :NN が必要です。

正規表現: [0-9a-f]{2}(:[0-9a-f]{2}){5}

あとは、一致がテキスト全体を捕らえるよう、先頭から開始し、末尾で終わるようにします: ^...$ でパターンをラップすればOKです。

最終的に:

let regexp = /^[0-9a-fA-F]{2}(:[0-9a-fA-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数字が5個, 6個である必要があります)

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

形式 #abc または #abcdef で色をマッチする正規表現を書いてください。つまり、# の後に3桁または6桁の16進数が続きます。

使用例:

let reg = /your regexp/g;

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

alert( str.match(reg) ); // #3f3 #AA0ef

P.S. 正確に 3 または 6 桁の16進数であるべきです。#abcd のような値はマッチしません。

3桁の色 #abc を検索する正規表現は /#[a-f0-9]{3}/i です。

正確に3つの任意の16進数を指定できます。 私たちはそれ以上もそれ以下も必要ではありません。それを持っているか、持っていないかのどちらかです。

それらを追加する最も単純な方法は – 正規表現に追加することです: /#[a-f0-9]{3}([a-f0-9]{3})?/i

よりスマートな方法で書くこともできます: /#([a-f0-9]{3}){1,2}/i.

ここで正規表現 [a-f0-9]{3} は括弧の中にあり、全体として量指定子 {1,2} を適用します。

動作:

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

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

alert( str.match(reg) ); // #3f3 #AA0ef #abc

ここで小さな問題があります: パターンは #abcd の中で #abc を見つけます。これを避けるには、末尾に \b を追加します。:

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

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

alert( str.match(reg) ); // #3f3 #AA0ef

整数、浮動小数点や負数も含むすべての10進数を探す正規表現を書いてください。

使用例:

let reg = /あなたの正規表現/g;

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

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

任意で少数部分をもつ正の数は(前のタスクより): \d+(\.\d+)?.

先頭に任意の - を追加しましょう。:

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

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

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

算術式は2つの数字とそれらの間の演算子で構成されます。:

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

演算子は "+", "-", "*" または "/" のいずれかです。

先頭や末尾、間に余分なスペースがあるかもしれません。

式を取り、3つのアイテムを持つ配列を返す parse(expr) を作成してください。

  1. 最初の数値
  2. 演算子
  3. 2番目の数値

例:

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

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

数値の正規表現は: -?\d+(\.\d+)? です。前のタスクで作ったものです。

演算子は、[-+*/] です。ダッシュ - を先頭に置きます。中央にある場合、ダッシュは文字の範囲を意味しますが、それは必要ないからです。

JavaScript の正規表現 /.../ の中ではスラッシュをエスケープする必要があることに注意してください。

私たちは、数値、演算子、そして別の数値が必要です。そしてそれらの間にスペースがある場合があります。

完全な正規表現は次のようになります: -?\d+(\.\d+)?\s*[-+*/]\s*-?\d+(\.\d+)?.

配列として結果を取得するため、必要なデータの周りに括弧を置きましょう: 数値と演算子です: (-?\d+(\.\d+)?)\s*([-+*/])\s*(-?\d+(\.\d+)?).

動作:

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

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

結果は次の内容を含みます:

  • result[0] == "1.2 + 12" (完全なマッチ)
  • result[1] == "1" (最初の括弧)
  • result[2] == "2" (2番目の括弧 – 小数部 (\.\d+)?)
  • result[3] == "+" (…)
  • result[4] == "12" (…)
  • result[5] == undefined (最後は小数部がないので undefined です)

必要なのは数値と演算子だけです。小数部だけは不要です。

なので、(?:\.\d+)? のように、?: を追加することで、キャプチャグループから余分なグループを取り除きましょう。

最終的な解法は次の通りです:

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

  let result = expr.match(reg);

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

  return result;
}

alert( parse("-1.23 * 3.45") );  // -1.23, *, 3.45
チュートリアルマップ