首页 > 解决方案 > 将 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 标题和我的地图?

标签: c#csvcsvhelper

解决方案


带有标题的 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))),
       })

演示小提琴:

这可以使用额外的测试,例如用于具有参数化构造函数和额外可变属性的数据模型。


推荐阅读