QA@IT

C# の正規表現クラス Regex で、Captures の要素がないと詰められてしまい、対応するインデックスが分からなくなってしまう

2686 PV

Visual C# 2010 で正規表現を使ったプログラムを作っていて、.NET に標準の Regex クラスを使っています。
Capture クラスや Group クラス絡みのことで、クラスの仕様がすでにこうだからもうしかたがないのか、それとも私の使い方がマズイだけでもっと便利な使い方があるのかを見極めたいと思っています。

目的は繰り返しのある入力文字列に対して、@"((\d+)|(\w+))" のような正規表現のパターンで、\d+ にマッチしたのか \w+ にマッチしたのかのどちらであるかを弁別したいです。具体的な例で言いますと、HTML を正規表現で解析する際に、たとえば TR タグの中に複数の TD タグがありますが、その TD タグの中に数字列かアルファベット列のいずれかが入っているとします。
つぎのようなサンプルコードを書きました。

// using System.Text.RegularExpressions;

private void button1_Click(object sender, EventArgs e)
{
    // 括弧の後方参照の番号       1    23     4
    Regex regex = new Regex(@"<TR>(<TD>((\d+)|(\w+))</TD>)*</TR>");

    Match match = regex.Match(@"<TR><TD>123</TD><TD>abc</TD><TD>456</TD></TR>");
    if (!match.Success)
    {
        throw new Exception();
    }

    Console.WriteLine("digits の数 = " + match.Groups[3].Captures.Count);
    Console.WriteLine("words  の数 = " + match.Groups[4].Captures.Count);
    for (int i = 0; i < match.Groups[2].Captures.Count; i++)
    {
        Console.WriteLine("i = " + i + ", digits = " + match.Groups[3].Captures[i] + ", words = " + match.Groups[4].Captures[i]);
    }
}

つぎのような表示になるようにしたいです。
digits の数 = 3
words の数 = 3
i = 0, digits = 123, words = null
i = 1, digits = null, words = abc
i = 2, digits = 456, words = null

実際はつぎのような出力になります。
digits の数 = 2
words の数 = 1
i = 0, digits = 123, words = abc
'System.ArgumentOutOfRangeException' の初回例外が System.dll で発生しました。

私は、マッチしなかったら、Captures の中の要素には null のようにないという目印が入っていてほしいのですが、上記のサンプルコードの挙動を見る限り、マッチしなかったらそれに対応した要素が入れられずにインデックス番号が詰めて格納されてしまいます。こういう仕様だと、何番目の TD タグに入っていたのかということが分からなくなってしまうので、これを詰められないで格納されるような正規表現パターン文字列の書き方はあるでしょうか?

追記します。

ご回答ありがとうございます。
質問では @"((\d+)|(\w+))" と書きましたが、私の頭の中では @"((\d+)|([a-z]+))" を意図していましたので、排他で良いです。
しかし、たとえば "1a2b3c" のような入力文字列はエラーとして弾きたいと思っています。そのため @"(\d*)(\w*)" だと "1" および "a2b3c" としてマッチしてしまい、エラーにならないので残念ながら使えません。
できれば正規表現のパターンを1回使うだけで、すべての解析を済ませたいと思っています。

追記します。

教えていただいた、Group を名前で参照するやりかたで解決できました。このやりかたに直したサンプルコードをここに追記します(ノートは字数制限があるので)。

private void button1_Click(object sender, EventArgs e)
{
    // 括弧の後方参照の番号       1    23                     4
    Regex regex = new Regex(@"<TR>(<TD>(((?<dig>\d+)(?<wrd>))|((?<dig>)(?<wrd>[a-z]+)))</TD>)*</TR>");

    Match match = regex.Match(@"<TR><TD>123</TD><TD>abc</TD><TD>456</TD></TR>");
    if (!match.Success)
    {
        throw new Exception();
    }

    Console.WriteLine("digits の数 = " + match.Groups["dig"].Captures.Count);
    Console.WriteLine("words  の数 = " + match.Groups["wrd"].Captures.Count);
    for (int i = 0; i < match.Groups[2].Captures.Count; i++)
    {
        Console.WriteLine("i = " + i + ", digits = " + match.Groups["dig"].Captures[i] + ", words = " + match.Groups["wrd"].Captures[i]);
    }
}

このコードの実行結果。
digits の数 = 3
words の数 = 3
i = 0, digits = 123, words =
i = 1, digits = , words = abc
i = 2, digits = 456, words =

回答

\d と \w に含まれる文字が排他であれば
(\d*)(\w*)
と並べてしまえば良いのですが
\wには\dの文字も含まれてしまったいるのが厄介ですね。
<td>.*</td>
としてしまい。
この要素に対し^(\d+)|(\w+)$により再分析するのはどうでしょう?

追記します。
排他で良いという追記がありますので
<TR>(<TD>((\d*)([a-z]*))</TD>)*</TR>
により

<TD>123</TD> => Groups[3].Capture[i] = "123",
                Groups[4].Capture[i] = ""

となり

<TD>abc</TD> => Groups[3].Capture[i] = "",
                Groups[4].Capture[i] = "abc"

となるのでGroup[3]のCapture数とGroup[4]のCapture数が
一致します。ただし <TD></TD>もマッチしてしまうのでこれを
さけたければ

<TR>(<TD>(((?<dig>\d+)(?<wrd>))|((?<dig>)(?<wrd>[a-z]+)))</TD>)*</TR>

のようにして、Groupを名前で参照すると良いかと思います。

編集 履歴 (1)
  • ありがとうございます。質問文に追記を書きました。 -
  • ありがとうございます。教えていただいた、Group を名前で参照するやりかたでできました。なるほど、あらかじめ Capture されるものを名前を付けて置いておけば良いわけですね。なお "1a2b3c" のような入力文字列をエラーとして弾くことも、このやりかたなら実現できました。このやりかたに直したサンプルコードを質問に追記します(ノートは字数制限があるので)。 -
ウォッチ

この質問への回答やコメントをメールでお知らせします。