sh1’s diary

プログラミング、読んだ本、資格試験、ゲームとか私を記録するところ

厄介なフォーマットの CSV ファイルを読み込む

古いソフトウェアが保存しているセーブデータファイルを開いてみると、CSV ファイルだった、なんてことが 2020 年の今でもあるようです。

場合によっては CSV 形式といっても、ときどきちょっと変わったフォーマットを採用していることがあります。

f:id:shikaku_sh:20200312144526p:plain
各行の数が一定ではない

厄介な CSV ファイル

普通の CSV ファイルは、最初に示される(または、省略される)ヘッダーの列定義に従って、行のデータが追加されていくため、基本的には各行のデータ数は一致します。

こんな感じが、よくある CSV ファイルのフォーマットかと思います。(マッピングできるファイル形式)

列1,列2,列3
aaa,bbb,ccc
111,222,333

ちょっと変わったフォーマットというのは、次のようにバラバラのフォーマットで、行ごとの要素数が一致せず、区切り文字だけが CSV っぽい状態で保存されている状態のものです。

SAMPLE
True,False,True,False,False,True,True,False,False,, 200 
 0 , 1 , 1 , 0 , 1 , 1 , 0 , 0 , 0 , 1 , 0 , 1 , 0 , 1 

 1 ,-1 ," ", 3,""
 2 , 21 ," ", 1,""
 3 , 33 ," ", 1,"BB,BB"
 4 , 44 ," ", 1,""
 5 , 55 ," ", 2,""
 6 , 66 ," ", 1,"A,A,A,A"

false, " ", 0 ,"",-1 , 0

セーブデータファイルに、設定情報なんかも全部まとめて1つのファイルに付与していると、こういうことに……。

CsvHelper」などのライブラリーの多くは、(できなくもないとしても)本来期待しているフォーマットとはいえないため、冗長なコーディングになってしまいます。

そんなわけで、CSV ファイルをシンプルに読み込むだけの機能を用意して、CSV の標準規格である「RFC 4180」のルールを守った(つもりの)CSV パーサーを書いてみました。

Qiita に「C# 用の CSV パーサーを書いた」というライセンスフリーCSV パーサーがあったのですが、後述のようになっています。

使い方

一行だけの CSV レコードをパースするときは、次の使い方で CSV レコードをフィールドに分解して IEnumerable<string> を返却します。

var fields = CsvParser.ParseFieldsFromText("aaa,bbb,ccc");

foreach (var field in fields)
{
    Console.WriteLine(field);
}
aaa
bbb
ccc

n行の CSV レコードをパースするときは、次の使い方で CSV レコードを分解します。

var fields = CsvParser.ParseFromText("aaa,bbb,ccc\r\n111,222,333");

foreach (var record in records)
{
    foreach (var field in record)
    {
        Console.WriteLine(field);
    }
}
aaa
bbb
ccc
111
222
333

フィールドのテキストがダブルクォート(")を囲まれているときは、次のように動作します。

var records = CsvParser.ParseFromText("aaa, bbb, ccc\r\n111,, 333\r\nAAA, \"BBB\"");

foreach (var record in records)
{
    Console.WriteLine($"record has {record.Count()} fields.");

    foreach (var field in record)
    {
        Console.WriteLine(field);
    }
}
record has 3 fields.
aaa
 bbb
 ccc
record has 3 fields.
111

 333
record has 2 fields.
AAA
BBB

省略表記、ダブルクォートの処理は次のとおり。

var records = CsvParser.ParseFromText("aaa,bbb,ccc\r\n111,222,\r\n,\"\"\"bbb\",");

foreach (var record in records)
{
    Console.WriteLine($"record has {record.Count()} fields.");

    foreach (var field in record)
    {
        Console.WriteLine(field);
    }
}
record has 3 fields.
aaa
bbb
ccc
record has 3 fields.
111
222

record has 3 fields.

"BBB

ファイルから読み込むときは、こんな感じに。読み込んだあとは、ParseFromText と同じです。

var path = "..."; // System.AppDomain.CurrentDomain.BaseDirectory;
var data = CsvParser.ParseFromFile(System.IO.Path.Combine(path, "sample.csv"));

foreach (var record in records)
{
    foreach (var field in record)
    {
        Console.WriteLine("do something...");
    }
}

f:id:shikaku_sh:20200313090139p:plain
読み込んだ CSV ファイルの内容
f:id:shikaku_sh:20200313090158p:plain
行数とか各行の要素数から、うまくいけてそう

こんな感じで無事に厄介なフォーマットの CSV ファイルも読み込めました。

テストによる検証

Wiki - Comma-Separated Values」と「RFC 4180」に CSV フォーマットのサンプルがあるのですが、このあたりのルールを守っている保証がある CSV パーサーがほしかった。

yutokun/CSV-Parser」は、惜しい完成度で、テストコードを走らせてみると一部うまく通りませんでした。(2020年 03 月 12 日時点)

たとえば、こんな感じです。

"日本CRLF国","""東京""","127,767,944"
[0]=日本CRLF国
[1]="京"
[2]=127,767,944

他にも 空データ, B"BB, 空データ だとこんな感じ。

"","b""bb",""
[0]=","bb
[1]="

そんなわけで、CSV パーサーを作ってみた感じです。WIKIRFC 4180 のサンプルテキストをコピーしただけですが、自作のパーステストは、すべてパスしています。

f:id:shikaku_sh:20200312144905p:plain
やらしい図ですね。すいません

サンプル

参考