Thumbnail image

正規表現のグループ

概要

はじめに

こんにちは、株式会社 Life Arc System の 開発担当の mzn です。

正規表現の中でもグループは、複数の正規表現パターンをまとめて扱うことができ、とても便利です。
一方で、キャプチャ後方参照等関連する機能も多く、複雑に感じることもあるのではないでしょうか。
mzn 自身、グループの学習に手間取った記憶があります。

そのため、今回、 mzn なりに正規表現のグループ、および関連する機能について記事にさせていただきました。
正規表現のグループ、および関連する機能に興味のある方の一助になれば幸いです。

なお、言語により正規表現の仕様や機能の名称、実装状況など統一されていない場合がある1ため、本投稿では、JavaScript にて正規表現を使用する前提2の記事となる旨、ご了承のほどお願いいたします。

対象読者

本投稿は、以下のような方を対象として執筆しています。

  • JavaScript で正規表現を使ったことがある方
  • 正規表現のグループの使い方を確認したい方
  • 正規表現のグループの詳細な機能について興味がある方

環境

本投稿の内容は、以下の環境にて確認しています。

  • 言語 … JavaScript (ES2015 以降)
  • ブラウザ … Google Chrome, Firefox

グループとは

グループを使用する主な目的は、複数の正規表現パターンをまとめて扱うことです。

正規表現は以下のような機能を持ち、基本的に 1 文字単位で判定を行います。

  • アルファベットや数字、記号のように、文字がどのような種類かの判定
  • 既定回数連続して、特定の文字が出現するかの判定
  • 先頭や最後尾、あるいは特定の文字の隣であるか、位置の判定

しかし、1 文字を対象とした判定や操作では、特定の文字列の繰り返しや位置の判定を利用したパターンマッチは、ループなど言語の制御構文を組み合わせなければできません。 このようなとき、複数の正規表現パターンをグループでまとめることで、正規表現のパターンマッチのみで、文字列単位での判定を行うことが可能となります。

例えば、ある文字列について、「ab<任意の数字>」が 2 回以上、5 回以下繰り返されているものか判定を行いたいとします。
グループを利用しない場合、一例として、以下のようなソースコードを記述することができるかと思います。

/**
 * 「ab<任意の数字>」が 2 回以上、5 回以下繰り返されているか判定し、真偽値を返す
 */
function isMatch_NotUseGroup(str) {
  const pattern = /^ab\d$/;

  // 3 文字ずつ順番にパターンマッチを行い、マッチした回数をカウントする
  // 1 つでもマッチしない場所があったら、引数で与えられた str 全体としてマッチしなかったものとする
  let count = 0;
  while (str.length > 0) {
    const threeLetters = str.substr(0, 3);
    str = str.substr(3);
    if (!pattern.test(threeLetters)) {
      return false;
    }
    count++;
  }

  return 2 <= count && count <= 5;
}

console.log(isMatch_NotUseGroup("ab1ab2ab3")); // true
console.log(isMatch_NotUseGroup("ab1")); // false
console.log(isMatch_NotUseGroup("ab1ab2")); // true
console.log(isMatch_NotUseGroup("ab1ab2ab3ab4ab5ab6")); // false
console.log(isMatch_NotUseGroup("ab1ab2ab")); // false

グループを使わない例では、JavaScript の制御構文を用いて回数の判定を行いましたが、グループを利用する場合、以下のように大幅にソースコードを削減することができます。
ただし、グループの括弧の数やネストを増やしすぎると、逆に正規表現パターンを理解することが難しくなるため、この辺りの塩梅は案件の特性に応じて基準を決めていただくと良いかと考えています。

/**
 * 「ab<任意の数字>」が 2 回以上、5 回以下繰り返されているか判定し、真偽値を返す
 */
function isMatch_UseGroup(str) {
  // 括弧でくくることで、「ab<任意の文字>」の 3 文字の判定をグループとし、
  // 繰り返しマッチも全て正規表現のパターンマッチで行う
  const pattern = /^(ab\d){2,5}$/;
  return pattern.test(str);
}

console.log(isMatch_UseGroup("ab1ab2ab3")); // true
console.log(isMatch_UseGroup("ab1")); // false
console.log(isMatch_UseGroup("ab1ab2")); // true
console.log(isMatch_UseGroup("ab1ab2ab3ab4ab5ab6")); // false
console.log(isMatch_UseGroup("ab1ab2ab")); // false

また、後述しますが、キャプチャグループを利用することで以下のような操作を行うことが可能になります。

  • 部分的にマッチした文字列が再出現するかの正規表現パターンの記述
  • 部分的にマッチした文字列をプログラム上で取得

グループの使い方

複数の正規表現パターンを()でくくることで、グループとなります。
()の中では全ての特殊文字を使用することができ、数量詞3やアサーション4をグループに対して適用することが可能です。

なお、()の内部を以下のように書き始めた場合はグループではなく、位置関係を判定するアサーションとなりますので、ご注意ください。

  • ?=
  • ?!
  • ?<=
  • ?<!

また、グループは 3 種類存在し、以下のような機能の違いがあります。

グループの名称() 内の書き始めマッチした部分文字列を添え字で参照マッチした部分文字列を名前で参照
キャプチャグループアサーションや他のグループに該当しないもの全て不可
非キャプチャグループ?:不可不可
名前付きキャプチャグループ?<Name>Nameには任意の文字列を指定可能

1. キャプチャグループ

キャプチャグループは、一番基本的なグループになります。
()内において、?から始まる特定の書き方をしない限り、グループはキャプチャグループとなります。

キャプチャグループを含む正規表現パターンを用いてパターンマッチングを行う際、キャプチャグループに一致した部分文字列は保持されます。 マッチした部分文字列は、正規表現中の後方参照や、プログラム上の文字列として添え字で参照することができます。
※実際に、後方参照やプログラム上の文字列として参照する方法は後述します。

// キャプチャグループと後方参照を利用した、
// 文字列 "abcdefghi defghi ghi" のみにマッチする正規表現パターン
// グループはネストすることができる
const pattern = /^abc(def(ghi)) \1 \2$/;

console.log(pattern.test("abcdefghi defghi ghi")); // true

2. 非キャプチャグループ

()内において、?:から書き始めると、グループは非キャプチャグループとなります。

非キャプチャグループはキャプチャグループと異なり、非キャプチャグループに一致した部分文字列が保持されません。
そのため、非キャプチャグループに一致した部分文字列を、正規表現中の後方参照やプログラム上の文字列として利用することはできません。

なお、キャプチャグループのほうが高機能で、非キャプチャグループの機能も内包しているため、非キャプチャグループを使う理由が無いように思われるかもしれません。
しかし、キャプチャグループは高機能な分オーバーヘッドがあります。
マッチした部分文字列の参照予定がないグループは、非キャプチャグループとすると効率がよいでしょう。

特に、数量詞によってキャプチャグループの繰り返しマッチを行っても、保持される部分文字列は繰り返しの最後にマッチしたものだけとなります。
繰り返しの中の、一番最後にマッチしたものだけ取得したいというような目的がなければ、グループの繰り返しマッチは非キャプチャグループで行うのが良いと考えます。

// 3 文字以上のアルファベットと 3 文字以上の数値が交互に現れている文字列か判定
// グループに対して数量詞を指定することで、文字列単位での出現有無や繰り返しを判定することができる
const pattern = /^(?:[a-zA-Z]{3,})?(?:\d{3,}[a-zA-Z]{3,})+(?:\d{3,})?$/;

console.log(pattern.test("abc1234ABCDEF123")); // true
console.log(pattern.test("1234ABCDEF123")); // true
console.log(pattern.test("abc12ABCDEF123")); // false
console.log(pattern.test("abc1234ab123")); // false
console.log(pattern.test("1234@ABCDEF 123")); // false

3. 名前付きキャプチャグループ

()内において、?<Name>から書き始めると、グループは名前付きキャプチャグループとなります。
Nameの部分には、数字以外から始まる任意の半角英数字の文字列を指定可能です。

名前付きキャプチャグループに一致した文字列は、Nameで設定した値をキーとして後方参照やプログラム上の文字列としてアクセス可能です。 グループに名前を付けることでグループの目的を読み取れるようになるため、複雑な正規表現パターンを書く際には、可読性の向上に一役買うかもしれません。

なお、キャプチャグループの機能は全て利用できます。

// 名前付きキャプチャグループと後方参照を用いて、日付文字列の区切り文字を統一
const pattern = /^\d{4}(?<delimiter>[-/]?)\d{2}\k<delimiter>\d{2}$/;

console.log(pattern.test("2020/01/15")); // true
console.log(pattern.test("2020-01-15")); // true
console.log(pattern.test("20200115")); // true

// 区切り文字が統一されていないものはマッチしない
console.log(pattern.test("2020/01-15")); // false

パターンマッチング結果の活用

キャプチャグループと名前付きキャプチャグループは、以下の 2 つの方法でグループにマッチした部分文字列を参照することができます。

  1. 後方参照
  2. プログラム上で、パターンマッチの結果オブジェクトから参照

また、どちらの方法も、添え字による参照と、名前による参照の 2 通りの方法でマッチした部分文字列を参照することができます。

なお、添え字で参照する場合、添え字は正規表現パターンで出現する、グループの開始括弧の位置で決まります。 具体的に、添え字は以下のルールで決定します。

  • グループの開始括弧の出現位置が前にあるものから、順番に 1 から添え字が割り振られる。
  • 開始括弧が非キャプチャグループの場合は、添え字の割り振りを行わず、次のグループの開始括弧を探す。
  • グループに数量詞が指定されていても、添え字の割り振りに繰り返しの回数は考慮されない。繰り返しでマッチした部分文字列は、全て同じ添え字の要素として格納されるため、繰り返しの最後にマッチした部分文字列で上書きされる。
  • グループに数量詞が指定されており、結果が 0 回の繰り返しであった場合には、該当する添え字の要素はundefinedとなる。
  • グループのネストの深さは添え字の割り振りに影響しない。添え字はグループの開始括弧の出現順序によってのみ決まる。

例えば、正規表現パターンとして、以下が与えられたとします。

const pattern =
  /(substrA (substrB (substrC )*))(?:substrD (substrE ))(?<named>substrF )/;

上記正規表現パターンに対して行われる、グループの添え字の割り振りは以下のようになります。

割り振られる添え字該当する正規表現パターンマッチした際、添え字の位置に格納される部分文字列の例
1substrA (substrB (substrC )*)"substrA substrB substrC ", "substrA substrB "
2substrB (substrC )*"substrB substrC ", "substrB "
3substrC"substrC ", ""
4substrE"substrE "
5substrF"substrF "
無しsubstrD (substrE )無し

1. 後方参照

後方参照を行うことで、パターンマッチ対象の文字列中で、グループにマッチした部分文字列が指定位置に再出現するか確認を行うことが可能です。
具体的な用途の例として、日付文字列の区切り文字の統一や、HTML の開始タグに対応する終了タグの判定など行うことができます。

正規表現には、後方参照を行うための特殊文字が存在します。

添え字による参照は、\(バックスラッシュ) と 後方参照を行う対象のグループに割り振られた添え字を、組み合わせて行います。

// substrC に実際にマッチした部分文字列が \3 の位置に、
// substrF に実際にマッチした部分文字列が \5 の位置に出現するかを確認する正規表現パターン
const pattern =
  /(substrA (substrB (substrC )*))(?:substrD (substrE ))(?<named>substrF )\3\5/;

console.log(
  pattern.test(
    "substrA substrB substrC substrC substrD substrE substrF substrC substrF "
  )
); // true
// substrC の部分は繰り返しが 0 回で undefined が格納されている
// undefined の部分を後方参照することはできないためNG
console.log(pattern.test("substrA substrB substrD substrE substrF substrF")); // false
// substrC の部分には "substrC" が格納されているが、\3 の位置は空文字となっており、
// グループにマッチした値と後方参照の位置の値が異なるためNG
console.log(
  pattern.test("substrA substrB substrC substrD substrE substrF substrF")
); // false

名前による参照は、\k<Name>と記述することで行うことが可能です。
Nameには、参照したい名前付きキャプチャグループと同じ名前を指定します。

// HTML要素の検証を行う
const pattern = /^<(?<tag>\w+)>(?<innerHTML>.*?)<\/\k<tag>>$/;

console.log(pattern.test("<div><p>inner</p></div>")); // true
// div に対応する終了タグが存在しない
console.log(pattern.test("<div><p>inner</p></article>")); // false

2. パターンマッチの結果オブジェクトから参照

グループにマッチした部分文字列は、プログラム上の文字列として、パターンマッチの結果オブジェクトから参照することも可能です。

JavaScript において、パターンマッチの結果オブジェクトは、下記文字列のメソッドの戻り値として取得できます。

matchメソッドの戻り値は配列で、パターンマッチした結果マッチした文字列全体が添え字 0 の要素へ、グループにマッチした部分文字列が添え字 1 以降の要素として格納されます。
グループに割り振られた添え字を、結果オブジェクトの添え字として指定することで、該当するグループにマッチした部分文字列を参照することができます。

また、groupsプロパティが追加されており、こちらには名前付きキャプチャの名前をキーとしたハッシュオブジェクトが格納されます。
<結果オブジェクト>.groups.<グループの名前>とコードを書くことで、該当する名前のグループにマッチした部分文字列を参照することができます。

const pattern =
  /(substrA (?<named1>substrB (substrC )*))(?:substrD (substrE ))(?<named2>substrF)/;
const result = "substrA substrB substrC substrC substrD substrE substrF".match(
  pattern
);

// 結果配列の添え字の内容
console.log(result[0]); // "substrA substrB substrC substrC substrD substrE substrF"
console.log(result[1]); // "substrA substrB substrC substrC "
console.log(result[2]); // "substrB substrC substrC "
console.log(result[3]); // "substrC "
console.log(result[4]); // "substrE "
console.log(result[5]); // "substrF"

// 名前付きキャプチャグループの内容
console.log(result.groups.named1); // "substrB substrC substrC "
console.log(result.groups.named2); // "substrF"

なお、matchメソッドの引数に gフラグ5を加えた正規表現オブジェクトを指定した場合、グループにマッチした部分文字列を参照することはできません。
gフラグを加えた正規表現のパターンマッチの結果から、グループにマッチした部分文字列を参照する場合はmatchAllメソッドを使用する必要があります。

matchAllの戻り値はイテレーターで、文字列中に存在する、正規表現にマッチした結果を全て取得することができます。
for...ofや、配列スプレッドを利用した配列への変換により、正規表現にマッチしたそれぞれの結果へアクセスすることが可能6です。
正規表現にマッチしたそれぞれの結果オブジェクトはmatchメソッドの結果オブジェクトと同様の構造をしています。

// g フラグを加え、文字列中の複数個所マッチする部分をすべて取得する
const pattern =
  /(substrA (?<named1>substrB (substrC )*))(?:substrD (substrE ))(?<named2>substrF)/g;
const iter =
  "substrA substrB substrC substrC substrD substrE substrF -- substrA substrB substrC substrD substrE substrF".matchAll(
    pattern
  );

// // for...of で順番にアクセス
// for (const result of iter) {
//     console.log(result[0]);
//     // ...
// }

// スプレッド構文で配列に変換したうえでアクセス
const results = [...iter];

// 最初にマッチした部分
console.log(results[0][0]); // "substrA substrB substrC substrC substrD substrE substrF"
console.log(results[0][1]); // "substrA substrB substrC substrC "
console.log(results[0][2]); // "substrB substrC substrC "
console.log(results[0][3]); // "substrC "
console.log(results[0][4]); // "substrE "
console.log(results[0][5]); // "substrF"

// 名前付きキャプチャグループの内容
console.log(results[0].groups.named1); // "substrB substrC substrC "
console.log(results[0].groups.named2); // "substrF"

// 2 つ目にマッチした部分
console.log(results[1][0]); // "substrA substrB substrC substrD substrE substrF"
console.log(results[1].groups.named1); // "substrB substrC "
console.log(results[1].groups.named2); // "substrF"

おわりに

正規表現のグループについて調べる中で、特にキャプチャグループにマッチする部分文字列の参照が面白いと感じました。
特定の条件に合致する部分文字列の抽出操作を簡潔に記述することが可能ですので、ソースファイルの静的な解析や、XML 文書等のパーサの実装を行う場合などに活躍しそうです。

また、グループは他の正規表現の機能と組み合わせることで真価を発揮するものと考えます。
そのため、グループのみではなく、正規表現全般の機能についてもより詳細に学びたいと思いました。

以上、正規表現のグループについて説明させていただきましたが、参考になりましたら幸いです。


  1. 正規表現といっても、ECMAScript や PCRE 、POSIX など、様々な仕様が書かれ、それぞれ文法や機能等異なる部分があります。
    例えば、正規表現のリファレンスについて、JavaScript の正規表現と PHP の PCRE 正規表現構文を比較した際に、翻訳による表記ゆれを含め、同じ概念でも下記のように名称が異なっているものがあります。

    • 特別な機能を持つ文字の呼称 - JavaScript では特殊文字、PHP ではメタ文字
    • 繰り返し回数を制御するパターン - JavaScript では数量詞、PHP では量指定子
    • ()でくくられたパターン - JavaScript ではグループ、PHP ではサブパターン

    上記のように、言語により正規表現の仕様や機能の名称、実装状況など統一されていない場合があります。 ↩︎

  2. 本記事内での用語の統一と、掲載ソースコードの再現環境構築の容易さから、JavaScript にて正規表現を使用する前提とさせていただきます。
    用語については、mdn web docs の正規表現のリファレンスに掲載されているものを利用しています。 ↩︎

  3. 数量詞は*+{n,m}のような特殊文字を記載することで、直前の正規表現パターンの繰り返しマッチ回数を制御します。
    詳細は mdn web docs の数量詞のリファレンスを参照してください。 ↩︎

  4. アサーションは、文頭や文末、単語の境界位置や前後に特定の正規表現パターンが存在する位置であるか、などの位置について判定を行います。
    詳細は mdn web docs のアサーションのリファレンスを参照してください。 ↩︎

  5. グローバル検索を行うためのフラグです。
    正規表現にgフラグを指定することで、文字列中に、正規表現パターンにマッチする場所が複数ある場合も、全て取得することが可能です。
    JavaScript の正規表現フラグについての詳細は mdn web docs より、フラグを用いた高度な検索を参照してください。 ↩︎

  6. matchAllメソッドのイテレーターは再起動不可能なもののため、for...ofや配列スプレッドによる配列への変換などは 1 度しか行うことができません。
    2 回目以降は空配列に対して行った結果と同様になりますので、matchAllメソッドの結果を複数回利用したい場合は、あらかじめ配列への変換を済ませておくとよいでしょう。

    なお、ここでの「再起動不可能」はmatchAllメソッド返値のリファレンスより引用しており、イテレーターを取得直後の状態に戻せないことを言います。
    JavaScript のイテレーターについての詳細は mdn web docs より、イテレーターとジェネレーターを参照してください。 ↩︎