如何解决将 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 文件的代码,它运行良好。我有一个 try catch 来解决错误,但理想情况下,如果它专门因标题未命中匹配而失败,我想专门处理它。
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
,其当前行为是在有任何未映射的模型属性时抛出 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 here。
额外的 CSV 列。
目前 CsvHelper
不会在发生这种情况时通知应用程序。请参阅 Throw if csv contains unexpected columns #1032 以确认这不是开箱即用的。
在 GitHub comment 中,用户 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))),})
演示小提琴:
- #2 (your model)。
- #3 (with external references)。
- #4 (duplicate names)。
- #5 (using the auto-generated map)。
这可以使用额外的测试,例如用于具有参数化构造函数和附加可变属性的数据模型。
,在抓到HeaderValidationException
之前先抓到CsvHelperException
怎么样
catch (HeaderValidationException ex)
{
var message = ex.Message.Split('\n')[0];
var currentHeader = ex.Context.Reader.HeaderRecord;
message += $"{Environment.NewLine}Header: \"{string.Join(",currentHeader)}\"";
Console.WriteLine(message);
fileLoadedLink.Text = "Error loading file.";
viewModel.IsFileLoaded = false;
}
catch (CsvHelperException ex)
{
Console.WriteLine(ex.InnerException != null ? ex.InnerException.Message : ex.Message);
fileLoadedLink.Text = "Error loading file.";
viewModel.IsFileLoaded = false;
}
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。