使用CSVHelper将复杂对象列表写入CSV文件

3

我有一个类对象的列表,其中又包含另一个类对象的列表。它们看起来像这样:

public class Column
{
    public string ColName { get; set; }
    public List<Item> ItemList { get; set; }
}

public class Item
{
    public DateTime TimeStamp { get; set; }
    public double Value { get; set; }
}

它们有两个重要属性:

  1. 所有列中 List<Item> ItemListLength 将相同。但是,我直到运行时才知道该长度。
  2. 每个列中 TimeStamp 的实际值将相同。换句话说,每个列中的 TimeStamp 列表将是相同的。

如果您查看下面我编写的创建 Column 对象列表的模拟函数,您将获得一个清晰的图片。这是我的实际数据(从程序的其他地方获取)的准确描述:

private static List<Column> GetColumns()
{
    var dt1 = DateTime.Now;
    var dt2 = dt1.AddSeconds(1);
    var dt3 = dt2.AddSeconds(1);
    var dt4 = dt3.AddSeconds(1);

    var col1 = new Column()
    {
        ColName = "ABC",
        ItemList = new List<Item>
        {
            new Item() { TimeStamp = dt1, Value = 1 },
            new Item() { TimeStamp = dt2, Value = 2 },
            new Item() { TimeStamp = dt3, Value = 3 },
            new Item() { TimeStamp = dt4, Value = 4 }
        }
    };

    var col2 = new Column()
    {
        ColName = "XYZ",
        ItemList = new List<Item>
        {
            new Item() { TimeStamp = dt1, Value = 4 },
            new Item() { TimeStamp = dt2, Value = 3 },
            new Item() { TimeStamp = dt3, Value = 2 },
            new Item() { TimeStamp = dt4, Value = 1 }
        }
    };

    var col3 = new Column()
    {
        ColName = "KLM",
        ItemList = new List<Item>
        {
            new Item() { TimeStamp = dt1, Value = 1 },
            new Item() { TimeStamp = dt2, Value = 2 },
            new Item() { TimeStamp = dt3, Value = 4 },
            new Item() { TimeStamp = dt4, Value = 8 }
        }
    };

    var list = new List<Column>
    {
        col1,
        col2,
        col3,
    };

    return list;
}

重要提示是,我不会在运行时知道 List<Column> 的长度,也不会知道它们内部的 ItemList 的长度。此外,我也不会在运行时知道每个列的名称。现在我的目标是将这些信息写入以下格式的 CSV 文件。

enter image description here

我觉得这里应该使用动态的方式,所以我从这个开始:

var columns = GetColumns();

var timeStamps = columns.First().ItemList.Select(x => x.TimeStamp).ToList();

var writeList = new List<dynamic>();
for (int i = 0; i < timeStamps.Count; i++)
{
    dynamic csvItem = new ExpandoObject();
    csvItem.TimeStamp = timeStamps[i];
    // How to get columns?
    writeList.Add(csvItem);
}

using (var writer = new StreamWriter("output.csv"))
{
    using (var csv = new CsvHelper.CsvWriter(writer))
    {
        csv.WriteRecords(writeList);
    }
}

这是一个开始,但由于我不知道将拥有多少列和它们的名称,所以不确定如何继续。在这里使用 dynamic 不是一个选项吗?
或者我可以完全放弃使用 CSVHelper,像下面这样从头开始编写,但有点凌乱。我需要两次迭代列列表,并总共需要三个循环。如果可能的话,我正在寻找更优雅的解决方案。
var columns = GetColumns();

var timeStamps = columns.First().ItemList.Select(x => x.TimeStamp).ToList();

var headers = "TimeStamp";
foreach (var col in columns)
{
    headers += "," + col.ColName;
}

using (var fs = new FileStream("output.csv", FileMode.Create, FileAccess.Write))
{
    using (var sw = new StreamWriter(fs))
    {
        sw.WriteLine(headers);
        for (int i = 0; i < timeStamps.Count; i++)
        {
            var line = timeStamps[i].ToString("yyyy/MM/dd HH:mm:ss");
            foreach (var col in columns)
            {
                line += "," + col.ItemList[i].Value;
            }
            sw.WriteLine(line);
        }
    }
}

为什么不使用StringBuilder创建整个csv结构,然后将其保存为csv呢?这比第二种方法要快得多。至于csvHelper,对于像这样简单的东西,我甚至不会费心添加nuget依赖项。 - Kosala W
@KosalaW 你有一个可以让我开始的例子吗?我使用CSVHelper的原因是因为我们的项目已经在其他大量CSV操作中使用它,所以我想坚持使用它。 - Sach
3个回答

3
这可能是您在满足要求时使用CsvHelper的最佳选择。
using (var writer = new StreamWriter("output.csv"))
{
    using (var csv = new CsvHelper.CsvWriter(writer))
    {
        var columns = GetColumns();

        // Write header
        csv.WriteField("TimeStamp");

        foreach (var column in columns)
        {
            csv.WriteField(column.ColName);
        }

        csv.NextRecord();

        // Write rows
        for (int i = 0; i < columns[0].ItemList.Count; i++)
        {
            csv.WriteField(columns[0].ItemList[i].TimeStamp.ToString("yyyy/MM/dd HH:mm:ss"));

            foreach (var column in columns)
            {
                csv.WriteField(column.ItemList[i].Value);
            }

            csv.NextRecord();
        }
    }
}

谢谢!是的,它能完成任务,但几乎与我的第二个代码示例完全相同;我想知道是否有一种避免使用这样东西的方法。 - Sach

1
这是一个使用dynamic的版本。
var columns = GetColumns();

var writeList = new List<dynamic>();

for (int i = 0; i < columns[0].ItemList.Count; i++)
{
    var csvItem = new ExpandoObject() as IDictionary<string, object>;

    csvItem.Add("TimeStamp", columns[0].ItemList[i].TimeStamp.ToString("yyyy/MM/dd HH:mm:ss"));

    foreach (var column in columns)
    {
        csvItem.Add(column.ColName, column.ItemList[i].Value);
    }

    writeList.Add(csvItem);
}

using (var writer = new StreamWriter("output.csv"))
{
    using (var csv = new CsvHelper.CsvWriter(writer))
    {
        csv.WriteRecords(writeList);
    }
}

0

下面是使用 StringBuilder 处理 CSV 写入的方法。此代码适用于仅需要将对象转换为 CSV 的情况。但如果您需要使用 CSV 作为持久化机制(这不太可能,应该使用 mongoDb 或 Sqlite),则必须向这些扩展方法添加更多功能。

分隔符:文件类型为 CSV - 逗号分隔值。因此,此代码使用“,”。如果你愿意,你也可以通过对此代码进行一些简单的修改来创建 TAB 分隔值。

用法:

var columns = GetColumns();

Console.WriteLine(columns.ToCsv());
//OR
columns.SaveAsCsv(@"c:\columns.csv");

Csv扩展;

public static class ColumnExtensions
{
    public static void SaveAsCsv(this List<Column> columns, string filePath)
    {
        File.WriteAllText(columns.ToCsv(), filePath);
    }

    public static string ToCsv(this List<Column> columns)
    {
        var csv = new StringBuilder();

        // Write as an expression or simply 
        //csv.AppendCsvHeader(nameof(Item.TimeStamp));
        csv.AppendCsvHeader<string, DateTime>(x => columns.First().ItemList.First().TimeStamp);

        for (var index = 0; index < columns.Count; index++)
        {
            var column = columns[index];

            // Most csv readers don't care if you have a "," at the end of the line. But for completeness we avoid doing that.
            // It makes the code a bit more complicated though. You can ignore this you want.
            csv.AppendCsvHeader(column.ColName, index == columns.Count - 1);
        }

        csv.AppendLine();

        for (var i = 0; i < columns[0].ItemList.Count; i++)
        {
            csv.AppendCsvField(columns[0]
                .ItemList[i]
                .TimeStamp.ToString("yyyy/MM/dd HH:mm:ss"));

            for (var index = 0; index < columns.Count; index++)
            {
                var column = columns[index];

                csv.AppendCsvField(column.ItemList[i]
                        .Value.ToString("N"), index == columns.Count - 1);
            }

            csv.AppendLine();
        }

        return csv.ToString();
    }
}

public static class CsvExtensions
{
    private const string Delimiter = ",";

    private static string AsCsvFriendly(this string val)
    {
        return val?.Replace(",", ";") ?? string.Empty;
    }

    private static string AddDelimiterIfRequired(bool withoutDelimiter)
    {
        return withoutDelimiter ? string.Empty : Delimiter;
    }

    public static void AppendCsvField(this StringBuilder stringBuilder, string value, bool withoutDelimiter = false)
    {
        stringBuilder.Append($"{value.AsCsvFriendly()}{AddDelimiterIfRequired(withoutDelimiter)}");
    }

    public static void AppendCsvHeader(this StringBuilder stringBuilder, string value, bool withoutDelimiter = false)
    {
        stringBuilder.Append($"{value.AsCsvFriendly()}{AddDelimiterIfRequired(withoutDelimiter)}");
    }

    public static void AppendCsvHeader<TIn, TOut>(this StringBuilder stringBuilder, Expression<Func<TIn, TOut>> f, bool withoutDelimiter = false)
    {
        stringBuilder.Append($"{(f.Body as MemberExpression)?.Member.Name}{AddDelimiterIfRequired(withoutDelimiter)}");
    }
}

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