1. 程式人生 > >【C#】CsvHelper 使用手冊

【C#】CsvHelper 使用手冊

[TOC] > **本文程式碼基於 CsvHelper 15.0.5** ## 簡介 CsvHelper 是一個用於讀寫 CSV 檔案的.NET庫。極其快速,靈活且易於使用。 CsvHelper 建立在.NET Standard 2.0 之上,幾乎可以在任何地方執行。 Github 地址:https://github.com/joshclose/csvhelper ### 模組 | 模組 | 功能 | | ---------------------------------- | ------------------------------------ | | CsvHelper | 讀寫 CSV 資料的核心類。 | | CsvHelper.Configuration | 配置 CsvHelper 讀寫行為的類。 | | CsvHelper.Configuration.Attributes | 配置 CsvHelper 的特性。 | | CsvHelper.Expressions | 生成 LINQ 表示式的類。 | | CsvHelper.TypeConversion | 將 CSV 欄位與 .NET 型別相互轉換的類。| ## 讀取 **測試類** ```csharp public class Foo { public int ID { get; set; } public string Name { get; set; } } ``` **csv 檔案資料** ``` ID,Name 1,Tom 2,Jerry ``` ### 讀取所有記錄 ```csharp using (var reader = new StreamReader("foo.csv")) { using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture)) { var records = csv.GetRecords(); } } ``` > 讀取 csv 檔案時,空行將被忽略,若空行中包含空格,將報錯。 > 如果是 Excel 編輯的 CSV 檔案,空行將會變成僅包含分隔符 `,` 的行,也會報錯。 ### 逐條讀取 ```csharp using (var reader = new StreamReader("foo.csv")) { using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture)) { while (csv.Read()) { var record = csv.GetRecord(); } } } ``` > `GetRecords` 方法通過 `yield` 返回一個 `IEnumerable`,並不會將內容一次全部讀進記憶體,除非呼叫了 `ToList` 或 `ToArray` 方法。所以這種逐條讀取的寫法沒有太多必要。 ### 讀取單個欄位 ```csharp using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture)) { csv.Read(); csv.ReadHeader(); while (csv.Read()) { var id = csv.GetField(0); var name = csv.GetField("Name"); } } ``` 逐行讀取時,可以不管標題行,但是,這裡不行。 `csv.Read();` 這句是讀取標題,如果沒有的話,`while` 迴圈第一次取到的是標題,肯定會報錯。 `csv.ReadHeader();` 這句是給標題賦值,如果沒有的話,`csv.GetField("Name")` 會報找不到標題。 使用 `TryGetField` 可以防止意外的報錯。 ```csharp csv.TryGetField(0, out int id); ``` ## 寫入 ### 寫入所有記錄 ```csharp var records = new List { new Foo { ID = 1, Name = "Tom" }, new Foo { ID = 2, Name = "Jerry" }, }; using (var writer = new StreamWriter("foo.csv")) { using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture)) { csv.WriteRecords(records); } } ``` ### 逐條寫入 ```csharp using (var writer = new StreamWriter("foo.csv")) { using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture)) { foreach (var record in records) { csv.WriteRecord(record); } } } ``` ### 逐欄位寫入 ```csharp using (var writer = new StreamWriter("foo.csv")) { using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture)) { csv.WriteHeader(); csv.NextRecord(); foreach (var record in records) { csv.WriteField(record.ID); csv.WriteField(record.Name); csv.NextRecord(); } } } ``` ## 特性 ### Index `Index` 特性用於標記欄位順序。 在讀取檔案時,如果沒有標題,就只能通過順序來確定欄位。 ```csharp public class Foo { [Index(0)] public int ID { get; set; } [Index(1)] public string Name { get; set; } } using (var reader = new StreamReader("foo.csv")) { using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture)) { csv.Configuration.HasHeaderRecord = false; var records = csv.GetRecords().ToList(); } } ``` > `csv.Configuration.HasHeaderRecord = false` 配置告知 `CsvReader` 沒有標題。必須要加這一行,否則會預設第一行為標題而跳過,導致最後的結果中少了一行。如果資料量比較多,會很難發現這個 bug。 在寫入檔案的時候,會按 `Index` 順序寫入。如果不想寫入標題,也需要新增 `csv.Configuration.HasHeaderRecord = false;` ### Name 如果欄位名稱和列名不一致,可以使用 `Name` 屬性。 ```csharp public class Foo { [Name("id")] public int ID { get; set; } [Name("name")] public string Name { get; set; } } ``` ### NameIndex `NameIndex` 用於處理 CSV 檔案中的同名列。 ```csharp public class Foo { ... [Name("Name")] [NameIndex(0)] public string FirstName { get; set; } [Name("Name")] [NameIndex(1)] public string LastName { get; set; } } ``` ### Ignore 忽略欄位 ### Optional 讀取時如果找不到匹配的欄位,則忽略。 ```csharp public class Foo { ... [Optional] public string Remarks { get; set; } } ``` ### Default 當讀取的欄位為空時 `Default` 特性可為其指定預設值。 `Default` 特性僅在讀取時有效,寫入時是不會將空值替換為預設值寫入的。 ### NullValues ```csharp public class Foo { ... [NullValues("None", "none", "Null", "null")] public string None { get; set; } } ``` 讀取檔案時,若 CSV 檔案中某欄位的值為空,那麼讀取後的值是 `""`,而非 `null`,標記 `NullValues` 特性後,若 CSV 檔案中的某欄位值為 `NullValues` 指定的值,則讀取後為 `null`。 若同時標記了 `Default` 特性,則此特性不起作用。 > 坑爹的是,在寫入檔案時,此特性並不起作用。因此會引起讀寫不一致的問題。 ### Constant `Constant` 特性為欄位指定一個常量值,讀寫時都使用此值,無論指定了什麼其他對映或配置。 ### Format `Format` 指定型別轉換時使用的字串格式。 例如數字和時間型別,我們經常會指定其格式。 ```csharp public class Foo { ... [Format("0.00")] public decimal Amount { get; set; } [Format("yyyy-MM-dd HH:mm:ss")] public DateTime JoinTime { get; set; } } ``` ### BooleanTrueValues 和 BooleanFalseValues 這兩個特性用於將 bool 轉換成指定的形式顯示。 ```csharp public class Foo { ... [BooleanTrueValues("yes")] [BooleanFalseValues("no")] public bool Vip { get; set; } } ``` ### NumberStyles ```csharp public class Foo { ... [Format("X2")] [NumberStyles(NumberStyles.HexNumber)] public int Data { get; set; } } ``` 比較有用是 `NumberStyles.HexNumber` 和 `NumberStyles.AllowHexSpecifier`,這兩個列舉的作用差不多。此特性僅在讀取時有效,寫入時並不會轉成 16 進位制寫入。這會導致讀寫不一致,可以用 `Format` 特性指定寫入格式。 ## 對映 如果無法給要對映的類新增特性,在這種情況下,可以使用 `ClassMap` 方式進行對映。 使用對映和使用特性效果是一樣的,坑爹的地方也一樣坑爹。以下示例用屬性實現了上面特性的功能。 ```csharp public class Foo2 { public int ID { get; set; } public string Name { get; set; } public decimal Amount { get; set; } public DateTime JoinTime { get; set; } public string Msg { get; set; } public string Msg2 { get; set; } public bool Vip { get; set; } public string Remarks { get; set; } public string None { get; set; } public int Data { get; set; } } public class Foo2Map : ClassMap { public Foo2Map() { Map(m => m.ID).Index(0).Name("id"); Map(m => m.Name).Index(1).Name("name"); Map(m => m.Amount).TypeConverterOption.Format("0.00"); Map(m => m.JoinTime).TypeConverterOption.Format("yyyy-MM-dd HH:mm:ss"); Map(m => m.Msg).Default("Hello"); Map(m => m.Msg2).Ignore(); Map(m => m.Vip) .TypeConverterOption.BooleanValues(true, true, new string[] { "yes" }) .TypeConverterOption.BooleanValues(false, true, new string[] { "no" }); Map(m => m.Remarks).Optional(); Map(m => m.None).TypeConverterOption.NullValues("None", "none", "Null", "null"); Map(m => m.Data) .TypeConverterOption.NumberStyles(NumberStyles.HexNumber) .TypeConverterOption.Format("X2"); } } ``` 在使用對映前,需要先註冊 ```csharp csv.Configuration.RegisterClassMap(); ``` ### ConvertUsing `ConvertUsing` 允許使用一個委託方法實現型別轉換。 ```csharp // 常數 Map(m => m.Constant).ConvertUsing(row => 3); // 把兩列聚合在一起 Map(m => m.Name).ConvertUsing(row => $"{row.GetField("FirstName")} {row.GetField("LastName")}"); Map(m => m.Names).ConvertUsing(row => new List { row.GetField("Name") } ); ``` ## 配置 ### Delimiter 分隔符 ```csharp csv.Configuration.Delimiter = ","; ``` ### HasHeaderRecord 此配置前文已經提到過,是否將第一行作為標題 ```csharp csv.Configuration.HasHeaderRecord = false; ``` ### IgnoreBlankLines 是否忽略空行,預設 `true` ```csharp csv.Configuration.IgnoreBlankLines = false; ``` 無法忽略一個僅包含空格或 `,` 的行。 ### AllowComments 是否允許註釋,註釋以 `#` 開頭。 ```csharp csv.Configuration.AllowComments = true; ``` ### Comment 獲取或設定用於表示註釋掉的行的字元。預設是 `#`。 ```csharp csv.Configuration.Comment = '/'; ``` ### BadDataFound 設定一個函式,該函式會在資料不正確時觸發,可用於記錄日誌。 ### IgnoreQuotes 獲取或設定一個值,該值指示在解析時是否應忽略引號並將其與其他任何字元一樣對待。 預設是 `false`,如果字串中有引號,必須是 3 個 `"` 連在一起,讀取到的字串中才會有一個 `"`,如果是 1 個則忽略,2 個則報錯。 如果為 `true`,則會將 `"` 當做字串原樣返回。 ```csharp csv.Configuration.IgnoreQuotes = true; ``` `CsvWriter` 中是沒有這個屬性的,一旦字串中包含 `"`,寫出來就是 3 個 `"` 連在一起。 ### TrimOptions 去除欄位首尾空格 ```csharp csv.Configuration.TrimOptions = TrimOptions.Trim; ``` ### PrepareHeaderForMatch `PrepareHeaderForMatch` 定義了屬性名稱與標題進行匹配的函式。標題和屬性名稱均通過該函式執行。此功能可用於刪除標題中的空格,或者當標題和屬性名稱大小寫不一致時統一大小寫後比較。 ```csharp csv.Configuration.PrepareHeaderForMatch = (string header, int index) => header.ToLowe