パターンの部分を括弧 (...) で囲むことができます。これは「キャプチャグループ」と呼ばれます。
これには2つの効果があります。
- マッチした部分を、結果の配列の個別の項目として取得することができます。
- 括弧の後に量指定子を置くと、括弧全体に適用されます。
例
括弧がどのように機能するかを例で見てみましょう。
例: gogogo
括弧がない場合、パターン go+ は、g 文字の後に o が1回以上繰り返されることを意味します。例えば、goooo や gooooooooo のようになります。
括弧は文字をグループ化するため、(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 がない場合、最初の一致を検索し、それを配列として返します。
- インデックス
0: 完全な一致。 - インデックス
1: 最初の括弧の内容。 - インデックス
2: 2番目の括弧の内容。 - ... など ...
例えば、HTMLタグ <.*?> を見つけて処理したいとします。タグの内容 (角括弧の中身) を別の変数に入れると便利です。
次のように、内側の内容を括弧で囲みましょう: <(.*?)>。
これで、タグ全体 <h1> とその内容 h1 の両方が結果の配列で取得されます。
let str = '<h1>Hello, world!</h1>';
let tag = str.match(/<(.*?)>/);
alert( tag[0] ); // <h1>
alert( tag[1] ); // h1
ネストされたグループ
括弧はネストできます。この場合、番号付けも左から右に行われます。
例えば、<span class="my"> でタグを検索するときに、次のようなものを求める場合があります。
- タグの内容全体:
span class="my"。 - タグ名:
span。 - タグの属性:
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つの違いがあります。
- 配列ではなく、iterableオブジェクトを返します。
- フラグ
gが存在する場合、すべての一致をグループを含む配列として返します。 - 一致がない場合、
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> で。
グループは、開始時に ?: を追加することで番号付けから除外できます。これは、グループ全体に量指定子を適用する必要があるが、結果配列の独立した項目としては必要ない場合に使用されます。また、置換文字列でそのような括弧を参照することもできません。
コメント
<code>タグを使用し、複数行の場合は<pre>タグで囲み、10行を超える場合はサンドボックス(plnkr、jsbin、codepen…)を使用してください。