C# CSVHelper读取CSV空字段报错 如何配置ShouldSkipRecord跳过异常
CsvHelper读取CSV空字段抛异常修复方案
现有代码不生效的核心问题
- 你定义的
CsvConfiguration实例没有传入CsvReader构造函数,所有自定义配置完全未生效,程序全程使用CsvHelper默认配置运行。 - 你在字段映射上加的
.Optional()作用是CSV表头不存在对应列时不触发缺失异常,无法处理行内字段为空、类型转换失败的场景。 CsvConfiguration不存在Encoding属性,你写在配置里的编码设置完全无效,编码需要在初始化StreamReader时指定。- 实体类中
int类型的LineNumber既没有配置映射规则,也没有标记忽略,CsvHelper默认会自动匹配同名列,空值/缺列时会触发类型转换异常。
正确配置方法(含ShouldSkipRecord用法)
1. 补全ClassMap配置
给不需要从CSV读取的字段加忽略标记,避免自动映射出错:
public class WheelProsAccessoriesInventoryItemMap : ClassMap<WheelProsAccessoriesInventoryItem> { public WheelProsAccessoriesInventoryItemMap() { Map(m => m.PartNumber).Name("PartNumber"); Map(m => m.Brand).Name("Brand"); Map(m => m.PartDescription).Name("PartDescription"); Map(m => m.CapHardwareDescription).Name("CapHardwareDescription").Optional(); Map(m => m.CapScrewQuantity).Name("CapScrewQuantity").Optional(); Map(m => m.CapWrench).Name("CapWrench").Optional(); Map(m => m.CapStyleDescription).Name("CapStyleDescription").Optional(); Map(m => m.ImageURL).Name("ImageURL"); Map(m => m.TotalQOH).Name("TotalQOH"); Map(m => m.Msrp).Name("MSRP"); Map(m => m.Map).Name("MAP"); Map(m => m.RunDate).Name("RunDate"); Map(m => m.StockLa1).Name("1011"); Map(m => m.StockLa2).Name("1031"); Map(m => m.StockLa3).Name("1085"); Map(m => m.StockTX).Name("1086"); Map(m => m.StockLangley).Name("4033"); Map(m => m.StockToronto).Name("4035"); Map(m => m.num1088).Name("1088"); Map(m => m.num1015).Name("1015").Optional(); Map(m => m.num1019).Name("1019").Optional(); Map(m => m.num1022).Name("1022").Optional(); Map(m => m.num1028).Name("1028").Optional(); Map(m => m.num1036).Name("1036").Optional(); Map(m => m.num1071).Name("1071").Optional(); Map(m => m.num1072).Name("1072").Optional(); // 不需要从CSV映射的字段直接标记忽略,避免自动匹配报错 Map(m => m.LineNumber).Ignore(); } }
2. 修正读取逻辑,正确传入配置+使用ShouldSkipRecord
ShouldSkipRecord委托返回true时会直接跳过当前行,你可以在这里自定义无效行判断规则(比如核心必填字段为空、行字段数和表头不匹配时直接跳过),同时把缺失字段、坏数据的异常回调配置好,避免抛异常中断读取:
public static IEnumerable<T> GetRecords<T, TMap>(string filename, bool hasFrenchChars = false) where T : class where TMap : ClassMap<T> { var errorRecsCollection = new List<string>(); // 先初始化正确的Csv配置 var config = new CsvConfiguration(CultureInfo.InvariantCulture) { Delimiter = ",", HasHeaderRecord = true, IgnoreBlankLines = true, // 缺字段时不抛异常 MissingFieldFound = null, // 读取到坏数据时记录日志,不抛异常 BadDataFound = context => { errorRecsCollection.Add(context.RawRecord); }, // 自定义跳过规则,返回true的行直接跳过 ShouldSkipRecord = context => { // 规则1:行字段数和表头列数不一致时跳过 if (context.Record.Length != context.Parser.Count) { errorRecsCollection.Add($"行{context.Row.Parser.Row}字段数不匹配:{string.Join(",", context.Record)}"); return true; } // 规则2:核心必填字段PartNumber为空时跳过 var partNumberIndex = context.Parser.GetFieldIndex("PartNumber"); if (partNumberIndex >=0 && string.IsNullOrWhiteSpace(context.Record[partNumberIndex])) { errorRecsCollection.Add($"行{context.Row.Parser.Row}PartNumber为空:{string.Join(",", context.Record)}"); return true; } return false; } }; // 编码在StreamReader初始化时指定,不要写在CsvConfiguration里 var encoding = hasFrenchChars ? Encoding.GetEncoding("ISO-8859-1") : Encoding.UTF8; using (var streamReader = new StreamReader(filename, encoding)) { // 把配置传入CsvReader构造函数,这步是之前配置不生效的核心原因 using (var csvReader = new CsvReader(streamReader, config)) { csvReader.Context.RegisterClassMap<TMap>(); return csvReader.GetRecords<T>().ToList(); } } }
额外注意事项
- 如果后续需要给
LineNumber赋值行号,直接在映射里用Convert配置即可,不需要让CsvHelper去CSV里匹配列:Map(m => m.LineNumber).Convert(row => row.Row.Parser.Row); - 所有非string类型的字段如果允许空值,一定要定义成可空类型(比如
int?、decimal?),否则空值转换时会触发类型转换异常。 - 读取大文件时不要直接用
.ToList()一次性加载到内存,可以逐行遍历GetRecords<T>()返回的迭代器,降低内存占用。
内容的提问来源于stack exchange,提问作者Lucas DJ
相关产品推荐
相关产品推荐

