Csv-CSharpという、CSVの解析やシリアライズ/デシリアライズを行うためのライブラリを公開しました!かなり前からプレビュー版として公開はしていましたが、ある程度安定してきたので正式リリースとすることにしました。

https://github.com/nuskey8/Csv-CSharp

既にC#にはCsvHelperSep等のライブラリがありますが、Csv-CSharpの最大の特徴はシリアライザとしてのAPIを整えてあることでしょう。

var array = new Person[]
{
    new() { Name = "Alice", Age = 18 },
    new() { Name = "Bob", Age = 23 },
    new() { Name = "Carol", Age = 31 },
}

string csv = CsvSerializer.SerializeToString(array);
array = CsvSerializer.DeserializePerson>(csv);

[CsvObject]
public partial class Person
{
    [Column(0)]
    public string Name { get; set; }

    [Column(1)]
    public int Age { get; set; }
}

System.Text.JsonやMessagePack-CSharpに近いAPIでCSVと配列を相互に変換することが可能です。もちろん対応する型を定義することなく、直接CSVの構造を解析する機能も提供されています。

また、高いパフォーマンスも特徴の一つです。Source Generatorを活用した設計により、非常に高いパフォーマンスを発揮するようになっています。CsvHelperやServiceStack.Textはもちろん、現状最速を掲げているSepともほぼ同等、Csv-CSharpに有利な状況であればより高速な解析が可能です。

Sepは良好なパフォーマンスの反面、APIにやや癖があったり、最新の.NETの機能に頼っているためUnityでは使えないなどの問題がありました。Csv-CSharpは.NET標準に沿った自然なAPIで構成されているほか、.NET Standard 2.1にも対応しているためUnityでも利用可能です。

さらに、Csv-CSharpはIBufferWriter/ReadOnlySequenceのようなモダンなI/O APIにも対応しています。これによりバッファ管理を外部に任せられる場合は更なる性能の向上が見込めます。

使い方

既に冒頭でも紹介した通り、CSVをデータの配列とみてSerialize()/Deserialize()するのが基本の使い方となります。最大のパフォーマンスを発揮したい場合はstringを通さずにUTF-8ベースのAPIを利用します。

byte[] csv = CsvSerializer.Serialize(array);
array = CsvSerializer.DeserializePerson>(csv);

また、対象の型にはpartialキーワードと[CsvObject]属性、メンバーが対応する[Column]属性が必須です。また、シリアライズの際に無視したいメンバーには[IgnoreMember]属性を追加してください。これが足りない場合はAnalyzerがコンパイルエラーを出力します。

[CsvObject]
public partial class Person
{
    [Column(0)]
    public string Name { get; set; }

    [Column(1)]
    int age;

    [IgnoreMember]
    public int Age => age;
}

[Column]の引数はintまたはstringが利用できます。intの場合は対象の列数が、stringの場合は同名のCSVのヘッダーが対応します。

また、プロパティ名をそのまま[Column]の引数として利用したい場合は[CsvObejct(keyAsPropertyName: true))]を指定することが可能です。

[CsvObject(keyAsPropertyName: true)]
public partial class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

この辺りの仕様はMessagePack-CSharpと全く同じになるようになっています。MessagePack-CSharpのAPIは本当に良く出来ているので…

CsvDocument

とはいえ、わざわざ対象の型を作るまでもないデータであったり、そもそもデータにキッチリ対応する型を作ることができないケースもあるでしょう。その場合はCsvDocumentで直接解析することが可能です。

var document = CsvSerializer.ConvertToDocument(csv);

foreach (var row in document.Rows)
{
    var name = row["Name"].GetValuestring>();
    var age = row["Age"].GetValueint>();
}

こちらも直感的で扱いやすいAPIになっているのではないでしょうか。通常のSerialize()/Deserialize()より若干パフォーマンスは落ちますが、それでも十分高速です。

カスタマイズ

シリアライズの際にCsvOptionsを渡すことで動作をカスタマイズすることが可能です。

CsvSerializer.Serialize(array, new CsvOptions()
{
    HasHeader = true, 
    AllowComments = true, 
    NewLine = NewLineType.LF, 
    Separator = SeparatorType.Comma, 
    QuoteMode = QuoteMode.Minimal, 
    FormatterProvider = StandardFormatterProvider.Instance, 
});

これにより#によるコメントを許可したり、Separatorを変更することでTSVなどの解析を行ったりなども可能になっています。

また、CSVの各要素のパースについてはICsvFormatterICsvFormatterProviderを差し替えることでカスタマイズできます。例として、intをラップする構造体のFormatterを作成してみましょう。

public struct Foo
{
    public int Value;

    public Foo(int value)
    {
        this.Value = value;
    }
}

public sealed class FooFormatter : ICsvFormatterFoo>
{
    public Foo Deserialize(ref CsvReader reader)
    {
        var value = reader.ReadInt32();
        return new Foo(value);
    }

    public void Serialize(ref CsvWriter writer, Foo value)
    {
        writer.WriteInt32(value.Value);
    }
}

このFormatterを利用するには、対応するFormatterProviderをセットで作成します。この例ではFormatterが1つだけですが、複数ある場合も単一のFormatterProviderにまとめることが可能です。

public class CustomFormatterProvider : ICsvFormatterProvider
{
    public static readonly ICsvFormatterProvider Instance = new CustomFormatterProvider();

    CustomFormatterProvider()
    {
    }

    static CustomFormatterProvider()
    {
        FormatterCacheFoo>.Formatter = new FooFormatter();
    }

    public ICsvFormatterT>? GetFormatterT>()
    {
        return FormatterCacheT>.Formatter;
    }

    static class FormatterCacheT>
    {
        public static readonly ICsvFormatterT> Formatter;
    }
}

あとはこれをCsvOptionsに渡してやればOKです。


var provider = CompositeFormatterProvider.Create(
    CustomFormatterProvider.Instance,
    StandardFormatterProvider.Instance
);

CsvSerializer.Serialize(array, new CsvOptions()
{
    FormatterProvider = provider
});

要するにMessagePack-CSharpのFormatter / Resolverですね。これもそのまんまです。

パフォーマンスのために柔軟性を若干犠牲にしてる部分はあるので、CsvHelperと比較すると若干カスタマイズの余地は少ないかもしれません。ただ、ほとんどのユースケースでは十分と言えるだけの機能は備わっているでしょう。

CSVの仕様

実はCSVも明文化された仕様は定められていて、RFC-4180で確認することができます。しかし、現実にはこの仕様に沿っていないCSVの方が多いでしょう。というかCsv-CSharpのデフォルトも厳密にはこの仕様に沿っていないですし。

というわけでCsv-CSharpではこの仕様から外れたCSVもある程度許容して解析できるようになっています。.csvとは名ばかりの魔改造CSVデータが投げられたらどうしようもないですが、まあそれはしょうがないということで…

まとめ

CSVの解析は比較的簡単にできるので、自前で実装している人も多いでしょう。ただ、どうしても自前でやっていると実装ミスが生じたり、データの変更に柔軟に対応できなかったりします。

また、C#の文字列周り、特にUTF-8の最適化は結構複雑で、都度適切に実装するのはかなり大変です。そのため、こういったガチガチに最適化された、それでいて手軽に扱えるライブラリを提供しておくことは重要でしょう。

というわけで、CSVを扱う際には是非使ってみてください〜!

フラッグシティパートナーズ海外不動産投資セミナー 【DMM FX】入金

Source link