c# - 将 CSV 标头与地图类进行比较
问题描述
我有一个过程,我们编写了一个类来使用 CsvHelper ( https://joshclose.github.io/CsvHelper ) 将大型 (ish) CSV 导入我们的应用程序。
我想将标头与 Map 进行比较以确保标头的完整性。我们从第 3 方获取 CSV 文件,我想确保它不会随着时间的推移而改变,并认为最好的方法是将其与地图进行比较。
我们有一个这样设置的类(修剪):
public class VisitExport
{
public int? Count { get; set; }
public string CustomerName { get; set; }
public string CustomerAddress { get; set; }
}
及其对应的地图(也修剪过):
public class VisitMap : ClassMap<VisitExport>
{
public VisitMap()
{
Map(m => m.Count).Name("Count");
Map(m => m.CustomerName).Name("Customer Name");
Map(m => m.CustomerAddress).Name("Customer Address");
}
}
这是我用于读取 CSV 文件的代码,效果很好。我有一个尝试捕获错误,但理想情况下,如果它专门针对标头未匹配而失败,我想专门处理它。
private void fileLoadedLink_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
{
try
{
var filePath = string.Empty;
data = new List<VisitExport>();
using (OpenFileDialog openFileDialog = new OpenFileDialog())
{
openFileDialog.InitialDirectory = new KnownFolder(KnownFolderType.Downloads).Path;
openFileDialog.Filter = "csv files (*.csv)|*.csv";
openFileDialog.FilterIndex = 2;
openFileDialog.RestoreDirectory = true;
if (openFileDialog.ShowDialog() == DialogResult.OK)
{
filePath = openFileDialog.FileName;
var fileStream = openFileDialog.OpenFile();
var culture = CultureInfo.GetCultureInfo("en-GB");
using (StreamReader reader = new StreamReader(fileStream))
using (var readCsv = new CsvReader(reader, culture))
{
var map = new VisitMap();
readCsv.Context.RegisterClassMap(map);
var fileContent = readCsv.GetRecords<VisitExport>();
data = fileContent.ToList();
fileLoadedLink.Text = filePath;
viewModel.IsFileLoaded = true;
}
}
}
}
catch (CsvHelperException ex)
{
Console.WriteLine(ex.InnerException != null ? ex.InnerException.Message : ex.Message);
fileLoadedLink.Text = "Error loading file.";
viewModel.IsFileLoaded = false;
}
}
有没有办法比较 Csv 标题和我的地图?
解决方案
带有标题的 CSV 文件有两种基本情况:缺少 CSV 列和额外的 CSV 列。第一个已经被检测到,CsvHelper
而第二个的检测不是开箱即用的,需要对 进行子类化CsvReader
。
(由于 CsvHelper 按名称将 CSV 列映射到模型属性,因此置换 CSV 文件中列的顺序不会被视为重大更改。)
请注意,这仅适用于实际包含标题的 CSV 文件。由于您没有设置CsvConfiguration.HasHeaderRecord = false
,我假设这适用于您的用例。
以下是有关这两种情况的详细信息。
缺少 CSV 列。
目前 CsvHelper在这种情况下已经默认抛出异常。当找到未映射的数据模型属性时,CsvConfiguration.HeaderValidated
调用 。默认情况下,如果有任何未映射的模型属性,则将ConfigurationFunctions.HeaderValidated
其设置为当前行为是抛出 a 。如果您愿意,可以用自己的逻辑HeaderValidationException
替换或扩展:HeaderValidated
var culture = CultureInfo.GetCultureInfo("en-GB");
var config = new CsvConfiguration (culture)
{
HeaderValidated = (args) =>
{
// Add additional logic as required here
ConfigurationFunctions.HeaderValidated(args);
},
};
using (var readCsv = new CsvReader(reader, config))
{
// Remainder unchanged
演示小提琴#1在这里。
额外的 CSV 列。
目前CsvHelper
不会在发生这种情况时通知应用程序。如果 csv 包含意外的列 #1032 ,请参阅Throw ,它确认这不是开箱即用的实现。
在GitHub 评论中,用户leopignataro提出了一种解决方法,即CsvReader
自己继承并添加必要的验证逻辑。但是,评论中显示的版本似乎无法处理重复的列名或嵌入的引用。下面的子类CsvHelper
应该正确地做到这一点。它基于中的逻辑CsvReader.ValidateHeader(ClassMap map, List<InvalidHeader> invalidHeaders)
。它递归地遍历传入的ClassMap
,尝试找到与每个成员或构造函数参数对应的 CSV 标头,并标记每个映射的索引。之后,如果有任何未映射的标头,Action<CsvContext, List<string>> OnUnmappedCsvHeaders
则调用提供的标头以通知应用程序问题并在需要时抛出一些异常:
public class ValidatingCsvReader : CsvReader
{
public ValidatingCsvReader(TextReader reader, CultureInfo culture, bool leaveOpen = false) : this(new CsvParser(reader, culture, leaveOpen)) { }
public ValidatingCsvReader(TextReader reader, CsvConfiguration configuration) : this(new CsvParser(reader, configuration)) { }
public ValidatingCsvReader(IParser parser) : base(parser) { }
public Action<CsvContext, List<string>> OnUnmappedCsvHeaders { get; set; }
public override void ValidateHeader(Type type)
{
base.ValidateHeader(type);
var headerRecord = HeaderRecord;
var mapped = new BitArray(headerRecord.Length);
var map = Context.Maps[type];
FlagMappedHeaders(map, mapped);
var unmappedHeaders = Enumerable.Range(0, headerRecord.Length).Where(i => !mapped[i]).Select(i => headerRecord[i]).ToList();
if (unmappedHeaders.Count > 0)
{
OnUnmappedCsvHeaders?.Invoke(Context, unmappedHeaders);
}
}
protected virtual void FlagMappedHeaders(ClassMap map, BitArray mapped)
{
// Logic adapted from https://github.com/JoshClose/CsvHelper/blob/0d753ff09294b425e4bc5ab346145702eeeb1b6f/src/CsvHelper/CsvReader.cs#L157
// By https://github.com/JoshClose
foreach (var parameter in map.ParameterMaps)
{
if (parameter.Data.Ignore)
continue;
if (parameter.Data.IsConstantSet)
// If ConvertUsing and Constant don't require a header.
continue;
if (parameter.Data.IsIndexSet && !parameter.Data.IsNameSet)
// If there is only an index set, we don't want to validate the header name.
continue;
if (parameter.ConstructorTypeMap != null)
{
FlagMappedHeaders(parameter.ConstructorTypeMap, mapped);
}
else if (parameter.ReferenceMap != null)
{
FlagMappedHeaders(parameter.ReferenceMap.Data.Mapping, mapped);
}
else
{
var index = GetFieldIndex(parameter.Data.Names.ToArray(), parameter.Data.NameIndex, true);
if (index >= 0)
mapped.Set(index, true);
}
}
foreach (var memberMap in map.MemberMaps)
{
if (memberMap.Data.Ignore || !CanRead(memberMap))
continue;
if (memberMap.Data.ReadingConvertExpression != null || memberMap.Data.IsConstantSet)
// If ConvertUsing and Constant don't require a header.
continue;
if (memberMap.Data.IsIndexSet && !memberMap.Data.IsNameSet)
// If there is only an index set, we don't want to validate the header name.
continue;
var index = GetFieldIndex(memberMap.Data.Names.ToArray(), memberMap.Data.NameIndex, true);
if (index >= 0)
mapped.Set(index, true);
}
foreach (var referenceMap in map.ReferenceMaps)
{
if (!CanRead(referenceMap))
continue;
FlagMappedHeaders(referenceMap.Data.Mapping, mapped);
}
}
}
然后在您的代码中,随心所欲地处理OnUnmappedCsvHeaders
回调,例如通过抛出一个CsvHelperException
或其他一些自定义异常:
using (var readCsv = new ValidatingCsvReader(reader, culture)
{
OnUnmappedCsvHeaders = (context, headers) => throw new CsvHelperException(context, string.Format("Unmapped CSV headers: \"{0}\"", string.Join(",", headers))),
})
演示小提琴:
这可以使用额外的测试,例如用于具有参数化构造函数和额外可变属性的数据模型。
推荐阅读
- ios - 关闭应用程序并立即重新启动时,颤动的 iOS 出现意外行为
- node.js - 在 web 套接字流 binance api(node js) 中获取最近的交易订单信息
- npm - 运行脚本时缺少 NPM 命令
- android - Flutter 尝试创建文件 android\settings_aar.gradle,但失败
- python - PyCharm 在 Mac 上不显示任何绘图
- asp.net-core - 程序启动时自定义健康检查的 Masstransit 7.2.1 问题
- java - Kafkalistener SpringBoot 故障保护
- delphi - 如何暂时禁用单个 TButtonCategory?
- android - 将活动结果用于多种用途
- android - getItem.get(position) 而不是 get(position).get("item")