【ワテのC#】正規表現 Regex.ReplaceでMatchEvaluatorが便利

この記事は約27分で読めます。
スポンサーリンク

このところ、C#の正規表現を勉強中だ。

プログラミングを長年やっている人でも、正規表現を知らない人や使えない人は多い。

正規表現を使うと文字列処理において各種の小細工が出来るので、いろんな応用が効く。

正規表現はとっても楽しい。

とは言ってもワテの場合はある程度は正規表現を使えるが、使いこなすと言うレベルでは無い。

正規表現を使ったパターンマッチング、置換などが出来る程度である。

複雑な正規表現はまだ書けない。

 

さて、そんなワテであるが、最近C#の正規表現で新しい手法を覚えた。

Regex.Replace()でMatchEvaluatorと言うのを使うと、マッチして取得したデータを置換する前に自分で加工する事が出来るのだ。

この記事ではそのMatchEvaluatorと言う奴の使い方を紹介したい。

即席で覚えたので間違いなど有りましたらご指摘頂けると有難いです。

 

テスト環境は以下の通り。

  • Windows 10 x64 Professional
  • Visual Studio 2017 Community
  • .Net Frameworkバージョン 4.6.2
  • C#コンソールプロジェクト

では本題に入ろう。

スポンサーリンク
ワテ推薦のプログラミングスクールで学ぶ
スポンサーリンク
スポンサーリンク

何をどう置換したいのか?

例えばこんな住所データがあるとする。

"100ー0014  東京都千代田区永田町1丁目7−1  "

国会議事堂の住所だ。

 

ところが、郵便番号の数字が半角や全角が混じっている。

またハイホンも半角のマイナスでは無くて全角のマイナスとか長音記号の「ー」などが入っているかも知れない。

そんなヘンテコな郵便番号を以下のように修正したい。

"100-0014 東京都千代田区永田町1丁目7−1"

 

従来のワテなら、こう言う住所データに対しては、C#の正規表現でMatchあるいはMatchesを使って郵便番号と住所の部分を分離する事は出来た。この記事で説明している。

その後で、分離したデータをちょこちょこ加工する感じ。

 

例えばこんなパターンを書いてみた。

var pattern = "〒?([0-90-9]{3})[-―ー-—\\s]([0-90-9]{4})\\s*(.*)";

 

その説明。

パターン 説明
〒?  郵便番号記号が0個あるいは1個あり、
([0-90-9]{3}) 半角数字か全角数字が3個あり、それをキャプチャする。
[-―ー-—\\s] 半角マイナスやそれに似たような記号やスペースのどれがが一個あり、 
([0-90-9]{4}) 半角数字か全角数字が4個あり、それをキャプチャする。
\\s*

空白文字が0個以上あり、

補足 厳密に言うと、\s は空白、タブ、およびフォーム フィードなど。[ \f\n\r\t\v] に等価

(.*) 0個以上の文字があり、それをキャプチャする。

 

今回は、こういうパターンで郵便番号の数字の部分を取り出して、全角文字列を半角文字列に変換した上で、元の文字列をReplaceするのだ。

そう言う手法を覚えたので備忘録としてメモしておく事にした。

入力データ

例えばこんな住所データがあるとする(入力データ)。

string[] addressArr =
{
    "〒100-0014 東京都千代田区永田町1丁目7−1 ",
    "100ー0014  東京都千代田区永田町1丁目7−1  ",
    "100ー0014  東京都千代田区永田町1丁目7−1 \r\n  100ー0014  東京都千代田区永田町1丁目7−1", // 複数行のデータ
    "100ー00    東京都千代田区永田町1丁目7−1  ", // 郵便番号が7桁では無いのでpatternにマッチしないデータ
};

 

四つの入力データがあるが、それらをそれをこんな風にしたい(出力データ)。

->100-0014 東京都千代田区永田町1丁目7−1<-
->100-0014 東京都千代田区永田町1丁目7−1<-
->100-0014 東京都千代田区永田町1丁目7−1
 100-0014 東京都千代田区永田町1丁目7−1<-
->100ー00 東京都千代田区永田町1丁目7−1  <-

なお、四番目のデータは郵便番号の数字が7桁では無いので、今回のパターンにはマッチしないので処理されていない。

またあるいは、住所の何丁目何番何号の数字の全角や半角が混在するケースもあるが、それらも半角数字あるいは全角数字のどちらかに統一したいが、今回はそこまではやらない。

と言うか、ややこしそうなので今回は出来なかった。

 

さて、MatchEvaluatorはこんな風に使う。

MatchEvaluatorの登場

Replace()の第二引数に自作の関数を入れておけば良い。

Console.WriteLine("\n①Regex.Replaceの中でMatchEvaluatorを使う。別の関数として");
foreach (var addr_i in addressArr)
{
    var rgx = new Regex(pattern);
    var addr_i2 = rgx.Replace(addr_i, MatchEvaluatorFunc_NumStrToHalf);
    Console.WriteLine("addr_i2= ->{0}<-", addr_i2);
}

そうすると、一回の正規表現検索にマッチした結果を引数としてこの関数が実行される。

 

で、その関数はこんな風に作ってみた。

MatchEvaluatorの例

 

static string MatchEvaluatorFunc_NumStrToHalf(Match m)
{
    // Match m にはパターンにマッチした結果が来るので、
    // その中のデータを取り出して、自由に加工できる。
    // 関数の戻り値の文字列が、Replaceの置換結果となる。
 
    //                                  // m.Groups[0].Valueはマッチした全体の文字列
    var postNum1 = m.Groups[1].Value;   // ([0-90-9]{3})
    var postNum2 = m.Groups[2].Value;   // ([0-90-9]{4})
    var postAddr = m.Groups[3].Value;   // (.*)
    postNum1 = NumStr_FullToHalf(postNum1); // 郵便番号前半三桁を半角数字に変換
    postNum2 = NumStr_FullToHalf(postNum2); // 郵便番号後半四桁を半角数字に変換
 
    var postAddrNew = postNum1 + "-" + postNum2 + " " + postAddr.TrimEnd(); // 目的の住所文字列を生成
 
    return postAddrNew;
}

今回利用した正規表現パターンでは、丸カッコで囲った (pattern) が三つあるので、それらはキャプチャされていて、置換の際には $1,$2,$3 で参照する事もできる。

例えば、

var addr_i2 = rgx.Replace(addr_i, "$1-$2 $3" );

とすれば、置換結果に利用出来る。

でもこれだと、キャプチャした単語をそのまま再利用するだけなので、全角文字列なら全角文字列のままだ。

 

それを半角数字に変換した後で再利用するのが今回の記事の目的である。

 

三つのキャプチャデータは、上の関数では引数 Match m の Group[1] ~ Group[3] に入っている。Group[0] はマッチした全体のデータが入っているが今回は使わない。

上記の関数内では、郵便番号の数字の部分を全角から半角数字に変換する処理をしている。

その為に、即席でこんな小細工関数を作ってみた。

文字列中の全角数字を半角数字に置換する関数

まあ、即席で作ったので一応動くが、より良い方法があるかもしれない。

 
static string NumStr_FullToHalf(string strFull)
{
    // strFullの中に全角数字0~9があればそれを半角数字に置換する関数
 
    string strHalf = "";
    for (int i = 0; i < strFull.Length; i++)
    {
        char cF = strFull[i];
        if ('0' <= cF && cF <= '9')
        {
            var cHalf = (char)('0' + (cF - '0'));
            strHalf += cHalf;
        }
        else
        {
            strHalf += cF;
        }
    }
    return strHalf;
}

ここでやっている処理は、C#が使っている文字コード(UTF-16)において、’0’~’9′ や ‘0’~’9’ の文字コードが連続で並んでいると言う前提である。

CやC++でもこの手の手法は良くあるが、ワテの場合あまり好きではない。

理由は、将来、文字コードが不連続となる可能性も無きにしも非ずだからだ。

でも、まあそんな事は無いだろうから心配は無いが。

MatchEvaluatorの実行結果

実行結果はこんな感じ。

⓪入力データ
addr_i= ->〒100-0014 東京都千代田区永田町1丁目7−1 <-
addr_i= ->100ー0014 東京都千代田区永田町1丁目7−1  <-
addr_i= ->100ー0014 東京都千代田区永田町1丁目7−1
100ー0014 東京都千代田区永田町1丁目7−1<-
addr_i= ->100ー00 東京都千代田区永田町1丁目7−1  <-


①Regex.Replaceの中でMatchEvaluatorを使う。別の関数として
addr_i2= ->100-0014 東京都千代田区永田町1丁目7−1<-
addr_i2= ->100-0014 東京都千代田区永田町1丁目7−1<-
addr_i2= ->100-0014 東京都千代田区永田町1丁目7−1
100-0014 東京都千代田区永田町1丁目7−1<-
addr_i2= ->100ー00 東京都千代田区永田町1丁目7−1  <- 

まあ、四番目のデータ以外は一応は目的通り置換出来ている。

三番目のデータは二行に渡るデータであるが、この場合にも二行共に正しく半角数字に置換出来た。

 

さて、これでまあ一件落着なのであるが、もう少し調べてみた。

例えば、ワテの場合、最近はLINQに凝っている。

何でもかんでもLINQを使って一行で書いてしまいたい症候群に罹っている感じ。

その影響だと思うが、今回のRegex.Replace() 処理では、MatchEvaluatorは便利ではあるが、別関数にしたくない。出来れば LINQ 風にインラインで一行に書いてしまいたい。

調べたら出来た。

二種類の方法を紹介したい。

MatchEvaluatorを別関数ではなくインラインで実行

ワテも良く分かっていない部分もあるが、こんな風に書くと良い。

Console.WriteLine("\n②Regex.Replaceの中でMatchEvaluatorを使う。インラインで");
foreach (var addr_i in addressArr)
{
    var rgx = new Regex(pattern);
    var addr_i2 = rgx.Replace(addr_i,
 
            m => NumStr_FullToHalf(m.Groups[1].Value)
                + "-"
                + NumStr_FullToHalf(m.Groups[2].Value)
                + " "
                + m.Groups[3].Value.TrimEnd()
         );
 
    Console.WriteLine("addr_i2= ->{0}<-", addr_i2);
}

この処理では、m にマッチした結果が来るので、それをラムダ式を使って匿名メソッドとして実行している事になる。

この例ではラムダ式の右辺に書いているのは一つの式なので、複雑な処理を記述する事は出来ない。

もし、複数行の処理を記述したい場合には、こんな風に書ける。

m => {
     var postAddrNew = NumStr_FullToHalf(m.Groups[1].Value)
      + "-"
      + NumStr_FullToHalf(m.Groups[2].Value)
      + " "
      + m.Groups[3].Value.TrimEnd();
 
     return postAddrNew;
 }

要するに { } で囲うと良い。

そして、処理結果をreturnで返す。

 

実行結果は、今までと同じ。

②Regex.Replaceの中でMatchEvaluatorを使う。インラインで
addr_i2= ->100-0014 東京都千代田区永田町1丁目7−1<-
addr_i2= ->100-0014 東京都千代田区永田町1丁目7−1<-
addr_i2= ->100-0014 東京都千代田区永田町1丁目7−1
 100-0014 東京都千代田区永田町1丁目7−1<-
addr_i2= ->100ー00 東京都千代田区永田町1丁目7−1  <-

 

C# 2.0 までの匿名メソッドでは以下のようにデレゲートを使って書いていた

C# 2.0 までの匿名メソッドでは以下のようにデレゲートを使って書いていたらしい。

デリゲートと言うのが正しいのかな?まあどっちでも良いか。

Console.WriteLine("\n③Regex.Replaceの中でMatchEvaluatorを使う。デレゲート方式");
foreach (var addr_i in addressArr)
{
    var rgx = new Regex(pattern);
    var addr_i2 = rgx.Replace(addr_i,
 
         delegate (Match m)
         {
             var postNum1 = m.Groups[1].Value;
             var postNum2 = m.Groups[2].Value;
             var postAddr = m.Groups[3].Value;
             postNum1 = NumStr_FullToHalf(postNum1);
             postNum2 = NumStr_FullToHalf(postNum2);
 
             var postAddrNew = postNum1 + "-" + postNum2 + " " + postAddr.TrimEnd();
 
             return postAddrNew;
         });
 
    Console.WriteLine("addr_i2= ->{0}<-", addr_i2);
}

ワテの場合、C#を本格的にやり始めたのはC#3.0になってからだ。

なので、C# 2.0( .NET Framework 2.0、Visual Studio 2005)の頃の話題は疎い。

と言う事で、delegateと言うのを使えば上の例のように匿名メソッドを記述出来るらしい。

でもまあ、今ならもう一つ前の例のように、単純にラムダ式で m => { … } と書く方がシンプルだな。

 

実行結果は同じなので省略。

今回のRegex.ReplaceのMatchEvaluator実験で用いた全ソースコード

まあ、ワテの場合、ヘンテコな変数名や関数名を使うので突っ込み所は多いと思う。

もし何かお気づきの点などありましたら御遠慮無くご指摘頂ければ有難いです。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
 
namespace 今日覚えたCSharpの小技ConsoleApp
{
    class Class2_MatchEvaluatorを使う
    {
 
        public static void test5()
        {
 
            Console.WriteLine("デフォルトのエンコーディングはシフトジスのようだ。");
            Console.WriteLine("IANA Name: {0}", Console.OutputEncoding.WebName);   // 標準のエンコード shift_JIS
            Console.WriteLine("Code Page: {0}", Console.OutputEncoding.CodePage);  // コードページ     932
 
            Console.WriteLine("それをUnicode(= UTF-16)に変更しておく。");
            Console.OutputEncoding = System.Text.Encoding.Unicode;                 // 以下ではUnicode文字のハイホンを使って
            Console.WriteLine("IANA Name: {0}", Console.OutputEncoding.WebName);   // utf-16   いるので、出力時の文字化け
            Console.WriteLine("Code Page: {0}", Console.OutputEncoding.CodePage);  // 1200       防止の為にUnicode指定しておく
 
 
 
            string[] addressArr =
            {
                "〒100-0014 東京都千代田区永田町1丁目7−1 ",
                "100ー0014  東京都千代田区永田町1丁目7−1  ",
                "100ー0014  東京都千代田区永田町1丁目7−1 \r\n  100ー0014  東京都千代田区永田町1丁目7−1", // 複数行のデータ
                "100ー00    東京都千代田区永田町1丁目7−1  ", // 郵便番号が7桁では無いのでpatternにマッチしないデータ
            };
 
            Console.WriteLine("\n⓪入力データ");
            foreach (var addr_i in addressArr)
            {
                Console.WriteLine("addr_i= ->{0}<-", addr_i);
            }
 
 
 
            var pattern = "〒?([0-90-9]{3})[-―ー-—\\s]([0-90-9]{4})\\s*(.*)"; // 半角と全角スペースも区切り文字に入れてみた。
            Console.WriteLine("\npattern: " + pattern);
 
            Console.WriteLine("\n①Regex.Replaceの中でMatchEvaluatorを使う。別の関数として");
            foreach (var addr_i in addressArr)
            {
                var rgx = new Regex(pattern);
 
                // Microsoft Developer Network のSystem.Text.RegularExpressionsのMatchEvaluator デリゲートの説明
                // https://msdn.microsoft.com/ja-jp/library/system.text.regularexpressions.matchevaluator%28v=vs.110%29.aspx?f=255&MSPPError=-2147217396
                // replace methodにMatchEvaluator delegateを設定する
                MatchEvaluator myEvaluator = new MatchEvaluator(MatchEvaluatorFunc_NumStrToHalf);
                var addr_i2_ = rgx.Replace(addr_i, myEvaluator);
 
                var addr_i2 = rgx.Replace(addr_i, MatchEvaluatorFunc_NumStrToHalf);
                Console.WriteLine("addr_i2= ->{0}<-", addr_i2);
            }
 
 
            Console.WriteLine("\n②Regex.Replaceの中でMatchEvaluatorを使う。インラインで");
            foreach (var addr_i in addressArr)
            {
                var rgx = new Regex(pattern);
                var addr_i2 = rgx.Replace(addr_i,
 
                        m => NumStr_FullToHalf(m.Groups[1].Value)
                            + "-"
                            + NumStr_FullToHalf(m.Groups[2].Value)
                            + " "
                            + m.Groups[3].Value.TrimEnd()
 
                    );
 
                Console.WriteLine("addr_i2= ->{0}<-", addr_i2);
            }
 
            Console.WriteLine("\n③Regex.Replaceの中でMatchEvaluatorを使う。デレゲート方式");
            foreach (var addr_i in addressArr)
            {
                var rgx = new Regex(pattern);
                var addr_i2 = rgx.Replace(addr_i,
 
                     delegate (Match m)
                     {
                         var postNum1 = m.Groups[1].Value;
                         var postNum2 = m.Groups[2].Value;
                         var postAddr = m.Groups[3].Value;
                         postNum1 = NumStr_FullToHalf(postNum1);
                         postNum2 = NumStr_FullToHalf(postNum2);
 
                         var postAddrNew = postNum1 + "-" + postNum2 + " " + postAddr.TrimEnd();
 
                         return postAddrNew;
                     });
 
                Console.WriteLine("addr_i2= ->{0}<-", addr_i2);
            }
        }
 
        static string MatchEvaluatorFunc_NumStrToHalf(Match m)
        {
            // Match m にはパターンにマッチした結果が来るので、
            // その中のデータを取り出して、自由に加工できる。
            // 関数の戻り値の文字列が、Replaceの置換結果となる。
 
            //                                  // m.Groups[0].Valueはマッチした全体の文字列
            var postNum1 = m.Groups[1].Value;   // ([0-90-9]{3})
            var postNum2 = m.Groups[2].Value;   // ([0-90-9]{4})
            var postAddr = m.Groups[3].Value;   // (.*)
            postNum1 = NumStr_FullToHalf(postNum1); // 郵便番号前半三桁を半角数字に変換
            postNum2 = NumStr_FullToHalf(postNum2); // 郵便番号後半四桁を半角数字に変換
 
            var postAddrNew = postNum1 + "-" + postNum2 + " " + postAddr.TrimEnd(); // 目的の住所文字列を生成
 
            return postAddrNew;
        }
 
 
        static string NumStr_FullToHalf(string strFull)
        {
            // strFullの中に全角数字0~9があればそれを半角数字に置換する関数
 
            string strHalf = "";
            for (int i = 0; i < strFull.Length; i++)
            {
                char cF = strFull[i];
                if ('0' <= cF && cF <= '9')
                {
                    var cHalf = (char)('0' + (cF - '0'));//.ToString();
                    strHalf += cHalf;
                }
                else
                {
                    strHalf += cF;
                }
            }
            return strHalf;
        }
 
 
        static string NumStr_FullToHalf1(string strFull)
        {
            Dictionary<char, char> dic = new Dictionary<char, char>()
            {
                {'0','0' },
                {'1','1' },
                {'2','2' },
                {'3','3' },
                {'4','4' },
                {'5','5' },
                {'6','6' },
                {'7','7' },
                {'8','8' },
                {'9','9' },
            };
 
            string strHalf = "";
            for (int i = 0; i < strFull.Length; i++)
            {
                char c0 = strFull[i];
                if (dic.ContainsKey(c0))
                {
                    char c1 = dic[c0];
                    strHalf += c1;
                }
                else
                {
                    strHalf += c0;
                }
            }
            return strHalf;
        }
     }
 }

 

上のソースで幾つかの補足

以下のURLのマイクロソフト社の説明を見ると、MatchEvaluatorを使う場合には、new MatchEvaluator() としてから使うようだ。

 // Microsoft Developer Network のSystem.Text.RegularExpressionsのMatchEvaluator デリゲートの説明
 // https://msdn.microsoft.com/ja-jp/library/system.text.regularexpressions.matchevaluator%28v=vs.110%29.aspx?f=255&MSPPError=-2147217396
 // replace methodにMatchEvaluator delegateを設定する
  MatchEvaluator myEvaluator = new MatchEvaluator(MatchEvaluatorFunc_NumStrToHalf);
  var addr_i2_ = rgx.Replace(addr_i, myEvaluator); 

でも、そうしなくても直接Replaceの第二引数に実行したい関数を書いたらMatchEvaluatorとして実行出来るようだ。これでいいのかな?まあ動いているからいいんだろうと思う。

 

次に、文字列中の全角数字を半角数字に置換する関数であるが、上のコードの末尾には使っていないが以下の関数がある。

static string NumStr_FullToHalf1(string strFull)

まあ、辞書(Dictionary)を使って文字を置き換えると言う安易な関数だ。

これでも動く事は動く。

まとめ

C#の正規表現 Regex.ReplaceでMatchEvaluatorと言うのを覚えたので使ってみた。

マッチした結果に対して自由に加工した上でそれらを置換結果に利用出来るので、文字列置換処理に於いて各種の応用が効く。

MatchEvaluatorを別関数としてい実行する方法と、ラムダ式の匿名メソッドとして実行する方法を覚えた。あるいはdelegateの匿名メソッドでも良い。

なお、今回は住所文字列の郵便番号の数字のみを処理したが、次回は何丁目何番何号の部分の数字を正規表現で取り出して処理してみたい。

例えば 

1丁目2-3
1-2-3
1-2-3
1丁目2の3

などバラバラの書式を

1丁目2番3号

のように統一するなど。

でも、これが意外に難しい。

と言うのは、住所文字列の場合には

100-0014 東京都千代田区永田町1丁目7−1

の形式だけでなくて、アパート名やマンション名が付く場合もある。

100-0014 東京都千代田区永田町1丁目7−1 ワレコマンション205号室

こんな感じ。

 

処理する入力データが名簿の住所録のような形式なら、一行に一個だけ住所データが入っているので処理し易い。

ところが、例えば新聞記事などの長文の文字列中に複数の住所が入っている場合には、正規表現だけではそれらを分離するのは難しいと思う。

住所の開始は郵便番号の数字や都道府県名で検出し易い。一方、住所の終わりを判定するのが困難なのだ。

人工知能など使うと出来ると思うのだが。

ワテもAIに挑戦するかな。

今ならオープンソースなAIのプログラムも有るようだし、そう言うのに学習させれば出来るのかな。

ちょっと調べてみるかな。

C#の本を読む

独習C#は有名だ。

この本は、アマゾンのレビューの評価も高い。

著者も有名な人ばかりだ。山田さんの名前もある。

でもワテは一冊も持っていない。

やっぱりこう言うちゃんとした教科書を読むべきだな。

スポンサーリンク
ワテ推薦のプログラミングスクールで学ぶ
コメント募集

この記事に関して何か質問とか補足など有りましたら、このページ下部にあるコメント欄からお知らせ下さい。

C#正規表現
スポンサーリンク
warekoをフォローする
スポンサーリンク
われこ われこ

コメント