在C#中读取CSV文件的最佳方法,以提高时间效率

6

我有以下代码来读取一个大文件,比如超过一百万行。我使用并行和Linq方法。有更好的方法吗?如果有,那么怎么做呢?

        private static void ReadFile()
        {
            float floatTester = 0;
            List<float[]> result = File.ReadLines(@"largedata.csv")
                .Where(l => !string.IsNullOrWhiteSpace(l))
                .Select(l => new { Line = l, Fields = l.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) })
                .Select(x => x.Fields
                              .Where(f => Single.TryParse(f, out floatTester))
                              .Select(f => floatTester).ToArray())
                .ToList();

            // now get your totals
            int numberOfLinesWithData = result.Count;
            int numberOfAllFloats = result.Sum(fa => fa.Length);
            MessageBox.Show(numberOfAllFloats.ToString());
        }

        private static readonly char[] Separators = { ',', ' ' };

        private static void ProcessFile()
        {
            var lines = File.ReadAllLines("largedata.csv");
            var numbers = ProcessRawNumbers(lines);

            var rowTotal = new List<double>();
            var totalElements = 0;

            foreach (var values in numbers)
            {
                var sumOfRow = values.Sum();
                rowTotal.Add(sumOfRow);
                totalElements += values.Count;
            }
            MessageBox.Show(totalElements.ToString());
        }

        private static List<List<double>> ProcessRawNumbers(IEnumerable<string> lines)
        {
            var numbers = new List<List<double>>();
            /*System.Threading.Tasks.*/
            Parallel.ForEach(lines, line =>
            {
                lock (numbers)
                {
                    numbers.Add(ProcessLine(line));
                }
            });
            return numbers;
        }

        private static List<double> ProcessLine(string line)
        {
            var list = new List<double>();
            foreach (var s in line.Split(Separators, StringSplitOptions.RemoveEmptyEntries))
            {
                double i;
                if (Double.TryParse(s, out i))
                {
                    list.Add(i);
                }
            }
            return list;
        }

        private void button1_Click(object sender, EventArgs e)
        {
            Stopwatch stopWatchParallel = new Stopwatch();
            stopWatchParallel.Start();
            ProcessFile();
            stopWatchParallel.Stop();
            // Get the elapsed time as a TimeSpan value.
            TimeSpan ts = stopWatchParallel.Elapsed;

            // Format and display the TimeSpan value.
            string elapsedTime = String.Format("{0:00}:{1:00}:{2:00}.{3:00}",
                ts.Hours, ts.Minutes, ts.Seconds,
                ts.Milliseconds / 10);
            MessageBox.Show(elapsedTime);

            Stopwatch stopWatchLinQ = new Stopwatch();
            stopWatchLinQ.Start();
            ReadFile();
            stopWatchLinQ.Stop();
            // Get the elapsed time as a TimeSpan value.
            TimeSpan ts2 = stopWatchLinQ.Elapsed;

            // Format and display the TimeSpan value.
            string elapsedTimeLinQ = String.Format("{0:00}:{1:00}:{2:00}.{3:00}",
                ts2.Hours, ts.Minutes, ts.Seconds,
                ts2.Milliseconds / 10);
            MessageBox.Show(elapsedTimeLinQ);
        }

一般来说,Parallel是用于CPU密集型任务的。当你锁定Parallel.ForEach时,你得不到可扩展性,因此我认为它并不能显著地帮助你。 - Just another metaprogrammer
最好在codereview上询问此问题。 - Gert Arnold
4个回答

5

最近我遇到了一个问题,需要尽可能快地解析大型CSV文件,以实现数据聚合和度量计算(在我的情况下,最终目标是生成数据透视表)。我测试了最流行的CSV阅读器,但发现它们并不适合解析具有百万行或更多行的CSV文件;JoshClose的CsvHelper很快,但最终我能够使用流处理CSV,速度快2倍至4倍!

我的方法基于两个假设:

  • 尽可能避免创建字符串,因为这会浪费内存和CPU资源(增加GC负载)。相反,解析器的结果可以表示为一组“字段值”描述符,其中只保存缓冲区中的起始和结束位置+一些元数据(引用值标志、值中双引号的数量),只有在需要时才构建字符串值。
  • 使用循环char[]缓冲区读取CSV行,以避免过多的数据复制
  • 没有抽象层,最少的方法调用-这使得有效的JIT优化成为可能(例如,避免数组长度检查)。没有LINQ,没有迭代器(foreach)-因为for更高效。

真实使用情况数字(通过200MB CSV文件生成数据透视表,共17列,只使用3列来构建交叉表):

  • 我的自定义CSV读取器:约1.9秒
  • CsvHelper:约6.1秒

--- 更新 ---

我已在GitHub上发布了符合上述描述的库:https://github.com/nreco/csv

Nuget包:https://www.nuget.org/packages/NReco.Csv/


你把你的库发布到GitHub上了吗? - rburte
2
@rburte 目前还没有,你对这个库感兴趣吗?代码在我的产品(SeekTable)的生产环境中稳定运行良好,因此如果其他人也需要这个超快速/内存高效的CSV解析器,我可以将其发布到github / nuget上。 - Vitaliy Fedorchenko
是的,我会感激的。我的工作流程中需要加载和扫描的一部分,所以...目前这是一个瓶颈。 - rburte
你有计划使用新的 Span 和 Memory 类更新你的库吗? - Akmal Salikhov
1
@AkmalSalikhov 这个问题已经存在:https://github.com/nreco/csv/issues/1 实际上,这只是一种替代API方法,用于在不复制的情况下访问内部char[]缓冲区中的字符(结果是ReadOnlySpan<char>),我会适时处理——或者有人可能提出所需的更改和PR。我不指望从中获得显着的性能增益——因为这只能帮助避免不必要的分配。在内部,解析器直接使用char[]缓冲区+索引,这与Span<char>在底层上非常相似。 - Vitaliy Fedorchenko

3
你可以使用内置的OleDb来实现这一点。
public void ImportCsvFile(string filename)
{
    FileInfo file = new FileInfo(filename);

    using (OleDbConnection con = 
            new OleDbConnection("Provider=Microsoft.Jet.OLEDB.4.0;Data Source=\"" +
            file.DirectoryName + "\";
            Extended Properties='text;HDR=Yes;FMT=Delimited(,)';"))
    {
        using (OleDbCommand cmd = new OleDbCommand(string.Format
                                  ("SELECT * FROM [{0}]", file.Name), con))
        {
            con.Open();

            // Using a DataTable to process the data
            using (OleDbDataAdapter adp = new OleDbDataAdapter(cmd))
            {
                DataTable tbl = new DataTable("MyTable");
                adp.Fill(tbl);

                //foreach (DataRow row in tbl.Rows)

                //Or directly make a list
                List<DataRow> list = dt.AsEnumerable().ToList();
            }
        }
    }
} 

请参考以下链接了解更多信息:这个这个。该内容涉及IT技术,需要您进行进一步的解读。

看起来不错,可能会救我的命,但有一个更新。Microsoft.Jet.OLEDB.4.0在2002年被弃用,并且后来被KB 2017破坏了。请使用“Microsoft.ACE.OLEDB.12.0”。我已经成功地使用了Excel 8.0。 - John Pittaway

2

0

你应该看一下 CsvHelper => https://github.com/JoshClose/CsvHelper/

它允许你将你的 .csv 文件映射到一个类中,这样你就可以将你的 .csv 文件作为一个对象使用。试试它,然后尝试应用并行操作,看看是否有更好的性能。

这是我为一个项目准备的示例代码:

 using (var csv = new CsvReader(new StreamReader(filePath, Encoding.Default)))
 {
            csv.Configuration.Delimiter = ';'; 
            csv.Configuration.ClassMapping<LogHeaderMap, LogHeader>(); 


            var data = csv.GetRecords<LogHeader>();

            foreach (var entry in data.OrderByDescending(x => x.Date))
            {
               //process
            }
 }

对于 LogHeaderMap 我感到困惑。它是什么?它与 LogHeader 类有关吗?如果有关,那么它们之间的关系是什么?csv.Configuration.ClassMapping<LogHeaderMap, LogHeader>(); - MD TAREQ HASSAN

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