处理CsvHelper中的错误CSV记录

19
我希望能够遍历CSV文件中的所有记录,并将所有好的记录添加到一个集合中,将所有“坏”的记录分别处理。但我似乎无法做到这一点,我想我一定是漏掉了什么。
如果我尝试捕获BadDataException,那么后续的读取将会失败,这意味着我不能继续读取文件的剩余部分。
while (true)
{
    try
    {
        if (!reader.Read())
            break;

        var record = reader.GetRecord<Record>();
        goodList.Add(record);
    }
    catch (BadDataException ex)
    {
        // Exception is caught but I won't be able to read further rows in file
        // (all further reader.Read() result in same exception thrown)
        Console.WriteLine(ex.Message);
    }
}

另一个讨论的选择是将BadDataFound回调操作设置为处理它 -
reader.Configuration.BadDataFound = x =>
{
    Console.WriteLine($"Bad data: <{x.RawRecord}>");
};

然而,尽管回调被调用,但坏记录仍然出现在我的“好列表”中。

我能否查询读取器以查看记录是否良好,然后再将其添加到列表中?

对于此示例,我的记录定义为 -

class Record
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Age { get; set; }
}

而数据(第一行有误,第二行正确)-

"Jo"hn","Doe",43
"Jane","Doe",21

有趣的是,使用MissingFieldException处理缺少字段似乎正好符合我的要求——抛出异常,但后续行仍然可以正常读取。


好的,文件格式并没有规定什么是“错误行”。你遇到的是一个已损坏的文件。在读取文件时猜测其余部分可能性是毫无意义的,因为它不是有效的CSV文件。 - nvoigt
我不理解你上一条评论的意思,如果 MissingFieldException 是你想要的并且它能够满足你的需求,为什么它不能正常工作呢? - Tim Schmelter
为什么不在 while 块中的 try { } 之前放置指令 if (!reader.Read()) break;? - Jean-Claude Colette
@nvoigt 我同意文件格式并没有说明什么是坏行,不幸的是我无法控制源代码,但是如果你设置了BadDataFound回调函数,CsvHelper就能够完美地找出坏行并继续处理下一行。 - Zak
@Zak 在这种情况下,最好的方法是将CSV文件作为普通文本文件逐行读取,并解析每个字符串。如果字符串中没有逗号,您可以使用Split(',')。 - Jean-Claude Colette
显示剩余3条评论
2个回答

16

这是我提供的示例

void Main()
{
    using (var stream = new MemoryStream())
    using (var writer = new StreamWriter(stream))
    using (var reader = new StreamReader(stream))
    using (var csv = new CsvReader(reader))
    {
        writer.WriteLine("FirstName,LastName");
        writer.WriteLine("\"Jon\"hn\"\",\"Doe\"");
        writer.WriteLine("\"Jane\",\"Doe\"");
        writer.Flush();
        stream.Position = 0;

        var good = new List<Test>();
        var bad = new List<string>();
        var isRecordBad = false;
        csv.Configuration.BadDataFound = context =>
        {
            isRecordBad = true;
            bad.Add(context.RawRecord);
        };
        while (csv.Read())
        {
            var record = csv.GetRecord<Test>();
            if (!isRecordBad)
            {
                good.Add(record);
            }

            isRecordBad = false;
        }

        good.Dump();
        bad.Dump();
    }
}

public class Test
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

1
你需要将变量 var record = csv.GetRecord<Test>(); 放在 if 块内。 - Dmytro Laptin
3
在2021年,CsvHelp似乎不再起作用了。行csv.Configuration.BadDataFound = context...抛出只读属性。看起来曾经友好的CsvHelp库现在更加复杂难懂。我开始觉得编写自己的解析器比尝试理解如何防止CSVHelp抛出BadData异常更少工作量。唉。 - Fidel Orozco
5
配置已更改为只读,以避免线程问题。您只需要创建一个配置并将其传递到构造函数中即可。这个变化已经在更新日志中了,并且文档也已经进行了更新以反映这一点。我不能期望更新每一个堆栈溢出的问题,但我会在遇到它们时进行更新。如果您想编写自己的代码,那太棒了!请将其提交到速度测试中:https://www.joelverhagen.com/blog/2020/12/fastest-net-csv-parsers。他还将在未来添加兼容性测试。 - Josh Close
@JoshClose - 我已经在GitHub和项目网站上搜寻了你提到的示例,但似乎找不到任何方法“创建配置并将其传递给构造函数”,就像你所建议的那样。我的理解是创建CsvConfiguration实例,并设置它的BadDataFound委托,但是CsvConfiguration上唯一的构造函数需要CultureInfo而没有其他参数。尝试在实例本身上设置BadDataFound委托只会引起上面那个抱怨者提到的相同只读问题。 - bubbleking
@JoshClose - 没关系。我的疲惫让我忘记了对象初始化器的工作原理。 ‍♂️ 看到这条评论让我清醒过来。https://github.com/JoshClose/CsvHelper/issues/803#issuecomment-766199301 - bubbleking

0

可以一次性加载整个列表来完成。这是通用的实现。请参阅内联注释

public static List<TOut>? CsvLoad<TMap, TOut>(string path) where TMap : ClassMap<TOut> where TOut : class, 
{
    List<TOut>? modelList = null;

    if (File.Exists(path))
    {
        var config = new CsvConfiguration(CultureInfo.InvariantCulture);
        config.ReadingExceptionOccurred = re =>
        {
            // HERE YOU CAN DO ANYTHING YOU WANT WITH A BAD ROW
            Debug.WriteLine($"Bad Row in file '{path}'; CSV ERROR: {re.Exception}");
            return false; // <-- tells process to continue
        };      


        try
        {

            using (var stream = new StreamReader(path))
            using (var csv = new CsvReader(stream, config))
            {
                csv.Context.RegisterClassMap<TMap>();
                modelList = csv.GetRecords<TOut>().ToList(); // <-- get all records
            }
        }
        catch (Exception ex)
        {
            // This must be some bad exception
        }
    }
    else
    {
        // LOG, THROW EXCEPTION, whatever
    }
    return modelList;

}

请注意:可以根据需要添加更多的配置。可以向方法添加options参数。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接