パターンの部分を括弧 (...)
で囲むことができます。これは「キャプチャグループ」と呼ばれます。
これには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…)を使用してください。