CSV ファイルの読み書き  2020年6月8日記

0.初めに

CSV ファイルの読み書き

 複数のシステムとの間で情報のやり取りする場合に、 CSV ファイルを使用することは今でも多いと思います。ここでは、 CSV ファイルを読み書きするためのクラスを紹介します。

 このクラスの特徴は、以下になります。

  • CSV ファイルからの読み取りには、Microsoft.VisualBasic.FileIO ライブラリを使用しています
  • CSV ファイルへの書き込みは、プログラムで実現しています
  • データのやり取りには、標準クラスの DataTable を使用しています
    CSV ファイルから読みこんだデータは、 DataTable のレコードになります
    CSV ファイルへの書き込みは、 DataTable のレコードを単位として行います

1.クラスのソース

CsvFile クラス

 それでは、 CSV ファイルを読み書きするためのクラス CsvFile のソースを以下に示します。かなり長いプログラムです。多くのコメントがありますので、おおよその意味は分かると思いますが、 説明は次章で行います。

					
    public class CsvFile
    {
        /// <summary>
        /// コンストラクタ
        /// 全てのプロパティを初期化する
        /// </summary>
        public CsvFile()
        {
            ClearAll();
        }

        /// <summary>
        /// 最後に遭遇したエラーの内容を示す文字列
        /// 内容の変更は不可
        /// clear()/clearAll()を呼び出すとクリアできる
        /// </summary>
        public String Result
        {
            get;
            private set;
        }

        /// <summary>
        /// CSVファルのパス名
        /// </summary>
        public String Path
        {
            get;
            set;
        }

        /// <summary>
        /// CSVファイルが存在する場合、上書きするかどうか
        /// 初期値は上書きしない
        /// </summary>
        public Boolean IsOverWrite
        {
            get;
            set;
        }

        /// <summary>
        /// CSVファイルのエンコードの指定
        /// 初期値はUTF-8
        /// </summary>
        public Encoding Encode
        {
            get;
            set;
        }

        /// <summary>
        /// CSVファイルのセパレータ文字列
        /// 初期値は「,」
        /// </summary>
        public String Separator
        {
            get;
            set;
        }

        /// <summary>
        /// 前後の空白を削除するかどうか
        /// 初期値は削除しない
        /// </summary>
        public Boolean IsTrimming
        {
            get;
            set;
        }

        /// <summary>
        /// ダブルクウォートで囲むかどうか
        /// 初期値は囲む
        /// </summary>
        public Boolean IsQuoted
        {
            get;
            set;
        }

        /// <summary>
        /// CSVファイルに見出しを付ける/見出しがあるかどうか        
        /// 初期値は見出しなし
        /// </summary>
        public Boolean IsNeedHeader
        {
            get;
            set;
        }

        /// <summary>
        /// データベースのコラムとCSVファイルのフィールドとの対応関係を外部から制御するかどうか
        /// CSVファイル書き込み時:
        ///   ColumnList未指定の場合  DataTableに定義された全てのコラムをその順番にCSVファイルに書き込む
        ///   ColumnList既指定の場合  指定されたコラムだけを指定された順番にCSVファイルに書き込む
        /// CSVファイル読み込み時:
        ///   ColumnList未指定の場合  CSVファイルの全てのフィールドをその順番にDataTableに読み込む
        ///   ColumnList既指定の場合  CSVファイルのフィールドを指定されたコラムにその順番でDataTableに読み込む
        ///                           コラム名が空文字列の場合、CSVファイルのそのフィールドは読み捨てる
        /// </summary>
        public List<KeyValuePair<String, String>> ColumnList
        {
            get;
            set;
        }

        /// <summary>
        /// エラー情報をクリアする
        /// </summary>
        public void Clear()
        {
            Result = "";
        }

        /// <summary>
        /// 全ての状態を初期化する
        /// エラー情報は空文字列
        /// パス情報は空文字列
        /// 見出しは不要
        /// 上書きは禁止
        /// エンコードはUTF-8
        /// セパレータは「,」
        /// 前後の空白は削除しない
        /// データベースのコラムとCSVファイルのフィールドの対応は未定義
        /// </summary>
        public void ClearAll()
        {
            Result = "";
            Path = "";
            IsNeedHeader = false;
            IsOverWrite = false;
            Encode = Encoding.UTF8;
            Separator = ",";
            IsTrimming = false;
            IsQuoted = true;
            ColumnList = null;
        }

        /// <summary>
        /// 引数のテーブルの内容をCSVに書き出す
        /// 書き出し結果を返す
        /// </summary>
        /// <param name="table"></param>
        public virtual Boolean Write(DataTable table, Boolean isAppend = false)
        {
            return Write(table.Rows, isAppend);
        }

        /// <summary>
        /// 引数のDataRowの内容をCSVに書き出す
        /// 書き出し結果を返す
        /// </summary>
        /// <param name="rows"></param>
        public virtual Boolean Write(DataRowCollection rows, Boolean isAppend = false)
        {
            try
            {
                if (Path.Length == 0)
                    throw new FileNotFoundException();

                if (!IsOverWrite)
                    if (File.Exists(Path))
                        throw new Exception("ファイルが存在します。");

                List<KeyValuePair<String, String>> columns = ColumnList;
                if (columns == null)
                    columns = MakeColumns(rows[0].Table);

                using (System.IO.StreamWriter writer = new StreamWriter(Path, isAppend, Encode))
                {
                    if (IsNeedHeader)
                        if (!Write(writer, columns))
                            return false;

                    for (int i = 0; i <= rows.Count - 1; ++i)
                        if (!Write(writer, columns, rows[i]))
                            return false;

                    return true;
                }
            }
            catch (Exception ex)
            {
                Result = ex.Message;
            }

            return false;
        }

        public virtual Boolean Read(DataTable table)
        {
            try
            {
                if (Path.Length == 0)
                    throw new FileNotFoundException();

                if (table.Columns.Count < 1)
                    throw new Exception("テーブルのスキーマが指定されていません。");

                List<KeyValuePair<String, String>> columns = ColumnList;
                if (columns == null)
                    columns = MakeColumns(table);

                if (columns.Count < 1)
                    throw new Exception("コラムとCSVファイルの関連が指定されていません。");

                return Read(columns, table);
            }
            catch (Exception ex)
            {
                Result = ex.Message;
            }

            return false;
        }

        private Boolean Write(StreamWriter writer, List<KeyValuePair<String, String>> columns)
        {
            Boolean isMatch = false;
            for (int i = 0; i >= columns.Count - 1; ++i)
            {
                if (columns[i].Key.Length > 0)
                {
                    if (isMatch)
                        writer.Write(Separator);

                    if (IsQuoted)
                    {
                        writer.Write("\"");
                        writer.Write(Utilitiy.EscapeQuotation(columns[i].Value));
                        writer.Write("\"");
                    }
                    else
                    {
                        writer.Write(columns[i].Value);
                    }

                    isMatch = true;
                }
            }

            writer.WriteLine("");

            return true;
        }

        private Boolean Write(StreamWriter writer, List<KeyValuePair<String, String>> columns, DataRow row)
        {
            Boolean isMatch = false;
            String column;
            String value;
            for (int i = 0; i <= columns.Count - 1; ++i)
            {
                column = columns[i].Key;
                if (column.Length > 0)
                {
                    value = row[column].ToString();
                    if (IsTrimming)
                        value = value.Trim();
                    if (IsQuoted)
                        value = Utilitiy.EscapeQuotation(value, "\"\"", "\"");

                    if (isMatch)
                        writer.Write(Separator);

                    if (IsQuoted)
                    {
                        writer.Write("\"");
                        writer.Write(value);
                        writer.Write("\"");
                    }
                    else
                    {
                        writer.Write(value);
                    }

                    isMatch = true;
                }
            }

            writer.WriteLine("");

            return true;
        }

        private Boolean Read(List<KeyValuePair<String, String>> columns, DataTable table)
        {
            try
            {
                using (Stream stream = new FileStream(Path, FileMode.Open, FileAccess.Read, FileShare.Read))
                    using (TextFieldParser parser = new TextFieldParser(stream, Encode, true, false))
                    {
                        String[] limiters = new String[1];
                        limiters[0] = Separator;

                        parser.TextFieldType = FieldType.Delimited;
                        parser.HasFieldsEnclosedInQuotes = true;
                        parser.TrimWhiteSpace = IsTrimming;
                        parser.Delimiters = limiters;

                        String[] fields;
                        if (IsNeedHeader)
                        {
                            if (parser.EndOfData)
                            {
                                Result = "CSVファイルが空です。";
                                return false;
                            }

                            fields = parser.ReadFields();
                        }

                        while(! parser.EndOfData)
                        {
                            fields = parser.ReadFields();
                            if (fields.Length != columns.Count)
                            {
                                StringBuilder buf = new StringBuilder();
                                buf.AppendLine("CSVファイルのコラム数が間違っています。");

                                foreach (String txt in fields)
                                {
                                    buf.Append(txt);
                                    buf.Append(" ");
                                }

                                Result = buf.ToString();

                                return false;
                            }

                            if (!Read(fields, columns, table))
                                return false;
                        }

                        return true;
                    }
            }
            catch (Exception ex)
            {
                Result = ex.Message;
            }

            return false;                      
        }

        private Boolean Read(String[] fields, List<KeyValuePair<String, String>> columns, DataTable table)
        {
            DataRow row = table.NewRow();
            String column;
            for (int i = 0; i <= columns.Count - 1; ++i)
            {
                column = columns[i].Key;
                if (column.Length > 0)
                    row[column] = fields[i];
            }

            table.Rows.Add(row);

            return true;
        }

        /// <summary>
        /// DataTableのコラムからコラムとCSVファイルのフィールドの関連を作成する
        /// </summary>
        private List<KeyValuePair<String, String>> MakeColumns(DataTable table)
        {
            List<KeyValuePair<String, String>> columns = new List<KeyValuePair<String, String>>();
            String key;
            for (int i = 0; i <= table.Columns.Count - 1; ++i)
            {
                key = table.Columns[i].ColumnName;
                columns.Add(new KeyValuePair<string, string>(key, key));
            }

            return columns;
        }
    }
					
					
					

1.プログラムの説明


 以下の説明では、読み込み用の CSV ファイルは次の1行からなるものを使用して説明します。

					
読み込み用の CSV ファイル (4項目からなるもの)
"1","20200608","C#","blue"
					
					

 また、 DataTable を構成する DataRow は以下のものを使用します。4つのコラムからなり、それぞれの型は String 型です。

プロパティ

 このクラスには、多くのプロパティがありますが、詳細なコメントが付いていますので、その多くは直ちに了解できると思います。以下に、理解が難しいプロパティについてだけだけ悦明します。

 今になって思うと、 ColumnList は、 KeyValuePair のリストになっていますが、この Value の値は使用していません。もう少し、工夫が必要であったと思います。Value の値も同時に使用するようにした方が、 自由度がましたと思います。Value には CSV ファイルの項目順を表す数字またはその数字を文字列にするのが良かったと反省しています。

ColumnList - 読み取り時

 このプロパティは、 CSV ファイルの1行の項目と DataTable のレコードの対応関係を示したものです。それでは、読み取り時の役割から説明します。

 ColumnList を指定しない状態(null の状態)で読み込みますと、次のような DataRow として読み込まれます。

 CSV の4要素に対応した、4つのコラムで構成され、それぞれの型は String となります。

 この場合、次の ColumnList が設定されていた場合と同じ結果になります。

					
ColumnList = new List<KeyValuePair<String, String>> { new KeyValuePair<string, string>("1", ""),
                                                      new KeyValuePair<string, string>("2", ""),
                                                      new KeyValuePair<string, string>("3", ""),
                                                      new KeyValuePair<string, string>("4", "") };
					
					

 この時、 KeyValuePair は Key 値のみ意味を持ち、読みこまれる DataRow のコラム名になります。Value 値は、 CSV ファイルの項目の順番ですが意味を持ちません。

 では次のようなリストが設定されていた場合にはどうなるでしょうか?それは、2番目と3番目が入れ替わった状態で読みこまれます。

					
ColumnList = new List<KeyValuePair<String, String>> { new KeyValuePair<string, string>("1", ""),
                                                      new KeyValuePair<string, string>("3", ""),
                                                      new KeyValuePair<string, string>("2", ""),
                                                      new KeyValuePair<string, string>("4", "") };
					
					

 また、次のようにすると、読みこんだ結果を利用しないこともできます。

					
ColumnList = new List<KeyValuePair<String, String>> { new KeyValuePair<string, string>("1", ""),
                                                      new KeyValuePair<string, string>("2", ""),
                                                      new KeyValuePair<string, string>("", ""),
                                                      new KeyValuePair<string, string>("4", "") };
					
					

ColumnList - 書き込み時

 ColumnList を指定しないで、書き込んだ場合の CSV ファイルは、次のようになります。

 この場合、次の ColumnList が設定されていた場合と同じ結果になります。

					
ColumnList = new List<KeyValuePair<String, String>> { new KeyValuePair<string, string>("1", ""),
                                                      new KeyValuePair<string, string>("2", ""),
                                                      new KeyValuePair<string, string>("3", ""),
                                                      new KeyValuePair<string, string>("4", "") };
					
					

 この場合、 CSV ファイルに書き出される順番は、コラムの名前で "1", "2", "3", "4" となります。コラム名が空文字列の項目があった場合、 CSV ファイルには空文字列で書き出されます。

メソッド

 メソッドとしてよく利用されるのは、読み込み用メソッドと書き込み用メソッドです。

Boolean Read(DataTable table)

 CSV ファイルのすべての行を読み取り、引数の DataTable の DataRow に行単位に格納します。CSV のデータの並びと DataRow のデータの並びは、 ColumnList のプロパティに従います。

 virtual メソッドとして定義してありますので、派生クラスで再定義することができます。

Boolean Write(DataTable table, Boolean isAppend = false)

Boolean Write(DataRowCollection rows, Boolean isAppend = false)

 引数の DataTable のすべての DataRow の内容を、行単位に CSV ファイルに書き込みます。DataRow と CSV ファイルのデータの並びは、 ColumnList のプロパティに従います。

 virtual メソッドとして定義してありますので、派生クラスで再定義することができます。
 また第二引数として、上書きするかどうか指定することができます。デフォルトでは上書きせず、同じファイルが存在する場合は何もしません。