2019年1月20日日曜日

さらに番外編からの・・・Cで正規表現

前回、前々回と、VBAとC#で書いてみた正規表現の扱い方でしたが、せっかくだからCで書いたらどうなるか、書いてみました。Qiitaのこの記事美味しいもの様の記事を参考にさせていただきました。

C#では、正規表現オブジェクトのコンストラクターが正規表現パターンのコンパイルをしてしまうのですが、Cにはそのような概念がないので、プログラマが明示的にコンパイル指示をしなければいけませんね。

/*
  *  Cで正規表現を使う
  */

 #include <stdlib.h>
 #include <stdio.h>
 #include <regex.h>

/*
  * 与えられた文字列のstart文字目からend文字目までをfpで与えられたファイルポインターに出力
  */
 void outputstring( fp, string, start, end )
 FILE *fp;
 char *string;
 int start, end;
 {
     int i;

     for( i=start; i<end; ++i )
     {
         fputc( string[i], fp );
     }

 }

 int main( argc, argv )
 int argc;
 char *argv[];
  {
     char checkString[] = "000aaa111bbb";
     const char regex[] = "(^.)(..)..(.+$)";
     regex_t regexBuffer;
     regmatch_t patternMatch[4];
     int size = sizeof(patternMatch)/sizeof(regmatch_t);
     int i;

    if( regcomp( &regexBuffer, regex, REG_EXTENDED | REG_NEWLINE) != 0 ) {
        fprintf( stderr, "regex compile failed¥n" );
        return 0xffffffff;
    }

    if( regexec( &regexBuffer, checkString, size, patternMatch,0) != 0 )
    {
        fprintf( stderr, "No match!!¥n");
        return 0xffffffff;
    }

    for( i=0; i<size; ++i) {
        int startIndex = patternMatch[i].rm_so;
        int endIndex = patternMatch[i].rm_eo;
        if( startIndex == -1 || endIndex == -1 ) {
            fprintf(stderr, "exit¥n");
            continue;
        }
        outputstring( stdout, checkString, startIndex, endIndex);
        fprintf( stdout, "\n");
    }
    
    regfree( &regexBuffer);
    return 0;
 }
これを実行すると、以下のような出力を出します。
000aaa111bbb
0
00
a111bbb

なお、マッチした文字列を取得するのにpatternMatch[i].rm_soとpatternMatch[i].rm_eoという変数を使用しています。これはちょっと使い方に注意が必要で、patternMatch[i].rm_soはマッチした文字列の先頭を示しますが、patternMatch[i].rm_eoは、マッチした文字列の次の文字を示します。

上のプログラムでは、patternMatch[i]_rm_soをstartIndex、patternMatch[i].rm_eoをendIndexという変数に入れて使っていますが、これを使ってマッチした文字列を表すためには、文字列checkStringにおいて、checkString[startIndex]からcheckString[endIndex-1]までの文字を参照する必要があります。

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$

2019年1月11日金曜日

番外編:Excel VBAで正規表現

VBAで正規表現を使ってみるテスト

SubMatchをつかって、グループ化した正規表現にマッチする文字列を拾い出す

Sub RegExpr()
'正規表現のテスト
'正規表現をグループ化して、submatchで取り出す
    '正規表現を使うときに使う変数類
    Dim RE As Object
    Dim Match, Matches, Matches2

    Dim STR As String
    Dim i As Integer

    Set RE = CreateObject("VBScript.RegExp")

    STR = "aaa000bbb111"

    RE.Pattern = "(^.)(..)..(.+$)"
    RE.ignorecase = True
    RE.Global = True

    Set Matches = RE.Execute(STR)
    Debug.Print Matches.Count
    Debug.Print Matches.Item(0)
    If Matches.Count > 0 Then
    
        Set Matches2 = Matches(0).SubMatches
        Debug.Print Matches2.Count
        For i = 0 To Matches2.Count - 1
            Debug.Print Matches2(i)
        Next
    
    End If

End Sub


これの実行結果は、以下。イミディエイトウィンドウに出力されます。
     1 
    aaa000bbb111
     3 
    a
    aa
    0bbb111


最初の1はDebug.Print Machtes.Countの出力で、RE.Patternで与えているパターン全体にマッチした回数が1であることを示しています。次の行は、与えたパターンでパターンマッチを行ってマッチした文字列全体です。

括弧でくくられた文字列グループにマッチした文字列は

set Matches2 = Matches(0).SubMatches

で取得され、Matches2コレクションに格納されます。これをFor Next文で取り出しています。

次の3は括弧でくくられた文字列グループ何個にマッチしたかを示しています。Debug.Print Matches2.Countの出力です。

(^.)は行頭の1文字、(..)は行頭の1文字に続く2文字、..で2文字置いて(.+$)で1文字以上で行末まで続く任意の文字にマッチして、それがSubMatchに格納されます。