2019年1月19日土曜日

番外編からの・・・VBAの正規表現と同じことをC#で書く

前回ExcelのVBAで書いた正規表現を扱うコードを、せっかくC#のブログをやっているんだからC#で書いてみようかな、ということで、色んな所(たとえばワレコC#講座さまなど)で調べて書いてみました。

VBAでは、VBScriptの中にある、正規表現を扱う機能を呼び出してプログラムから使う形でしたが、C#においては、.Net Flameworkもしくは.Net Coreの中にある正規表現エンジンを使う形になります。当然ながらC#の言語仕様に正規表現の取り扱いがあるわけではありません。

C#で正規表現のマッチをとるメソッドにはRegex.MatchとRegex.Matchesがあるんだそうで、Regex.Matchは複数のマッチする文字列がある場合でも最初にマッチした文字列だけをを取得し、Regex.Matchesは複数回マッチするような文字列がある場合、一度に全ての文字列を取得できます。
一方、VBAでは、マッチを取るのは正規表現オブジェクトであるRegExpオブジェクトのExecuteメソッド一本で、同じオブジェクトのGlobalプロパティを使って1回だけマッチを取るのか広域検索かを決める、というのが正規表現機能を司るRegExpオブジェクトの作り方になっています。
どちらが良いのかは好みの問題でしょうけど、VBAのほうがシンプルでわかりやすいように思います。
さて、.Net Frameworkで提供されている正規表現オブジェクトのメソッドであるRegex.MatchとRegex.Matchesですが、使い方が色々と異なるので、まずはRegex.Matchを使ってみました。

Visual Studio Codeで書いて、動作チェック済みです。Macでも動きます。

using System;
using System.Text.RegularExpressions;

namespace RegExpTest
{
    class Program
    {
        static void Main(string[] args)
        {
            Regex RE;

            RE = new Regex("(^.)(..)..(.+$)", RegexOptions.IgnoreCase);   // -> 1
            var MCS = RE.Match("000aaa111bbb");                                       // -> 2
            {
                Console.WriteLine( MCS.Groups.Count);                                   // -> 3 
                foreach (var MC in MCS.Groups)                                                 // -> 4
                {
                    Console.WriteLine(MC);
                }
            }
            Console.ReadKey(true);
        }
    }
}
これを動かすと、下記のようになります。

MacBook-Pro:RegexTest2 gustav$ dotnet run
4
000aaa111bbb
0
00
a111bbb

1の行でREという名前でRegexオブジェクトのインスタンスを生成、正規表現のパターンと大文字小文字を区別しないというオプションをRegexオブジェクトのREに設定しています。そして、2の行でMatchメソッドでパターンマッチを行います。

パターンマッチを行うと、Regex.Match.Groupsプロパティに、パターン全体としてマッチした文字列とVBAで言うところのSubMatchesに相当するグループ、上の例で言えば括弧で囲まれたパターンにマッチする文字列が入ります。そしてそれらの個数の合計がRegex.Match.Groups.Countに入ります。

なお、最初にも書きましたが、Regex.Match.Groupsメソッドでは、パターンマッチをする文字列の中に、正規表現のパターン全体としてマッチする文字列が複数あっても、最初にマッチするものしか返しません。

続いてRegex.Matchesです。

using System;
using System.Text.RegularExpressions;

namespace RegExpTest
{
    class Program
    {
        static void Main(string[] args)
        {
            Regex RE;

            RE = new Regex("(^.)(..)..(.+$)", RegexOptions.IgnoreCase);     // -> 1
            //RE = new Regex(".([a-z]+)", RegexOptions.IgnoreCase);         // -> 2
            var MCS = RE.Matches("000aaa111bbb");                                    // -> 3
            {
                Console.WriteLine(MCS.Count);                                                   // -> 4
                for(int i=0; i < MCS.Count; ++i){                                                  // -> 6
                    var M = MCS[i].Groups;                                                              // -> 7
                    for(int j=0; j < M.Count;++j) {                                                  // -> 8
                        var M_i_j = M[j].Value;                                                          // -> 9
                        Console.WriteLine("matches[{0}].Groups[{1}].Value = {2}", i, j, M_i_j);
                    }
                }

            }
        }
    }
}

上記をコンパイルして動かすと、以下のような出力を出します。
MacBook-Pro:RegexTest gustav$ dotnet run
1
matches[0].Groups[0].Value = 000aaa111bbb
matches[0].Groups[1].Value = 0
matches[0].Groups[2].Value = 00
matches[0].Groups[3].Value = a111bbb

この場合では、Matchメソッドで出て来たMatch.Groupsが、与える文字列の中で正規表現パターン全体としてマッチする文字列の数の分だけある、と考えるとわかりやすいと思います。

上の例では、正規表現パターン全体として与える文字列の中にマッチする文字列が1つしかないので、上記のような動作となりますが、下記のようにパターン全体としてマッチする文字列が複数個あると、そのそれぞれにGroupsプロパティが出来ます。

下記は、上のリストの中でコメントアウトしている2の行を生かして、1の行をコメントアウトしてコンパイル、動作させたものです。正規表現パターン全体として2回マッチする文字列が出て来ます。

MacBook-Pro:RegexTest gustav$ dotnet run
2
matches[0].Groups[0].Value = 0aaa
matches[0].Groups[1].Value = aaa
matches[1].Groups[0].Value = 1bbb
matches[1].Groups[1].Value = bbb
MacBook-Pro:RegexTest gustav$

0 件のコメント:

コメントを投稿

コメントを頂ければ幸いです。