【CSV読み込み】TextFieldParserではなく、CsvParserを使用する


2020/02/19追記

CsvParserを使用せずとも、CsvReaderで可能でした。
CSVの解説と各プログラミング言語での実装例

CSVパース時の懸念点

CSVをパースする際、単純に1行ずつ読み込んで、カンマ「,」でSplitするだけで事足りるようなデータであればいいのですが、実際はそうともいかないので、以下のような考慮が必要になるかと思います。

※前提 区切り文字:カンマ「,」
    囲み文字:ダブルクオーテーション「"」
    各行の列数は同じ

  • データ中にカンマが存在する(区切り文字に指定した文字がある)
"a","b","c,d,e"
  • データ中にダブルクォーテーションが存在する(ダブルクォーテーションを二重化してエスケープされている)
"a","""b","c"
  • データ中に改行が存在する
"a","b
ccc","d"
  • 囲んだパターンと囲まないパターンが混在
"a",b,"c,d,e"

他には、データ中にタブ「\t」が存在する等・・・

まずTextFieldParserを使用してみましたが・・・

こういったデータを考慮しながら自前でパースしていくのはなかなか大変なので、というか私は考えることすらやめたので・・・
(いつか挑戦したい)
便利なクラスがないか昔ネットで探したところ、TextFieldParserが良さげのようでした。
TextFiledParser

.Netのクラスなので、他のオープンソースのパーサーと比べて、動作に信頼が持て、コンプライアンス的にみても良いと思っていました。

実際に上記データを読み込んでみると綺麗にパースできます。

使用するCSV

"ヘッダー1","ヘッダー2","ヘッダー3"
"a","b","c,d,e"
"a","""b","c"
"a","b
ccc","d"
"a",b,"c,d,e"

コード

using Microsoft.VisualBasic.FileIO;
using System;
using System.Linq;
using System.Text;

namespace TestProject.CSVParseTest
{
    public class CSVParseTest
    {
        public void ReadCsv()
        {
            using (var parser = new TextFieldParser(@".\test.csv", Encoding.UTF8))
            {
                // 区切り文字
                parser.Delimiters = new string[] { "," };

                // 囲み文字あり
                parser.HasFieldsEnclosedInQuotes = true;

                while (!parser.EndOfData)
                {
                    var values = parser.ReadFields();
                    values.ToList().ForEach(v =>
                    {
                        // わかりやすいようにパイプを入れる
                        Console.Write(v + "|");
                    });

                    // わかりやすいように改行
                    Console.WriteLine();
                }
            }
        }
    }
}
using System;

namespace TestProject.CSVParseTest
{
    public class Program
    {
        public static void Main(string[] args)
        {
            try
            {
                var pTest = new CSVParseTest();
                pTest.ReadCsv();
            }
            catch(Exception err)
            {
                Console.WriteLine(err.Message);
            }
            finally
            {
                Console.Read();
            }
        }
    }
}

上記の結果に満足していたのですが、次のようなデータの場合に、意図しない動きとなってしまいました。

使用するCSV ※データ中に空の改行が入る

"ヘッダー1","ヘッダー2","ヘッダー3"
"a","b","c,d,e"
"a","""b","c"
"a","b

gggg

ffff

ccc","d"
"a",b,"c,d,e"

結果

データ中であっても、空行が削除されてしまうようです。
掲示板などの備考欄等のテキストには、よく空行を入れてみた目を整えることがあるかとおもいます。
そういったデータを扱えなくなるのは困るので不採用としました。

CsvHelperのCsvParserクラスを使用する

他をあたることになり、人気のCsvHelperを検討していました。
CsvHelper

ただ、これを利用する場合、CSVのフォーマットに沿ったエンティティのクラスを用意する必要があり、
CSVの列数が後から変更になったりすると、都度プログラムソースも修正しなければならないと思ったので躊躇していました。

TextFiledParserのように1行分の文字を区切り文字でsplitして配列で返してくれたらなぁ・・・
かつ、空行も再現してくれたらなぁ・・・
なんて思っていたら、CsvHelper.CsvParserのReadメソッドがそれでした。

using CsvHelper;
using System;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;

namespace TestProject.CSVParseTest
{
    public class CSVParseTest2
    {
        public void ReadCsv()
        {
            using(var stream = new StreamReader(@".\test.csv", Encoding.UTF8))
            using (var parser = new CsvParser(stream, CultureInfo.InvariantCulture))
            {
                string[] line;
                while ((line = parser.Read()) != null)
                {
                    line.ToList().ForEach(v =>
                    {
                        // わかりやすいようにパイプを入れる
                        Console.Write(v + "|");
                    });

                    // わかりやすいように改行
                    Console.WriteLine();
                }
            }
        }
    }
}

「TextFieldParser + 空行が削除されない」が実現できました。

おわりに

CsvHelperの中はおそらく1文字ずつ処理して、パースしているんだと思います。
いつか中身を見て勉強してみたいです。

誤認や、他の方法で解決できる等、ありましたら指摘いただけるとありがたいです。