ラベル Microsoft.VisualBasic.FileIO の投稿を表示しています。 すべての投稿を表示
ラベル Microsoft.VisualBasic.FileIO の投稿を表示しています。 すべての投稿を表示

2018年3月13日火曜日

CSVファイルの処理方法について

目標としているプログラムでは、データはCSV形式のファイルで渡されることを前提にしているので、CSVファイルを読み込んで、CSVデータの解釈をして、分解したデータを適切な変数に格納する、という動作が必要となります。

CSVとは、comma-separated valuesの略で、いくつかのフィールド(項目)を区切り文字であるカンマで区切ったテキストデータおよびテキストファイルのことです。(Wikipediaより)
、読んだテキストを分解する、ということができれば、あとはデータ変換をして変数に格納、とう流れに持って行けると思います。

このような処理は、たとえばawkだとsplitを使って文字列分割をするとか、perlだと正規表現を使って数値を拾うといった処理を使うと簡単です。たとえば,
0,200,-100,8
0,195,-100,8
0,190,-100,8
0,185,-100,8
0,180,-100,8
0,175,-100,8
0,170,-100,8
0,165,-100,8
0,160,-100,8
0,155,-100,8
といったCSVファイルがある場合、perlの場合だと

/^(-?\d+\.?\d*),(-?\d+\.?\d*),(-?\d+\.?\d*),(-?\d+\.?\d*)/

で括弧内の正規表現を入力の数値にマッチさせてあげると、変数$1、$2、$3、$4にそれぞれ順番に各フィールドの数値が入ります。

#/usr/local/bin/perl
$cnt = 0;
while(<>) {
    if( /^(-?\d+\.?\d*),(-?\d+\.?\d*),(-?\d+\.?\d*),(-?\d+\.?\d*)/ ) {
        printf( "%04d : %f : %f : %f : %f\n", $cnt++, $1, $2, $3, $4 );

    }

}

このスクリプトは、CSVファイルの各フィールドを拾い上げて、画面に出力してくれるものです。私が書いているもう一つのブログ、「半導体やってました」で紹介しています。

実は、後で調べたらC#にもsplitや正規表現によるMatchesなどというものが用意されているみたいです。これは後日研究してみることにしたいと思います。

さて、正規表現やsplitメソッドがあることを知らなかった私は、インターネットを検索して、面白いものを見つけました。それはTextFieldParserというもの。これは、Visual Basicが持っているCSVのパーサーを呼び出して、CSVを解釈しようというものです。

上に書いた正規表現によるフィールドの分割は、各フィールドの文字列がしっかりと正規表現にマッチしないとうまく動いてくれません。たとえば、上の正規表現の例では、コンマの前後にスペースがあったらマッチしません。

それに対して、CSVファイルというものは、意外に表記方法にバラツキがあって、そのバラツキが正規表現でカバーできる範囲を超えてしまうとうまく動かなくなってしまいます。それに対して、このTextFieldParserは、かなり広範囲にわたってCSVファイルを適切に解釈してくれるもののようです。フィールドの数に動作が左右されることもありません。

下記は、TextFieldParserを使ってCSVファイルの各行をフィールド分割し、

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Threading.Tasks;
using System.Collections;
using Microsoft.VisualBasic.FileIO;

namespace sample
{
    class Program
    {
        static void Main(string[] args)
        {
            string line = "";
            getDataFromCSV("c:\\users\\ぐすたふ\\検討用テストデータ.csv");

            Console.ReadKey();
        }

        static void getDataFromCSV(string file)
        {
            try
            {
                TextFieldParser parser =
                     new TextFieldParser(file, System.Text.Encoding.GetEncoding("Shift_JIS"));
                parser.TextFieldType = FieldType.Delimited;
                parser.SetDelimiters(","); // 区切り文字はコンマ
                parser.CommentTokens = new string[1] { "#" };
                int col = 0;

                while (!parser.EndOfData)
                {
                    col = 0;

                    string[] row = parser.ReadFields();
                    Console.Write("{0} : ", line);
                    // 配列rowの要素は読み込んだ行の各フィールドの値
                    foreach (string field in row)
                    {
                        ++col;
                        Console.Write("{0}:{1} | ", col, field);
                    }
                    ++line;
                    Console.WriteLine(" ");
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
            }
        }
    }
}

これは、区切り文字をコンマ、行頭が#の行はコメントであることをパーサーに指示して、CSVフィアルを解釈し、

行番号:フィールド番号:フィールドの中身 | フィールド番号:フィールドの中身 | ・・・

の形でデータをコンソールに出力します。

今回の主役であるTextFieldParserですが、解説のページを見るとわかるようにコンストラクタが8種類あります。引数の型や数に応じて異なるコンストラクタが動作するようになっています。

上記の例では、

TextFieldParser( string, Encoding )

を使用しています。stringは、入力ファイルの完全なパス、Encodingは文字エンコードで、上の例ではShift_JISを指定しています。

オブジェクトを生成したのち、

  • プロパティTextFieldTypeにFieldType.Delimitedを指定(可変長レコード)
  • プロパティSetDelimitersでフィールドの区切りを","に指定
  • プロパティCommentTalkenでコメント指定文字を"#"に指定

しています。FieldType.Delimitedは、名前空間Microsoft.VisualBasic.FileIOで定義された列挙体です。

そのあと、ReadField()メソッドでファイルから1行読み込んで解析を行い、配列rowにそれぞれのフィールドを代入して戻ってくる、という仕組みになっています。

なお、このTextFieldParserにはDisposeメソッドがあります。したがって、本来は、解析が終了したら、不要になったリソースを開放する必要があります。finallyでファイルをクローズしてDispose()を呼び出すか、usingを使うのが良いと思います。このことは、原稿を書いているときに気が付いたので、上記プログラムはusingを適用しないままのものを出しています。

usingで対応するとすれば、以下のようにすればよいのでしょうか。エラーなしでビルドできて、動作も問題なくできています。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Threading.Tasks;
using System.Collections;
using Microsoft.VisualBasic.FileIO;

namespace sample
{
    class Program
    {
        static void Main(string[] args)
        {
            string line = "";

            getDataFromCSV("c:\\users\\gustav\\検討用テストデータ.csv");

            Console.ReadKey();
        }

        static void getDataFromCSV(string file)
        {
            try
            {
                using (TextFieldParser parser = 
                    new TextFieldParser(file, System.Text.Encoding.GetEncoding("Shift_JIS")))
                {
                    parser.TextFieldType = FieldType.Delimited;
                    parser.SetDelimiters(","); // 区切り文字はコンマ
                    parser.CommentTokens = new string[1] { "#" };
                    int line = 0, col = 0;

                    while (!parser.EndOfData)
                    {
                        //++line;
                        col = 0;

                        string[] row = parser.ReadFields();
                        Console.Write("{0} : ", line);
                        // 配列rowの要素は読み込んだ行の各フィールdの値
                        foreach (string field in row)
                        {
                            ++col;
                            Console.Write("{0}:{1} | ", col, field);
                        }
                        ++line;
                        Console.WriteLine(" ");
                    }
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
            }
        }
    }
}

usingは書き方にクセがあって、あまり美しくないのが気に入りませんが、仕方ないでしょう。

今回はここまでにします。