正規表現では、"()"などのグループ化を行う正規表現要素によって正規表現を部分式に分けてグループ化したり、グループに分かりやすい名前を与えることによって構造化することができます。 System.Text.RegularExpressions名前空間にはRegexクラス以外にも正規表現の処理に関連するクラスがいくつかあり、構造化した正規表現を処理するために用います。

Matchクラスは正規表現にマッチした箇所を表すクラスで、例えばMatchesメソッドを使って正規表現にマッチする箇所を複数取得する場合、マッチした個々の箇所をMatchインスタンスとして参照することができます。 このほかにも、Match.Nextメソッドを用いることでマッチする複数の箇所を逐次処理したり、Match.Resultメソッドを用いることで正規表現にマッチした箇所を別の文字列に置き換えて取得するといったこともできます。

Groupクラスは正規表現要素"()"グループ化された箇所を表すクラスで、Match.Groupsプロパティから参照することができます。 例えば、PerlやRubyでは正規表現"(\d+)/(\d+)"のそれぞれのカッコ内にマッチした文字列は$1, $2といった変数で参照することができますが、.NET Frameworkではmatch.Groups[1], match.Groups[2]といったようにグループを参照します。 (なお、$1, $2等は置換文字列として使用することができます。) また、"(?<num>\d+)"のような名前付きグループ(名前付きキャプチャ)にマッチした箇所を参照する場合も、match.Groups["num"]のようにして参照します。

さらに、各グループがキャプチャした文字列は、CaptureクラスとしてGroup.Capturesプロパティから参照することができます。 Match・Group・Captureの各クラスは互いに親子関係になっていて、これらのクラスを用いることで正規表現にマッチした箇所を細かく分析することができます。

§1 マッチ箇所の参照および操作(Matchクラス)

Matchクラスは正規表現にマッチした箇所(部分文字列)を表すクラスで、Regex.Matchメソッド・Regex.Matchesメソッドでの戻り値として返されます。 Matchクラスを参照することでマッチした箇所の文字列を参照したり、別の文字列に置換したりすることができます。

Regex.Matchメソッドでは、マッチする箇所がない場合はSuccessプロパティFalseのMatchインスタンスが返されます。

Matchクラスのプロパティを参照して正規表現にマッチした個所の文字列・インデックス・長さを取得する
using System;
using System.Text.RegularExpressions;

class Sample {
  static void Main()
  {
    var text = "The quick brown fox jumps over the lazy dog";
    var pattern = @"\w{6,}"; // 6文字以上の単語

    var m = Regex.Match(text, pattern); // 最初にマッチする箇所を取得

    Console.WriteLine(m.Success);

    // Match.Successプロパティの値は、IsMatchメソッドを呼び出した結果と同じ
    Console.WriteLine(Regex.IsMatch(text, pattern));
  }
}
実行結果
False
False

上記の例にもあるように、Matchメソッドが返すMatchインスタンスのSuccessプロパティは、Regex.IsMatchメソッドによってパターンマッチングを行った場合の結果と同じになります。


Regex.Matchメソッドで正規表現にマッチする箇所が見つかった場合は、SuccessプロパティがTrueのMatchインスタンスが返されます。 この際、Valueプロパティには実際にマッチした部分の文字列が設定されます。 同様に、IndexプロパティLengthプロパティにはマッチした部分のインデックスと長さが設定されます。

Match.Successプロパティを参照して正規表現にマッチする個所があるかどうか調べる
using System;
using System.Text.RegularExpressions;

class Sample {
  static void Main()
  {
    var text = "The quick brown fox jumps over the lazy dog";
    var pattern = @"\w{5,}"; // 5文字以上の単語

    var m = Regex.Match(text, pattern); // 最初にマッチする箇所を取得

    if (m.Success) { // マッチする個所があれば、SuccessはTrueとなる
      Console.WriteLine(m.Value); // マッチした個所の文字列を取得する

      // Index・Lengthプロパティにはマッチした個所のインデックスと長さが設定される
      // (実際にマッチした個所の文字列はValueプロパティで取得できるが、
      // 次のようにしても取得することができる)
      Console.WriteLine(text.Substring(m.Index, m.Length));
    }
  }
}
実行結果
quick
quick

Regex.Matchesメソッドで返されるMatchCollectionには、正規表現にマッチした箇所すべてに対応するMatchが格納された状態で返されます。 MatchCollectionを列挙して取得できるMatchのSuccessプロパティは、当然すべてTrueとなっているため、個々にチェックする必要はありません。

Regex.Matchesメソッドで正規表現にマッチする個所すべてに対応するMatchインスタンスを取得する
using System;
using System.Text.RegularExpressions;

class Sample {
  static void Main()
  {
    var text = "The quick brown fox jumps over the lazy dog";
    var pattern = @"\w{4,}"; // 4文字以上の単語

    // マッチする個所すべてを列挙
    foreach (Match m in Regex.Matches(text, pattern)) {
      Console.WriteLine(m.Value);
    }
  }
}
実行結果
quick
brown
jumps
over
lazy

Matchesメソッドが返すMatchCollectionクラスは、.NET Framework 4.5の時点でもIEnumerable<Match>を実装しておらず非ジェネリックコレクションとなっています。 foreach等で列挙する際は型を明示するか、必要に応じてCast<Match>()メソッドを用いるなどしてください。


Matchesメソッドでは、正規表現にマッチする箇所がなければ空のMatchCollectionを返します。

Regex.Matchesメソッドで正規表現にマッチする個所の数を表示する
using System;
using System.Text.RegularExpressions;

class Sample {
  static void Main()
  {
    var text = "The quick brown fox jumps over the lazy dog";
    var pattern = @"\w{10,}"; // 10文字以上の単語

    // マッチする個所すべてを取得
    var matches = Regex.Matches(text, pattern);

    // マッチした数を取得・表示する
    Console.WriteLine(matches.Count);
  }
}
実行結果
0

§1.1 Match.NextMatchメソッド

Match.NextMatchメソッドは、同じ正規表現が次にマッチする箇所を返すメソッドです。 Regex.Matchメソッドは最初にマッチした箇所に対応するMatchインスタンスを返しますが、NextMatchメソッドを呼び出すとその次にマッチする箇所に対応するMatchインスタンスを返します。

Match.NextMatchメソッドを使って同じ正規表現が次にマッチする箇所を取得する
using System;
using System.Text.RegularExpressions;

class Sample {
  static void Main()
  {
    var text = "The quick brown fox jumps over the lazy dog";
    var pattern = @"\w{5,}"; // 5文字以上の単語

    // 最初に一致する箇所を取得
    var m1 = Regex.Match(text, pattern);

    Console.WriteLine(m1.Value);

    // 同じ正規表現が次にマッチする箇所を取得
    var m2 = m1.NextMatch();

    Console.WriteLine(m2.Value);
  }
}
実行結果
quick
brown

次にマッチする箇所がない場合、NextMatchメソッドはSuccessプロパティがFalseのMatchインスタンスを返します。


Regex.Matchesメソッドではマッチした箇所すべてが同時に返されますが、例えば、Regex.Matchメソッドで文字列中の最初にマッチする箇所を取得したのち、一旦何らかの処理を行ってから次のマッチ箇所に移動して処理を継続したいといった場合には、NextMatchメソッドを使うことができます。

マッチ箇所が多数になると想定される場合、Regex.Matchesメソッドではそのすべての探索を終えるまで結果が返されませんが、NextMatchメソッドを使うと、まずRegex.Matchメソッドで最初にマッチする箇所を探索し、その後NextMatchメソッドでマッチする箇所を逐次処理していく、といったことができます。

次の例では、正規表現にマッチする全ての箇所を、Regex.Match+Match.NextMatchメソッドと、Regex.Matchesメソッドを使って取得した場合の違いを示しています。 結果はどちらも同じになりますが、マッチ箇所を逐次取得するか、一度に取得するかの違いがある点に注目してください。

Regex.Match+Match.NextMatchメソッドを使ってマッチ箇所を逐次処理する
using System;
using System.Text.RegularExpressions;

class Sample {
  static void Main()
  {
    var text = "The quick brown fox jumps over the lazy dog";
    var pattern = @"\w{4,}"; // 4文字以上の単語

    var m = Regex.Match(text, pattern); // 最初にマッチする箇所を取得

    while (m.Success) {
      // マッチした箇所の文字列を表示する
      Console.WriteLine(m.Value);

      // 次にマッチする箇所を取得する
      m = m.NextMatch();
    }
  }
}
Regex.Matchesメソッドを使ってマッチ箇所を一括して取得してから処理する
using System;
using System.Text.RegularExpressions;

class Sample {
  static void Main()
  {
    var text = "The quick brown fox jumps over the lazy dog";
    var pattern = @"\w{4,}"; // 4文字以上の単語

    // マッチする箇所すべてを取得して列挙
    foreach (Match m in Regex.Matches(text, pattern)) {
      // マッチした箇所の文字列を表示する
      Console.WriteLine(m.Value);
    }
  }
}
実行結果
quick
brown
jumps
over
lazy

§1.2 Match.Resultメソッド

Match.Resultメソッドは、マッチした箇所を引数で指定した文字列に置き換えます。 このメソッドは置換文字列によってマッチ箇所を別の文字列に置き換えた結果を取得するために使うことができます。

Regex.Replaceメソッドで置換文字列を指定した場合、元の文字列全体に同一の置換文字列を適用した結果が返されます。 一方Match.Resultメソッドでは、マッチした箇所ごとに置換文字列を指定して置換した結果を取得することができます。

次の例では、文字列中にあるmm/dd/yyyy形式の日付部分を探索し、Match.Resultメソッドを使ってyyyy-mm-ddの形式に置換して表示しています。 正規表現"(?<y>...)"でグループ化した部分は置換文字列"${y}"によって取得することができるため、これを使って日付の順序を変えています。 また、比較としてRegex.Replaceメソッドを使って置換した場合も併記しています。

Match.Resultメソッドを使ってmm/dd/yyyy形式の日付部分をyyyy-mm-dd形式に置換する
using System;
using System.Text.RegularExpressions;

class Sample {
  static void Main()
  {
    var text = "02/29/2016 00:00:00";

    // mm/dd/yyyy形式の日付 (月日年の正規表現をそれぞれグループ名m, d, yでグループ化)
    var pattern = @"(?<m>\d{2})/(?<d>\d{2})/(?<y>\d{4})";

    var m = Regex.Match(text, pattern);

    // マッチした部分をyyyy-mm-ddの形式に置換した結果を表示
    // (グループm, d, yにマッチした箇所をy-m-dの順で表示)
    Console.WriteLine(m.Result("${y}-${m}-${d}"));

    // 上記と同じ結果を生成するコード
    Console.WriteLine(m.Groups["y"].Value + "-" + m.Groups["m"].Value + "-" + m.Groups["d"].Value);

    // (Regex.Replaceメソッドでは、入力文字列全体で置換が行われる)
    Console.WriteLine(Regex.Replace(text, pattern, "${y}-${m}-${d}"));
  }
}
実行結果
2016-02-29
2016-02-29
2016-02-29 00:00:00

グループ化およびグループ化した部分の置換については後述の§.グループ化とグループ番号および§.グループと置換文字列も合わせて参照してください。

マッチする箇所がない状態(SuccessプロパティがFalse)のMatchインスタンスに対してResultメソッドを呼び出した場合、例外NotSupportedExceptionがスローされます。

置換文字列を使った置換については正規表現によるパターンマッチングと文字列操作 §.マッチした文字列への置換 (置換の正規表現要素)、置換文字列として使用できる置換の正規表現要素については.NET Frameworkで使用できる正規表現 §.置換を参照してください。


Match.Resultメソッドを使ったもうひとつ別の例として、次の例では正規表現にマッチする箇所をすべて取得し、マッチした箇所を強調表示しています。 この例で用いている置換文字列の$`および$'は、それぞれマッチした箇所の前と後にある文字列を参照する正規表現要素、$0は実際にマッチした文字列を参照する正規表現要素です。

Match.Resultメソッドを使って正規表現にマッチする箇所を強調表示する
using System;
using System.Text.RegularExpressions;

class Sample {
  static void Main()
  {
    var text = "The quick brown fox jumps over the lazy dog";
    var pattern = @"\w{4,}"; // 4文字以上の単語

    Console.WriteLine(text);
    Console.WriteLine();

    foreach (Match m in Regex.Matches(text, pattern)) {
      Console.WriteLine(m.Result("$`<$0>$'"));

      // 上記と同じ結果を生成するコード
      //Console.WriteLine(text.Substring(0, m.Index) + "<" + m.Value + ">" + text.Substring(m.Index + m.Length));
    }
  }
}
実行結果
The quick brown fox jumps over the lazy dog

The <quick> brown fox jumps over the lazy dog
The quick <brown> fox jumps over the lazy dog
The quick brown fox <jumps> over the lazy dog
The quick brown fox jumps <over> the lazy dog
The quick brown fox jumps over the <lazy> dog


§2 グループ(Groupクラス)

Groupクラスは正規表現要素()グループ化された正規表現にマッチした部分を表すクラスです。 Matchクラスが正規表現全体にマッチした箇所を表すのに対し、Groupクラスは正規表現のうちグループ化された部分にマッチした箇所を表します。 Matchクラスとは異なりGroupクラス単独で用いることはなく、Match.Groupsプロパティを通じて参照します。

§2.1 グループ化とグループ番号

正規表現要素()を用いると、正規表現をグループ化することができます。 例えば、yyyy-dd-mm形式の日付にマッチする正規表現は"\d{4}-\d{2}-\d{2}"と記述することができますが、この正規表現の年月日部分をそれぞれグループ化すると"(\d{4})-(\d{2})-(\d{2})"となります。

このようにグループ化した正規表現では、各グループにマッチした箇所を個別に参照することができます。 例えば正規表現"(\d{4})-(\d{2})-(\d{2})"の場合、この正規表現全体にマッチした文字列はMatchクラスによって参照することができますが、それとは別にそれぞれのグループ(この例では(\d{4}),(\d{2}),(\d{2})の各部分)にマッチした文字列はMatch.Groupsプロパティを通してGroupクラスの形で個別に参照することができます。

また、各グループには先頭側にあるグループから順に1から始まるインデックス(グループ番号)が割り当てられます。 各グループにマッチした箇所を参照するには、match.Group[1],match.Group[2]のようにします。

Match.Groupsプロパティを使ってyyyy-mm-dd形式にマッチする文字列の年月日をそれぞれ取得する
using System;
using System.Text.RegularExpressions;

class Sample {
  static void Main()
  {
    var text = "2016-02-29";
    var pattern = @"(\d{4})-(\d{2})-(\d{2})"; // yyyy-mm-dd形式の日付

    var m = Regex.Match(text, pattern);

    // マッチした文字列のうち、yyyy, mm, ddのそれぞれのグループに対応する箇所を取得する
    Console.WriteLine(m.Groups[1].Value);
    Console.WriteLine(m.Groups[2].Value);
    Console.WriteLine(m.Groups[3].Value);
  }
}
実行結果
2016
02
29

Matchクラスと同様に、Groupクラスもグループ化された正規表現にマッチした文字列とその位置・長さを表すValue, Index, Lengthの各プロパティを持っています。

グループ内にグループを記述する、つまりグループを入れ子にすることもできます。 グループが入れ子になっている場合、最も外側・先頭側にあるグループから順にグループ番号が割り当てられます。 グループ番号の割れ当て方については.NET Frameworkで使用できる正規表現 §.グループ番号の割り振られ方も参照してください。

§2.1.1 マッチ箇所全体を表すグループ ($0)

インデックスが0のグループは特殊なグループで、正規表現全体にマッチした部分を表します。 つまり、match.Group[0]matchそのものと同じ値となります。 これはPerlなどの正規表現における変数$0に相当するものです。

グループ番号0を参照して正規表現全体にマッチした箇所を取得する
using System;
using System.Text.RegularExpressions;

class Sample {
  static void Main()
  {
    var text = "2016-02-29 00:00:00";
    var pattern = @"\d{4}-\d{2}-\d{2}"; // yyyy-mm-dd形式の日付

    var m = Regex.Match(text, pattern);

    // グループ番号0は正規表現にマッチした文字列全体を表す(Match.Valueプロパティと同じ値となる)
    Console.WriteLine(m.Groups[0].Value);
    Console.WriteLine(m.Value);
  }
}
実行結果
2016-02-29
2016-02-29

グループ化された正規表現を含まない場合でも、常にインデックス0のグループを参照することができます。 正規表現にマッチする箇所がない場合(SuccessプロパティがFalseのMatch)でも同様にインデックス0のグループを参照することができ、このときグループの値は空の文字列となります。

§2.2 名前付きグループ

正規表現要素(?<name>)を使うと、正規表現のグループ化と同時に名前を与えることができます。 グループに名前を与えることにより、正規表現の構造を把握しやすくすることができ、またグループを参照するコードの可読性を向上させることができます。 例えば正規表現\d{4}にグループ名nameを与える場合は(?<name>\d{4})と記述します。

Match.Groupsプロパティで名前付きグループを参照する場合は、match.Groups["name"]のように、インデックスの代わりにグループに与えた名前を文字列で指定します。 このようにすることで、名前を付けてグループ化した部分の正規表現にマッチした部分を個別に参照することができます。

グループ名が与えられている場合でも、インデックスを使って個々のグループを参照することもできます。 このとき、各グループに割り当てられるグループ番号はグループ名の有無にかかわらず同じ番号が割り当てられます。

Match.Groupsプロパティと名前付きグループを使ってyyyy-mm-dd形式にマッチする文字列の年月日をそれぞれ取得する
using System;
using System.Text.RegularExpressions;

class Sample {
  static void Main()
  {
    var text = "2016-02-29";
    var pattern = @"(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})"; // yyyy-mm-dd形式の日付
    //var pattern = @"(\d{4})-(\d{2})-(\d{2})"; // 上記と同等の正規表現をグループ名なしで記述した場合

    var m = Regex.Match(text, pattern);

    // マッチした文字列のうち、グループ名'y', 'm', 'd'のそれぞれに対応する箇所を取得する
    Console.WriteLine(m.Groups["y"].Value);
    Console.WriteLine(m.Groups["m"].Value);
    Console.WriteLine(m.Groups["d"].Value);
    Console.WriteLine();

    // グループ名を与えている場合でも、各グループをインデックスで参照することもできる
    Console.WriteLine(m.Groups[1].Value);
    Console.WriteLine(m.Groups[2].Value);
    Console.WriteLine(m.Groups[3].Value);
  }
}
実行結果
2016
02
29

2016
02
29

正規表現の異なる部分に同じグループ名を与えることもできます。 この場合、それぞれに部分にマッチした箇所は同じグループ名を使って参照することができます。 これにより、正規表現中で同じ意味を持つ部分を同一の名前でグループ化することができ、それにマッチした箇所を同じグループ名で抽出することができます。

正規表現の異なる部分を同じグループ名で参照・抽出する
using System;
using System.Text.RegularExpressions;

class Sample {
  static void Main()
  {
    var text = "02/29/2012 2016-02-29";

    // mm/dd/yyyy形式またはyyyy-mm-dd形式の日付
    // (それぞれの年部分をグループ名"y"でグループ化)
    var pattern = @"\d{2}/\d{2}/(?<y>\d{4})|(?<y>\d{4})-\d{2}-\d{2}";

    foreach (Match m in Regex.Matches(text, pattern)) {
      // グループ名"y"にマッチした部分を表示
      Console.WriteLine(m.Groups["y"].Value);
    }
  }
}
実行結果
2012
2016

§2.3 グループと置換文字列

Regex.ReplaceメソッドMatch.Resultメソッドでは、置換文字列として$nを指定すると、グループにマッチした文字列に置換することができます。 例えば$1とすればグループ番号1のグループにマッチした文字列に置換されます。 以降同様に、$2$3…と参照することができます。 $0とした場合、マッチした文字列全体が参照されます。

グループ番号の置換文字列を使ってmm/dd/yyyy形式の日付をyyyy-mm-dd形式に置換する
using System;
using System.Text.RegularExpressions;

class Sample {
  static void Main()
  {
    var text = "02/29/2016 00:00:00";

    Console.WriteLine(text);

    // mm/dd/yyyy形式の日付 (グループ番号1=月、番号2=日、番号3=年となる)
    var pattern = @"(\d{2})/(\d{2})/(\d{4})";

    // mm/dd/yyyy形式の日付をyyyy-mm-ddの形式に置換して表示
    Console.WriteLine(Regex.Replace(text, pattern, @"$3-$1-$2"));
  }
}
実行結果
02/29/2016 00:00:00
2016-02-29 00:00:00

PerlやRubyの正規表現では$nを変数として用いることができますが、C#およびVB.NETでは$nを変数として用いることはできません。


グループ番号ではなく名前付きグループを参照する場合は${name}とします。 先の例を名前付きグループを使ったものに書き換えると次のようになります。

名前付きグループの置換文字列を使ってmm/dd/yyyy形式の日付をyyyy-mm-dd形式に置換する
using System;
using System.Text.RegularExpressions;

class Sample {
  static void Main()
  {
    var text = "02/29/2016 00:00:00";

    Console.WriteLine(text);

    // mm/dd/yyyy形式の日付 (月日年の正規表現をそれぞれグループ名m, d, yでグループ化)
    var pattern = @"(?<m>\d{2})/(?<d>\d{2})/(?<y>\d{4})";

    // mm/dd/yyyy形式の日付をyyyy-mm-ddの形式に置換して表示
    Console.WriteLine(Regex.Replace(text, pattern, @"${y}-${m}-${d}"));
  }
}
実行結果
02/29/2016 00:00:00
2016-02-29 00:00:00

$n${name}の他にも置換文字列にはいくつか種類があります。 詳しくは.NET Frameworkで使用できる正規表現 §.置換を参照してください。

§2.4 グループ化の除外 (非キャプチャグループ)

()および(?<name>)ではグループ番号やグループ名を与えたグループ化がなされますが、正規表現要素(?:)を使うとグループ番号やグループ名を持たないグループ(キャプチャされないグループ)を構成することができます。 例えば、正規表現を記述する上で()を使いたいがMatch.Groupsプロパティ等で処理する必要のない・除外したいグループを構成したい場合に、このようなグループを使うことができます。

次の例では、yyyy-mm-dd形式またはyyyy/mm/dd形式の日付にマッチする文字列を探索し、yyyy年mm月dd日の形式に置換しています。 この際、区切り文字の正規表現(/|-)はグループ化から除外しています。 これにより、年・月・日のそれぞれ対応するグループのみにグループ番号1・2・3が割り当てられます。

非キャプチャグループを含む正規表現を使ってyyyy-mm-ddまたはyyyy/mm/dd形式の日付をyyyy年mm月dd日の形式に置換する
using System;
using System.Text.RegularExpressions;

class Sample {
  static void Main()
  {
    var text = "2016/01/23 2016-04-05";

    // yyyy-mm-dd形式またはyyyy/mm/dd形式の日付
    // (年月日部分のみがグループ化され、区切り文字の部分はグループ化から除外されるため
    //  グループ番号1=年、番号2=月、番号3=日となる)
    var pattern = @"(\d{4})(?:/|-)(\d{2})(?:/|-)(\d{2})";

    // 上記の正規表現にマッチする日付をyyyy年mm月dd日の形式に置換する
    Console.WriteLine(Regex.Replace(text, pattern, "$1年$2月$3日"));
  }
}
実行結果
2016年01月23日 2016年04月05日

§2.4.1 明示的なグループ化 (RegexOptions.ExplicitCapture)

RegexOptions.ExplicitCaptureを指定した場合、明示的に名前を与えたグループのみキャプチャされるようになり、それ以外のグループ化構成要素はグループ化から除外されます。 正規表現要素(?:)が明示的にキャプチャの除外対象を指定するものであるのに対し、RegexOptions.ExplicitCaptureは名前付きグループと組み合わせて明示的にキャプチャの対象を指定するものです。

次の例では、RegexOptionsがNoneの場合(指定しない場合のデフォルト)と、RegexOptions.ExplicitCaptureの場合でキャプチャされる結果の違いを示しています。 RegexOptions.Noneではグループ名を与えていないグループもキャプチャされるのに対し、RegexOptions.ExplicitCaptureではグループ名を与えているグループのみがキャプチャされています。

RegexOptions.ExplicitCaptureを指定して明示的にグループ名を指定したグループのみをキャプチャする
using System;
using System.Text.RegularExpressions;

class Sample {
  static void Main()
  {
    var text = "2016/02/29 00:00:00";

    // yyyy/mm/dd形式の日付 (yyyy部分のみ名前付きグループでグループ化)
    var pattern = @"(?<y>\d{4})/(\d{2})/(\d{2})";

    // RegexOptions.Noneの場合にキャプチャされるグループを列挙して表示
    Console.WriteLine("[RegexOptions.None]");

    foreach (Group g in Regex.Match(text, pattern, RegexOptions.None).Groups) {
      Console.WriteLine(g.Value);
    }

    // RegexOptions.ExplicitCaptureの場合にキャプチャされるグループを列挙して表示
    Console.WriteLine("[RegexOptions.ExplicitCapture]");

    foreach (Group g in Regex.Match(text, pattern, RegexOptions.ExplicitCapture).Groups) {
      Console.WriteLine(g.Value);
    }
  }
}
実行結果
[RegexOptions.None]
2016/02/29
02
29
2016
[RegexOptions.ExplicitCapture]
2016/02/29
2016

上記の結果にもあるように、RegexOptions.ExplicitCaptureによって明示的なグループ化を行った場合でも、match.Groupsプロパティにはインデックスが0のグループ、つまりマッチした箇所全体を表すグループが常に含まれます。

Regex.Splitメソッドでグループ化した正規表現を区切りとして分割する場合、RegexOptions.ExplicitCaptureを指定するかどうかで結果が変わります。 具体的な結果の違いについては§.グループ化された正規表現による分割 (Regex.Split)を参照してください。

§2.5 グループ化された正規表現による分割 (Regex.Split)

Regex.Splitメソッドを用いた正規表現による文字列分割では、区切りとして指定する正規表現がグループ化されているかどうかで結果が変わります。 区切りの正規表現がグループ化されている場合、分割した結果にはグループにマッチした部分も含まれます。 一方、グループ化されていない場合は分割した結果には含まれません。

次の例では、ディレクトリ区切り文字として\または/を用いてファイルパスの分割を行っています。 区切りの正規表現"\\|/"をグループ化しているかどうかによってRegex.Splitメソッドの結果が変わります。

区切りの正規表現がグループ化されているかどうかでRegex.Splitメソッドの結果が変わる
using System;
using System.Text.RegularExpressions;

class Sample {
  static void Main()
  {
    var path = "path/to/file.txt"; // ファイルパス

    // グループ化した正規表現を区切りとして分割する
    // (分割結果には区切り文字自体も含まれる)
    Console.WriteLine("[grouping]");
    foreach (var p in Regex.Split(path, @"(\\|/)")) {
      Console.WriteLine(p);
    }

    // グループ化していない正規表現を区切りとして分割する
    // (分割結果に区切り文字は含まれない)
    Console.WriteLine("[non-grouping]");
    foreach (var p in Regex.Split(path, @"\\|/")) {
      Console.WriteLine(p);
    }
  }
}
実行結果
[grouping]
path
/
to
/
file.txt
[non-grouping]
path
to
file.txt

Regex.Splitメソッドで区切り文字自体も分割結果に含めたい場合は、この動作を利用して区切りの正規表現をグループ化すればよいことになります。


明示的なグループ化を行っていない正規表現を区切りとして用いる場合、RegexOptionsの指定によっても結果が変わります。 RegexOptions.ExplicitCaptureを指定すると明示的なグループ(名前付きのグループ)のみがキャプチャされることになるため、名前付きでないグループはグループ化していない場合と同様に扱われます。

区切りの正規表現が明示的でないグループ化の場合、RegexOptions.ExplicitCaptureを指定するかどうかでRegex.Splitメソッドの結果が変わる
using System;
using System.Text.RegularExpressions;

class Sample {
  static void Main()
  {
    var path = "path/to/file.txt"; // ファイルパス
    var pattern = @"(\\|/)"; // 区切りとして用いる正規表現(明示的ではないグループ)

    // 明示的ではないグループ化を行っている正規表現を区切りとして分割する
    // (RegexOptions.Noneでは、正規表現がグループ化されたものとして扱われるため、
    //  区切り文字自体も結果に含まれる)
    Console.WriteLine("[RegexOptions.None]");
    foreach (var p in Regex.Split(path, pattern, RegexOptions.None)) {
      Console.WriteLine(p);
    }

    // (RegexOptions.ExplicitCaptureでは、明示的でないグループはグループ化されていない場合と
    //  同等に扱われるため、区切り文字は結果に含まれない)
    Console.WriteLine("[RegexOptions.ExplicitCapture]");
    foreach (var p in Regex.Split(path, pattern, RegexOptions.ExplicitCapture)) {
      Console.WriteLine(p);
    }
  }
}
実行結果
[RegexOptions.None]
path
/
to
/
file.txt
[RegexOptions.ExplicitCapture]
path
to
file.txt

§3 キャプチャ(Captureクラス)

キャプチャとは、グループ化された正規表現にマッチした文字列の一つ一つを表します。 言い換えると、正規表現の部分式にマッチした箇所がキャプチャとなります。 例えば、3桁の数字の連続を表す正規表現"(\d{3})+"を考えた場合、グループ(\d{3})にマッチする文字列は複数となる場合があります。 その一つ一つがキャプチャとなります。 このように、グループに*, +{n}などの量指定子を指定した場合には、マッチした個々の文字列をキャプチャによって参照することができます。

キャプチャはCaptureクラスで扱います。 CaptureクラスはGroupクラスと同様に単独で用いることはなく、Group.Capturesプロパティを通じて参照します。

グループ化された正規表現にマッチする箇所が複数ある場合、match.Groupsプロパティは各グループの最後にマッチした箇所=最後のキャプチャを表すGroupを返します。 一方group.Capturesプロパティは、マッチした個々の箇所に対応するCaptureを返します。 グループとは異なり、各キャプチャには0から始まるインデックスが割り当てられます。

Group.Capturesプロパティを参照して正規表現の部分式にマッチした箇所を列挙する
using System;
using System.Text.RegularExpressions;

class Sample {
  static void Main()
  {
    var text = "0123456789";

    // 3桁の数字("(\d{3})")がひとつ以上連続("+")する箇所を取得
    var m = Regex.Match(text, @"(\d{3})+");

    // グループ1の正規表現("(\d{3})")にマッチした文字列を取得して表示
    // (最後にマッチした箇所が表示される)
    Console.WriteLine(m.Groups[1].Value);
    Console.WriteLine();

    // グループ1の正規表現にマッチした文字列(=キャプチャ)を列挙して表示
    foreach (Capture c in m.Groups[1].Captures) {
      Console.WriteLine(c.Value);
    }
  }
}
実行結果
678

012
345
678

キャプチャを使った例として、カンマ区切り文字列(CSV)から各カラムの値を分割して取得する例を考えます。 次の例で使用している正規表現"((^|,)(?<column>[^,]*))+"では、行頭またはカンマ"(^|,)"に続いて、グループ名columnでグループ化した正規表現"(?<column>[^,]*)"が現れる箇所の組み合わせが、1つ以上連続する箇所"(...)+"をCSVにマッチする正規表現として構成しています。 そして、グループcolumnでは、カンマ以外の文字が0文字以上連続する箇所"[^,]*"をカラムの値として構成しています。 この一連の正規表現にマッチする文字列を取得し、グループcolumnにキャプチャされた文字列を列挙すれば、CSVの各カラムの値を取得できることになります。

グループ化した正規表現とキャプチャを使ってCSV文字列から各カラムの値を取得する
using System;
using System.Text.RegularExpressions;

class Sample {
  static void Main()
  {
    var text = "a,012,,xxxxx";

    // CSVの正規表現(グループcolumnにCSVの各カラムの値がキャプチャされる)
    var m = Regex.Match(text, @"((^|,)(?<column>[^,]*))+");

    // グループcolumnの正規表現にキャプチャされた数を表示
    Console.WriteLine("Count = {0}", m.Groups["column"].Captures.Count);

    // グループcolumnにキャプチャされた文字列を列挙して表示
    foreach (Capture c in m.Groups["column"].Captures) {
      Console.WriteLine("\"{0}\"", c.Value);
    }
  }
}
実行結果
Count = 4
"a"
"012"
""
"xxxxx"

この例で使用している正規表現では、クォーテーション("')によって括られたカラムを処理することができません。 CSVをより厳密に処理するバージョンについては§.グループとキャプチャを使った例 (CSVの処理)で紹介しています。

§4 Match・Group・Capture各クラスの関係とマッチ箇所

MatchクラスGroupクラスCaptureクラスの違いとそれぞれの関係を整理します。 それぞれのクラスが表す箇所(部分文字列)は次のようになります。

Match
正規表現全体にマッチした箇所を表す
Group
正規表現の個々のグループにマッチした箇所を表す
Capture
正規表現の部分式(グループ化された正規表現)にマッチしたすべての箇所を表す

例えば、Windows形式のフルパスにマッチする正規表現"([A-Z]:)(\\[^\\ ]+)+"を考えた場合、各クラスが表す部分文字列は次のようになります。

Match
正規表現([A-Z]:)(\\[^\\ ]+)+の全体にマッチした箇所
Group
グループ化された正規表現([A-Z]:)(\\[^\\ ]+)+のそれぞれにマッチした箇所
Capture
グループ化された正規表現内の部分式[A-Z]:\\[^\\ ]+にマッチしたすべての箇所

以下、入力文字列"xcopy C:\test D:\target\backup\files"を例にとり、正規表現"([A-Z]:)(\\[^\\ ]+)+"とMatch・Group・Captureのそれぞれが表す箇所を図示します。 この正規表現は、入力文字列のうち"C:\test""D:\target\backup\files"の部分がマッチします。

このとき、Matchクラスが表す箇所とインデックスは次のようになります。

Matchが表す個所
x c o p y   C : \ t e s t   D : \ t a r g e t \ b a c k u p \ f i l e s
            |           |   |                                         |
            `-----0-----'   `--------------------1--------------------'

同様に、各MatchクラスのGroupsプロパティに含まれるGroupが表す箇所とインデックスは次のようになります。 グループ番号0のグループは、マッチした箇所全体を表す点に注意してください。

Groupが表す箇所
            |<-Match[0]>|   |<----------------Match[1]--------------->|
            |           |   |                                         |
x c o p y   C : \ t e s t   D : \ t a r g e t \ b a c k u p \ f i l e s
            | | |       |   | | |                                     |
            `1' `---2---'   `1' `-----------------2-------------------'
            |           |   |                                         |
            `-----0-----'   `--------------------0--------------------'

最後に、各GroupクラスのCapturesプロパティに含まれるCaptureが表す箇所とインデックスは次のようになります。 グループとは異なり、インデックス0は最初のキャプチャを表す点に注意してください。

Captureが表す箇所
          Group[1]        Group[1]
            | |             | |
            | | |Group[2]   | | |<-------------Group[2]-------------->|
            | | |       |   | | |                                     |
x c o p y   C : \ t e s t   D : \ t a r g e t \ b a c k u p \ f i l e s
            | | |       |   | | |           | |           | |         |
            `0' `---0---'   `0' `-----0-----' `-----1-----' `----2----'

Match・Group・Captureと、実際にマッチする部分をツリー形式で表すと次のようになります。 カッコ内は入力文字列中におけるマッチした部分のインデックスとその長さです。

実行結果
Regex: ([A-Z]:)(\\[^\\ ]+)+
Input: xcopy C:\test D:\target\backup\files

Match[0]         C:\test                   (6+7)
  Group[0]       C:\test                   (6+7)
    Capture[0]   C:\test                   (6+7)
  Group[1]       C:                        (6+2)
    Capture[0]   C:                        (6+2)
  Group[2]       \test                     (8+5)
    Capture[0]   \test                     (8+5)

Match[1]         D:\target\backup\files    (14+22)
  Group[0]       D:\target\backup\files    (14+22)
    Capture[0]   D:\target\backup\files    (14+22)
  Group[1]       D:                        (14+2)
    Capture[0]   D:                        (14+2)
  Group[2]       \files                    (30+6)
    Capture[0]   \target                   (16+7)
    Capture[1]   \backup                   (23+7)
    Capture[2]   \files                    (30+6)
Match・Group・Captureの各内容をツリー形式で表示する
using System;
using System.Text.RegularExpressions;

class Sample {
  static void Main()
  {
    string text = @"xcopy C:\test D:\target\backup\files";
    string pattern = @"([A-Z]:)(\\[^\\ ]+)+";

    Console.WriteLine("Regex: {0}", pattern);
    Console.WriteLine("Input: {0}", text);

    Console.WriteLine();

    PrintMatch(Regex.Matches(text, pattern));
  }

  static void PrintMatch(MatchCollection matches)
  {
    for (int i = 0; i < matches.Count; i++) {
      Match m = matches[i];

      Console.WriteLine("Match[{0}]         {1,-25} ({2}+{3})", i, m.Value, m.Index, m.Length);

      PrintGroup(m.Groups);

      Console.WriteLine();
    }
  }

  static void PrintGroup(GroupCollection groups)
  {
    for (int i = 0; i < groups.Count; i++) {
      Group g = groups[i];

      Console.WriteLine("  Group[{0}]       {1,-25} ({2}+{3})", i, g.Value, g.Index, g.Length);

      PrintCapture(g.Captures);
    }
  }

  static void PrintCapture(CaptureCollection captures)
  {
    for (int i = 0; i < captures.Count; i++) {
      Capture c = captures[i];

      Console.WriteLine("    Capture[{0}]   {1,-25} ({2}+{3})", i, c.Value, c.Index, c.Length);
    }
  }
}

§4.1 Match・Group・Captureの継承関係とコレクションクラスの対応

Match・Group・Captureの各クラスは次のような継承関係となっています。 ValueIndexLengthの各プロパティは、すべてCaptureクラスから継承されます。

また、それぞれのクラスと取得できるプロパティ、対応するコレクションクラスの関係は次のようになっています。

Matchクラス (Regex.Matchesメソッド) MatchCollectionクラス
Groupクラス Match.Groupsプロパティ GroupCollectionクラス
Captureクラス Group.Capturesプロパティ CaptureCollectionクラス

§4.2 グループとキャプチャを使った例 (CSVの処理)

以下の例では、名前付きグループとキャプチャを使ってCSVの各レコードからフィールドの値を抽出し、表形式に整形しています。 この例で使用する正規表現は複雑になるため、構成要素毎に分けた正規表現を組み立てています。 それぞれの正規表現は次のようになっています。

recordPattern = ((^|,)fieldPattern)*$
レコード(1行分)のパターン
行頭(^)またはカンマ(,)の後にfieldPatternが続く組み合わせが0個以上続き(*)、最後に行末($)が出現する文字列で構成される
fieldPattern = (quotedFieldPattern|plainFieldPattern)
レコード内の各フィールドのパターン
クオートされたフィールドquotedFieldPattern、もしくはクオートされていないフィールドplainFieldPatternで構成される
quotedFieldPattern = "(?<value>(""|[^"])*?)"
クオートされているフィールドのパターン
開きの二重引用符(")、クオートされた値((?<value>...)の部分)、閉じの二重引用符(")で構成される
クオートされた値は、エスケープされた二重引用符("")または閉じの二重引用符以外の文字([^"])が0個以上続く(*?)文字列で構成される
(ここでは (""|[^"]) が閉じの二重引用符にマッチしないよう、* ではなく最短一致の *? を使用する)
plainFieldPattern = (?<value>[^,]*)
クオートされていないフィールドのパターン
カンマ以外の文字([^,])が0個以上続く(*)文字列で構成される

なお、最終的に組み立てられた正規表現は実行結果として出力しています。 また、この例で使用している正規表現では、名前付きグループvalueに複数のフィールドがキャプチャされるため、Group.Capturesプロパティを参照してキャプチャしたフィールドの値を列挙しています。

正規表現を使ってクオートを含むCSVの各レコードからフィールドの値を抽出する
using System;
using System.Text.RegularExpressions;

class Sample {
  static void Main()
  {
    var plainFieldPattern  = "(?<value>[^,]*)";
    var quotedFieldPattern = "\"(?<value>(\"\"|[^\"])*?)\"";
    var fieldPattern       = string.Format("({0}|{1})", quotedFieldPattern, plainFieldPattern);
    var recordPattern      = string.Format("((^|,){0})*$", fieldPattern);

    Console.WriteLine(recordPattern);
    Console.WriteLine();

    var records = new string[] {
      "type,example,max value",
      "",
      "int,\"0, 16, 42\",int.MaxValue",
      "double,3.14,double.MaxValue",
      "bool,\"true, false\",",
      "string,\"\"\"foo\"\", \"\"bar\"\"\",",
    };

    foreach (var record in records) {
      Console.WriteLine("<{0}>", record);
    }

    Console.WriteLine();

    foreach (var record in records) {
      Match m = Regex.Match(record, recordPattern);

      if (m.Success) {
        foreach (Capture c in m.Groups["value"].Captures) {
          Console.Write("|{0,-20}", c.Value.Replace("\"\"", "\""));
        }
      }

      Console.WriteLine();
    }
  }
}
実行結果
((^|,)("(?<value>(""|[^"])*?)"|(?<value>[^,]*)))*$

<type,example,max value>
<>
<int,"0, 16, 42",int.MaxValue>
<double,3.14,double.MaxValue>
<bool,"true, false",>
<string,"""foo"", ""bar""",>

|type                |example             |max value           
|                    
|int                 |0, 16, 42           |int.MaxValue        
|double              |3.14                |double.MaxValue     
|bool                |true, false         |                    
|string              |"foo", "bar"        |