如何使用 GetRecord 将具有单个标头的 CSV 文件中的数据解析为不同类型的实例?

问题描述 投票:0回答:1

我尝试按照链接中的方法https://joshclose.github.io/CsvHelper/examples/reading/reading-multiple-record-types/来实现将单个 CSV 文件转换为实例的特定要求不同种类。但是,链接中提供的示例选择不写入 CSV 标头,这对我提出了挑战。在我的 ClassMap 实现中,我无法使用 Name 和 AutoMap,因为删除标头会导致问题。并且在每个 ClassMap 中手动指定索引似乎很乏味。

因此,我尝试在 CSV 文件中写入标头,并将 CsvReader 使用的 CsvConfiguration 的 HasHeaderRecord 属性设置为 true。同时,我暂时保留了使用Index Way的映射。核心代码如下:

 public void CSVDeserialize() {
        var config = new CsvConfiguration(CultureInfo.InvariantCulture) {
            HasHeaderRecord = true,
            MissingFieldFound = null
        };
        using (var reader = new StreamReader(Application.dataPath + "/Data/Events/0.csv"))
            using (var csv = new CsvReader(reader, config)) {
                csv.Context.RegisterClassMap<FUF_Event_Dialogue.ClassMap>();
                csv.Context.RegisterClassMap<FUF_Event_Avatar.ClassMap>();

                List<FUF_Event_Dialogue> dialogues = new List<FUF_Event_Dialogue>();
                List<FUF_Event_Avatar> avatars = new List<FUF_Event_Avatar>();

                while (csv.Read()) {
                    switch (csv.GetField(0)) {
                        case "Dialogue":
                            dialogues.Add(csv.GetRecord<FUF_Event_Dialogue>());
                            break;
                        case "Avatar":
                            avatars.Add(csv.GetRecord<FUF_Event_Avatar>());
                            break;
                    }
                } 

                // Print Results
                foreach (FUF_Event_Avatar avatar in avatars) {
                    Debug.Log($"[Avatar] ID: {avatar.ID}, Delay: {avatar.Delay}, IsAsync: {avatar.IsAsync}, Animation: \"{avatar.Animation.AvatarId + "," + avatar.Animation.Label + "," + avatar.Animation.Emotion + "," + avatar.Animation.IsShutUp + "," + avatar.Animation.IsSkipPreAnim}\", Movement: {avatar.Movement.TimeSpeed + (avatar.Movement.UseSpeed ? "/s" : "ms") + "=>" + avatar.Movement.Position.x + "," + avatar.Movement.Position.y}");
                }

                foreach (FUF_Event_Dialogue dialogue in dialogues) {
                    Debug.Log($"[Dialogue] ID: {dialogue.ID}, Delay: {dialogue.Delay}, IsAsync: {dialogue.IsAsync}, Content: {dialogue.Content.ID}, Label: {dialogue.Label}, Branches: \"{dialogue.Branches?.Select(b => b.Content.ID + "," + b.GotoId).Aggregate((c, n) => c + ";" + n)}\"");
                }
            }
    }

public class FUF_Event_Dialogue : IFUF_Event {
        public int ID { get; set; }
        public float Delay { get; set; }
        public bool IsAsync { get; set; }
        public FUF_String Content { get; set; }
        public string Label { get; set; }
        public FUF_Branch[] Branches => branches ??= branchEnumerable?.ToArray();

        FUF_Branch[] branches;
        IEnumerable<FUF_Branch> branchEnumerable;

        public sealed class ClassMap : ClassMap<FUF_Event_Dialogue> {
            public ClassMap() {
                Map(m => m.ID).Index(1);
                Map(m => m.Delay).Index(2);
                Map(m => m.IsAsync).Index(3).Default(false);
                Map(m => m.Content).Index(4).Default(null).TypeConverter<FUF_String.Deserializer>();
                Map(m => m.Label).Index(5).Default("");
                Map(m => m.branchEnumerable).Index(6).TypeConverter<FUF_Branch.Deserializer>();
            }
        }
}

public class FUF_Event_Avatar : IFUF_Event {
        public int ID { get; set; }
        public float Delay { get; set; }
        public bool IsAsync { get; set; }
        public FUF_Animation Animation { get; set; }
        public FUF_Movement Movement { get; set; }

        public sealed class ClassMap : ClassMap<FUF_Event_Avatar> {
            public ClassMap() {
                Map(m => m.ID).Index(1);
                Map(m => m.Delay).Index(2).Default(0);
                Map(m => m.IsAsync).Index(3).Default(false);
                Map(m => m.Animation).Index(4).TypeConverter<FUF_Animation.Deserializer>();
                Map(m => m.Movement).Index(5).TypeConverter<FUF_Movement.Deserializer>();
            }
        }
}

执行此操作后,我注意到标题后的第一行被忽略并且未成功解析。这是我的 CSV 和输出日志:

CSV:

Type,ID,Delay,IsAsync,Property_4,Property_5,Property_6
Avatar,0,0,FALSE,"0,Jack,Stand,False,True","100ms=>256,128"
Avatar,1,0,FALSE,"0,Mary,Stand,False,True","20/s=>-256,128"
Dialogue,2,0,FALSE,1001,Jack
Dialogue,3,0,FALSE,1002,Mary,"9001,1005;9002,1006"
Dialogue,4,0,FALSE,1003,Jack
Dialogue,5,0,FALSE,1004,Jack,"256,128;257,129"

日志:

[Avatar] ID: 1, Delay: 0, IsAsync: False, Animation: "0,Mary,Stand,False,True", Movement: 20/s=>-256,128
[Dialogue] ID: 2, Delay: 0, IsAsync: False, Content: 1001, Label: Jack, Branches: ""
[Dialogue] ID: 3, Delay: 0, IsAsync: False, Content: 1002, Label: Mary, Branches: "9001,1005;9002,1006"
[Dialogue] ID: 4, Delay: 0, IsAsync: False, Content: 1003, Label: Jack, Branches: ""
[Dialogue] ID: 5, Delay: 0, IsAsync: False, Content: 1004, Label: Jack, Branches: "256,128;257,129"

为什么会发生这种情况? (我可以确认,在不指定 Header 并将 HasHeaderRecord 设置为 false 的情况下,所有内容都已成功解析。)

接下来,如果我将 ClassMap 中与 CSV 中的 Header 同名的属性更改为使用默认的基于名称的解析或使用 AutoMap,我会遇到这些属性都被解析为默认值的问题,例如 0 或错误的。这是我修改的代码和相应的日志:

        public sealed class ClassMap : ClassMap<FUF_Event_Dialogue> {
            public ClassMap() {
                Map(m => m.ID);
                Map(m => m.Delay);
                Map(m => m.IsAsync).Default(false);
                Map(m => m.Content).Index(4).Default(null).TypeConverter<FUF_String.Deserializer>();
                Map(m => m.Label).Index(5).Default("");
                Map(m => m.branchEnumerable).Index(6).TypeConverter<FUF_Branch.Deserializer>();
                // Or AutoMap, cause the same issue
                /*AutoMap(new CsvConfiguration(CultureInfo.InvariantCulture) {
                    HasHeaderRecord = true,
                    MissingFieldFound = null
                });*/
            }
        }

日志:

[Avatar] ID: 0, Delay: 0, IsAsync: False, Animation: "0,Mary,Stand,False,True", Movement: 20/s=>-256,128
[Dialogue] ID: 0, Delay: 0, IsAsync: False, Content: 1001, Label: Jack, Branches: ""
[Dialogue] ID: 0, Delay: 0, IsAsync: False, Content: 1002, Label: Mary, Branches: "9001,1005;9002,1006"
[Dialogue] ID: 0, Delay: 0, IsAsync: False, Content: 1003, Label: Jack, Branches: ""
[Dialogue] ID: 0, Delay: 0, IsAsync: False, Content: 1004, Label: Jack, Branches: "256,128;257,129"

如您所见,在这种情况下,使用 AutoMap 或基于名称解析“ID”字段将无法正常工作,并且您只会得到 0 作为结果。

如果您修改了 Header 之后被忽略的第一行的类型的 ClassMap,甚至可能会出现更严重的问题:

        public sealed class ClassMap : ClassMap<FUF_Event_Avatar> {
            public ClassMap() {
                Map(m => m.ID);
                Map(m => m.Delay).Default(0);
                Map(m => m.IsAsync).Default(false);
                Map(m => m.Animation).Index(4).TypeConverter<FUF_Animation.Deserializer>();
                Map(m => m.Movement).Index(5).TypeConverter<FUF_Movement.Deserializer>();
                // Or AutoMap, cause the same issue
                /*AutoMap(new CsvConfiguration(CultureInfo.InvariantCulture) {
                    HasHeaderRecord = true,
                    MissingFieldFound = null
                });*/
            }
        }

错误:

HeaderValidationException: Header with name 'ID'[0] was not found.
Header with name 'Delay'[0] was not found.
Header with name 'IsAsync'[0] was not found.
Headers: 'Avatar', '0', '0', 'FALSE', '0,Jack,Stand,False,True', '100ms=>256,128'
Headers: 'Avatar', '0', '0', 'FALSE', '0,Jack,Stand,False,True', '100ms=>256,128'
Headers: 'Avatar', '0', '0', 'FALSE', '0,Jack,Stand,False,True', '100ms=>256,128'
Headers: 'Avatar', '0', '0', 'FALSE', '0,Jack,Stand,False,True', '100ms=>256,128'
Headers: 'Avatar', '0', '0', 'FALSE', '0,Jack,Stand,False,True', '100ms=>256,128'
Headers: 'Avatar', '0', '0', 'FALSE', '0,Jack,Stand,False,True', '100ms=>256,128'
If you are expecting some headers to be missing and want to ignore this validation, set the configuration HeaderValidated to null. You can also change the functionality to do something else, like logging the issue.

IReader state:
   ColumnCount: 0
   CurrentIndex: 0
   HeaderRecord:
["Avatar","0","0","FALSE","0,Jack,Stand,False,True","100ms=>256,128"]
IParser state:
   ByteCount: 0
   CharCount: 117
   Row: 2
   RawRow: 2
   Count: 6
   RawRecord:
Avatar,0,0,FALSE,"0,Jack,Stand,False,True","100ms=>256,128"


CsvHelper.Configuration.ConfigurationFunctions.HeaderValidated (CsvHelper.HeaderValidatedArgs args) (at <809ff6784d2e43bdb7ac8274e1351270>:0)
CsvHelper.CsvReader.ValidateHeader (System.Type type) (at <809ff6784d2e43bdb7ac8274e1351270>:0)
CsvHelper.CsvReader.ValidateHeader[T] () (at <809ff6784d2e43bdb7ac8274e1351270>:0)
CsvHelper.CsvReader.GetRecord[T] () (at <809ff6784d2e43bdb7ac8274e1351270>:0)

从错误日志中可以看到,

'Avatar', '0', '0', 'FALSE', '0,Jack,Stand,False,True', '100ms=>256,128'
,也就是之前提到的被忽略的行,实际上被当作了Header。

有没有办法在不删除标题的情况下解决这些问题?如果可能的话,我仍然想包含标题以保持代码和 CSV 干净。

c# csv polymorphism csvhelper
1个回答
0
投票

您可以通过修改

Read()
循环以添加
default:
案例来发现问题:

while (csv.Read()) {
    string type;
    switch (type = csv.GetField(0)) {
        // Cases "Dialogue" and "Avatar" as before
        default:
            //Debug.Log(string.Format("Unknown type {0}", type));
            Console.WriteLine(string.Format("Unknown type {0}", type)); // 
            break;
    }
} 

如果有的话,您会看到一条错误消息:

Unknown type Type

因此,您的标题行实际上被第一个

Read()
消耗并丢弃。那么头记录什么时候真正被读取和处理呢?可能性包括:

  1. 每当您明确调用

    CsvReader.ReadHeader()
    (您没有这样做),或者

  2. 您第一次调用

    CsvReader.GetRecord<T>()
    源代码可以看出,如果尚未读取,此方法会自动读取当前行作为标题行,然后将其后面的行作为数据读取行:

    public virtual T? GetRecord<T>()
    {
        CheckHasBeenRead();
    
        if (headerRecord == null && hasHeaderRecord)
        {
            ReadHeader();
            ValidateHeader<T>();
    
            if (!Read())
            {
                return default;
            }
        }
    

因此你的代码实际做的是:

  1. 跳过第一行。
  2. 将第二行读取为标题行。
  3. 读取第三行作为第一个数据行。

演示小提琴 #1 此处[1]

既然你显然不希望这样,你应该简单地设置

CsvConfiguration.HasHeaderRecord = false
并且你的 CSV 文件将被正确读取,因为
Read()
循环将丢弃初始标题行(以及第一个字段不是
Avatar 的任何其他行) 
也不是
Dialogue
。)此外,我认为这个修复是正确的。您的两个模型
FUF_Event_Dialogue
FUF_Event_Avatar
具有不同的属性名称,因此两个模型的多态标题行将具有组合属性集的列标题,如下所示:

Type,ID,Delay,IsAsync,Content,Label,Branches,Animation,Movement

然后在反序列化时仅使用相关列。但在您的实际 CSV 中,列标题无论如何都是毫无意义的(因为您的任何模型中都没有名为 Property_4、Property_5 或 Property_6 的实际属性),只有索引很重要。

演示小提琴 #2 这里


[1] 您的问题中显示的代码无法编译,因为它缺少多个属性的类型定义。我能够通过对任何未知的属性类型使用

string
来重现该问题。

© www.soinside.com 2019 - 2024. All rights reserved.