c# - 将 CSV header 与 map 类进行比较

标签 c# csv csvhelper

我有一个过程,我们编写了一个类,使用 CsvHelper ( https://joshclose.github.io/CsvHelper ) 将一个大 (ish) CSV 导入我们的应用程序。
我想将标题与 Map 进行比较以确保标题的完整性。我们从第 3 方获得 CSV 文件,我想确保它不会随着时间的推移而改变,我认为最好的方法是将其与 map 进行比较。
我们有一个这样设置的类(修剪):

public class VisitExport
{
    public int? Count { get; set; }
    public string CustomerName { get; set; }
    public string CustomerAddress { get; set; }
}
及其对应的 map (也修剪过):
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 标题与我的 map ?

最佳答案

带有标题的 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
演示 fiddle #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 header ,并标记每个映射的索引。之后,如果有任何未映射的 header ,则提供 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))),
       })
演示 fiddle :
  • #2 (your model) .
  • #3 (with external references) .
  • #4 (duplicate names) .
  • #5 (using the auto-generated map) .

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

    关于c# - 将 CSV header 与 map 类进行比较,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/66984134/

    相关文章:

    python - 在Python中将行转置为列

    csv - 如何从 JMeter 生成的 csv 文件计算平均响应时间?

    c# - CsvHelper 并并行查询大型 csv 文件

    C# 锁定 Vault 对象

    java - 在 Java 中读取制表符分隔的文件

    c# - Javascript 文化总是 en-us

    c# - CsvHelper 未正确解析

    c# - CSV-Helper 无法转换 bool 值时出现问题

    c# - VS2010 Test Professional/Team Server 的替代品?

    c# - 在 SQL Server 2008 中实现(日期 - 时间增量)