2024年1月27日

キャプチャグループ

パターンの部分を括弧 (...) で囲むことができます。これは「キャプチャグループ」と呼ばれます。

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

  1. マッチした部分を、結果の配列の個別の項目として取得することができます。
  2. 括弧の後に量指定子を置くと、括弧全体に適用されます。

括弧がどのように機能するかを例で見てみましょう。

例: gogogo

括弧がない場合、パターン go+ は、g 文字の後に o が1回以上繰り返されることを意味します。例えば、goooogooooooooo のようになります。

括弧は文字をグループ化するため、(go)+go, gogo, gogogo などと続くものを意味します。

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

例: ドメイン

もう少し複雑なものを作成してみましょう。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-] に置き換えることで修正できます: ([\w-]+\.)+\w+

例: メール

前の例を拡張できます。それに基づいてメールの正規表現を作成できます。

メールの形式は 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

この正規表現は完璧ではありませんが、ほとんどの場合機能し、誤字の修正に役立ちます。メールの真に信頼できるチェックは、メールを送信することでしかできません。

マッチにおける括弧の内容

括弧には左から右に番号が付けられます。検索エンジンは、各括弧によって一致した内容を記憶し、結果でそれを取得できるようにします。

メソッド str.match(regexp) は、regexp にフラグ g がない場合、最初の一致を検索し、それを配列として返します。

  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*([^>]*))>

番号が付けられる方法を示します(左から右へ、開き括弧による)

実行

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] にはタグ: ([^>]*) が入ります。

文字列内の各グループの内容

オプションのグループ

グループがオプションで、一致に存在しない場合 (例えば、量指定子 (...)? を持つ)、対応する result 配列項目は存在し、undefined になります。

例えば、正規表現 a(z)?(c)? を考えてみましょう。これは、"a" の後にオプションで "z"、オプションで "c" が続くものを探します。

文字列に単一の文字 a で実行すると、結果は次のようになります。

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

alert( match.length ); // 3
alert( match[0] ); // a (whole match)
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 (whole match)
alert( match[1] ); // undefined, because there's nothing for (z)?
alert( match[2] ); // c

配列の長さは固定: 3 です。しかし、グループ (z)? には何もないため、結果は ["ac", undefined, "c"] になります。

グループを使用したすべての一致の検索: matchAll

matchAll は新しいメソッドで、ポリフィルが必要になる場合があります

メソッド matchAll は古いブラウザではサポートされていません。

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つの違いがあります。

  1. 配列ではなく、iterableオブジェクトを返します。
  2. フラグ g が存在する場合、すべての一致をグループを含む配列として返します。
  3. 一致がない場合、null ではなく、空のiterableオブジェクトを返します。

例えば

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

// results - is not an array, but an iterable object
alert(results); // [object RegExp String Iterator]

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

results = Array.from(results); // let's turn it into array

alert(results[0]); // <h1>,h1 (1st tag)
alert(results[1]); // <h2>,h2 (2nd tag)

見てわかるように、最初の違いは非常に重要です。(*)の行で示されています。オブジェクトは擬似配列であるため、results[0] として一致を取得することはできません。Array.from を使用して、それを実際の Array に変換できます。擬似配列とiterableの詳細については、記事 Iterables を参照してください。

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

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

for(let result of results) {
  alert(result);
  // first alert: <h1>,h1
  // second: <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 の結果が配列ではなくiterableオブジェクトであるのはなぜですか?

なぜメソッドがそのように設計されているのでしょうか?理由は簡単です。最適化のためです。

matchAll の呼び出しは検索を実行しません。代わりに、最初は結果を持たないiterableオブジェクトを返します。検索は、ループなどで反復処理するたびに実行されます。

したがって、必要な数の結果が検出されます。それ以上ではありません。

例えば、テキストに100個の一致がある可能性があり、for..of ループで5つの一致を見つけ、それで十分であると判断して break を行ったとします。次に、エンジンは他の95個の一致を検索するのに時間を費やしません。

名前付きグループ

グループを番号で記憶するのは困難です。単純なパターンでは実行可能ですが、より複雑なパターンでは括弧を数えるのは不便です。はるかに優れたオプションがあります。括弧に名前を付けることです。

これは、開き括弧の直後に ?<name> を置くことで行われます。

例えば、「年-月-日」形式の日付を探してみましょう。

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> になります。

例えば、「年-月-日」から「日.月.年」に日付の形式を変更してみましょう。

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!";

// ?: excludes 'go' from capturing
let regexp = /(?:go)+ (\w+)/i;

let result = str.match(regexp);

alert( result[0] ); // Gogogo John (full match)
alert( result[1] ); // John
alert( result.length ); // 2 (no more items in the array)

まとめ

括弧は正規表現の一部をグループ化するため、量指定子は全体として適用されます。

括弧グループには左から右に番号が付けられ、必要に応じて (?<name>...) で名前を付けることができます。

グループによって一致した内容は、結果で取得できます。

  • メソッド str.match は、フラグ g なしの場合のみキャプチャグループを返します。
  • メソッド 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 (no colons)

alert( regexp.test('01:32:54:67:89') ); // false (5 numbers, must be 6)

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

2桁の16進数は、[0-9a-f]{2}です(iフラグが設定されていると仮定)。

その数字NNと、その後に:NNが5回繰り返される(より多くの数字)必要があります。

正規表現は次のようになります:[0-9a-f]{2}(:[0-9a-f]{2}){5}

次に、一致がテキスト全体をキャプチャする必要があることを示しましょう。先頭から開始して末尾で終了します。それは、パターンを^...$で囲むことで行われます。

最終的に

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 (no colons)

alert( regexp.test('01:32:54:67:89') ); // false (5 numbers, need 6)

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

#abcまたは#abcdef形式の色に一致する正規表現を記述します。つまり、#の後に3つまたは6つの16進数が続きます。

使用例

let regexp = /your regexp/g;

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

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

追伸:これは、正確に3つまたは6つの16進数である必要があります。#abcdのような4桁の値は一致してはいけません。

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

正確に3つのオプションの16進数を追加できます。それ以上もそれ以下も必要ありません。色は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

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

使用例

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

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

先頭、末尾、または各部分の間に余分なスペースがある場合があります。

式を受け取り、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+)?

これは、3つの部分に分割され、その間に\s*があります。

  1. -?\d+(\.\d+)? – 最初の数値、
  2. [-+*/] – 演算子、
  3. -?\d+(\.\d+)? – 2番目の数値。

これらの各部分を結果配列の別々の要素にするには、それらを括弧で囲みます:(-?\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"(2番目のグループ(\.\d+)? – 最初の小数部分)
  • result[3] == "+"(3番目のグループ([-+*\/]) – 演算子)
  • result[4] == "12"(4番目のグループ(-?\d+(\.\d+)?) – 2番目の数値)
  • result[5] == undefined(5番目のグループ(\.\d+)? – 最後の小数部分は存在しないため、undefinedです)

完全一致や小数部分を含まず、数値と演算子のみが必要なため、結果を少し「クリーンアップ」しましょう。

完全一致(配列の最初の項目)は、配列をシフトすることで削除できます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

キャプチャしない?:を使用する代わりに、次のようにグループに名前を付けることもできます

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

  let result = expr.match(regexp);

  return [result.groups.a, result.groups.operator, result.groups.b];
}

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

コメント

コメントする前にこれを読んでください…
  • 改善するための提案がある場合は、コメントではなくGitHub issueまたはプルリクエストを送信してください。
  • 記事の中で何か理解できないことがあれば、詳しく説明してください。
  • コードを数語挿入するには、<code>タグを使用し、複数行の場合は<pre>タグで囲み、10行を超える場合はサンドボックス(plnkrjsbincodepen…)を使用してください。