講到 .NET 解析 CSV,我習慣用 ServiceStack.Text CsvSerializer 處理,功能與效能都沒啥問題,也就一直沿用至今。今天在 FB 看到相關討論,發現有個我錯過的 CSV 程式庫 - CsvHelper (感謝網友許智涵分享),保哥之前寫過文章分享(參考:使用 C# 與 CsvHelper 套件解析《臺北市政府行政機關辦公日曆表》公開資料 ),甚至當年 FB 貼文讀者 Tomex Ou 也曾推薦,不過機緣未到沒進我的工具箱。程式開發就是這樣,找到順手的程式庫一直用不必多想是好事,風險是會錯過其他更方便快速的選擇,挑選工具多少也靠緣分,感覺是該重新評估了,於是有了這篇。

關於 .NET CSV 解析工具,我查到一篇能讓選擇障礙患者一秒發病的好文章 - The fastest CSV parser in .NET,作者 Joel Verhagen 是微軟 NuGet 開發人員,文章蒐集了 39 種 CSV 解析程式庫 (39 種!!! 這是要逼死誰?),進行一百萬筆 CSV 大檔解析效能測試。

在這個測試中,具備平行處理能力的 RecordParser 以 0.826s 奪冠,CsvHelper 以 2.484 秒排第七, ServiceStack.Text 則為 4.139s 排名 18,CsvHelper 效能明顯勝過 ServiceStack.Text。

要留意該測試不包含欄位雙引號、欄位內含逗號或換行,並有大量重複欄位值(對實作 String Pooling 的程式庫有利),同時也不考慮記憶體耗用,強型別映對能力等,故此測試結果挺極端,可能與一般實務應用需求有所出入。

但作者有聲明,如果你只是需要一款功能完整的 CSV 程式庫,不在意幾 ms 的速度差異,那麼西瓜偎大邊,選最受歡迎的 CsvHelper 就對了,被廣泛使用且歷經各種情境考驗仍能存活,品質與方便性必然不差,並有範例與參考資料豐富的優點。

NuGet 下載量 CsvHelper 190.9M vs ServiceStack.Text 46.8M。

CsvHelper,就決定是你了!

thumbnail

CsvHelper 說明文件範例完整,不用擔心不會用,我打算將 ServiceStack.Text 範例改寫成 CsvHelper 版,檢測完自己最常用的情境就結束這回合。

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Dynamic;
using CsvHelper;
using System.Globalization;

namespace CsvTest
{
    class Program
    {
        public class Entity
        {
            public int Num { get; set; }
            public DateTime Date { get; set; }
            public string Text { get; set; }
            public bool Flag { get; set; }

            public Entity(int num, DateTime date, string text, bool flag)
            {
                Num = num;
                Date = date;
                Text = text;
                Flag = flag;
            }
            public Entity() { } // CsvHelper需要無參數建構式
        }

        static Entity[] TestData = new Entity[]
        {
            new Entity(1, new DateTime(2012,12,21), "Normal", true),
            new Entity(16, new DateTime(2012,12,21), "Taipei, Taiwan", true),
            new Entity(32, new DateTime(2012,12,21), $@"""雙引號""跟,都來一下
換行當然不可少
", false)
        };

        static void Main(string[] args)
        {
            Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
            Directory.CreateDirectory("output");
            ObjArrayToCsv();
            CsvToObjArray();
            ParseAsStrArray();
        }

        //物件陣列轉成 CSV
        static void ObjArrayToCsv()
        {
            using (var sw = new StreamWriter("output\\Test1.csv", false, Encoding.UTF8))
            {
                using (var csv = new CsvWriter(sw, CultureInfo.InvariantCulture))
                {
                    csv.WriteRecords(TestData);
                }
            }
        }
        // CSV 還原成物件陣列
        static void CsvToObjArray()
        {
            using var sr = new StreamReader("output\\Test1.csv", Encoding.UTF8);
            using var csv = new CsvReader(sr, CultureInfo.InvariantCulture);
            var data = csv.GetRecords<Entity>().ToList();
            Console.WriteLine(System.Text.Json.JsonSerializer.Serialize(data,
                new System.Text.Json.JsonSerializerOptions { 
                    WriteIndented = true,
                    Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
             }));
            Console.WriteLine();
        }
        // 不使用強型別,解析成 string 陣列
        static void ParseAsStrArray()
        {
            string[] propNames = null;
            List<string[]> rows = new List<string[]>();

            using var sr = new StreamReader("output\\Test1.csv", Encoding.UTF8);
            using var csv = new CsvReader(sr, CultureInfo.InvariantCulture);
            while (csv.Read())
            {
                string[] strArray = Enumerable.Range(0, csv.Parser.Count)
                    .Select(i => csv.GetField(i)).ToArray();
                if (propNames == null)
                    propNames = strArray;
                else
                    rows.Add(strArray);
            }
            Console.WriteLine($"PropNames={string.Join(",", propNames)}");
            for (int r = 0; r < rows.Count; r++)
            {
                var cells = rows[r];
                for (int c = 0; c < cells.Length; c++)
                {
                    Console.WriteLine($"[{r},{c}]={cells[c]}");
                }
            }
            Console.WriteLine();
        }

    }
}

輕鬆秒殺~

My reason for changing CSV library from ServiceStack.Text to CsvHelper.


Comments

Be the first to post a comment

Post a comment