使用 CsvHelper 将 csv 文件转换为 excel 时减少内存

Posted

技术标签:

【中文标题】使用 CsvHelper 将 csv 文件转换为 excel 时减少内存【英文标题】:Reduce the memory when converting csv file to excel with CsvHelper 【发布时间】:2021-11-09 19:02:09 【问题描述】:

当我们使用具有 100K 行和 14 列的 CSVHelper 将 csv 文件转换为 excel 时,需要 1.5GB 的进程内存。我们需要减少内存。这是 CsvHelper 的问题,占用了那么多内存来转换文件,还是我们在这里做错了什么。有没有办法减少内存。

这是我正在使用的示例程序:

using ClosedXML.Excel;
using CsvHelper;
using System;
using System.Globalization;
using System.IO;

namespace ConsoleApp2.Conversion

    public class CsvHelperExcelWriter
    
        public void Process(string csvFilePath)
        
            try
            
                Stream ms = new MemoryStream();
                using (var workbook = new XLWorkbook(XLEventTracking.Disabled))
                
                    var _worksheet = workbook.AddWorksheet("Sheet1");
                    using (var csv = new CsvReader(new StreamReader(csvFilePath), CultureInfo.InvariantCulture))
                    
                        csv.Read();
                        if (!string.IsNullOrWhiteSpace(csv.Context.Parser.RawRecord))
                        
                            AddHeaders(csv, _worksheet);
                            AddValues(csv, _worksheet);
                        
                    
                    workbook.SaveAs(ms);
                    ms.Position = 0;
                    using (FileStream fileStream = new FileStream("C:\\Projects\\POC\\SampleFile\\Excel\\100000 Sales Records with CsvHelper.xlsx", FileMode.Append, FileAccess.Write))
                    
                        ms.CopyTo(fileStream);
                        fileStream.Close();
                    
                
            
            catch (Exception)
            
                throw;
            
        

        private void AddValues(CsvReader csv, IXLWorksheet _worksheet)
        
            int rowNumber = 2;
            while (csv.Read())
            
                int cellNumber = 1;
                for (var i = 0; csv.TryGetField(i, out string value); i++)
                
                    _worksheet.Cell(rowNumber, cellNumber).SetValue(value);
                    cellNumber++;
                
                rowNumber++;
            
        

        private void AddHeaders(CsvReader csv, IXLWorksheet _worksheet)
        
            int index = 1;
            _ = csv.ReadHeader();
            foreach (var header in csv.HeaderRecord)
            
                _worksheet.Cell(1, index).Value = header;
                index++;
            
        

    

【问题讨论】:

当 workbook.SaveAs() 已经允许传递文件名时,为什么要创建 MemoryStream 只是为了将其保存到文件中。 您可能需要添加 ClosedXML 标签,因为您正在使用该标签 CsvHelper 将产生记录,因此您在读取行时只使用了一小块内存缓冲区。 ClosedXML 有几个内存消耗大的错误。与他们核实或尝试使用不同的 Excel 库。 【参考方案1】:

我不熟悉 C#。但是,如果我要猜测这个问题,它可能会一次读取所有内容并将其保存在内存中。您应该使用某种缓冲方法,其中整个 csv 文件不会一次加载到内存中。一旦你转换了 csv 行,你必须确保你立即将它输出到 excel 文件并丢弃 csv 行(不要将它存储在任何数组/列表中),以便它可以被垃圾收集器拾取。

【讨论】:

【参考方案2】:

通常高内存使用率是因为所有 Excel 库在创建 XLSX 文件时都使用最终 XML 序列化。为了解决这个问题,我创建了自己的 SwiftExcel 库,该库将数据直接输出到文件中,省略了实际的 XML 序列化。

在您的情况下,我建议使用 ExcelDataReader 之类的高效读取 CSV,然后使用 SwiftExcel 输出。

这是一个例子。首先使用 ExcelDataReader 阅读您的 CSV 并收集所有数据:

var list = new List<List<string>>();
using (var stream = File.Open("C:\\temp\\input.xlsx", FileMode.Open, FileAccess.Read))

    using (var reader = ExcelReaderFactory.CreateCsvReader(stream))
    
        while (reader.Read())
        
            var subList = new List<string>();
            for (var i = 0; i < reader.FieldCount; i++)
            
                subList.Add(reader.GetValue(i)?.ToString());
            
            list.Add(subList);
        
    
 

现在使用SwiftExcel将此数据输出到Excel:

using (var ew = new ExcelWriter("C:\\temp\\test.xlsx"))

    for (var row = 1; row <= list.Count; row++)
    
        var subList = list[row-1];
        for (var col = 1; col <= subList.Count; col++)
        
            ew.Write(subList[col-1], col, row);
        
    

仍然存在内存问题,因为您首先收集所有 CSV 数据,但至少它没有使用 XML 序列化那么高。您可以尝试通过将这两个循环合并为一个并在读取时同时输出值来消除这种情况。

【讨论】:

以上是关于使用 CsvHelper 将 csv 文件转换为 excel 时减少内存的主要内容,如果未能解决你的问题,请参考以下文章

将 CsvHelper 自定义转换器应用于一组类的所有字符串属性

使用 CsvHelper 时转换为十进制

使用 CsvHelper 将 CSV 中的所有值读入列表

使用 CsvHelper 将 CSV 文件中的编号列映射到数组

C#导出csv文件,不依赖System.Drawing.Common导出excel文件,导致在docker使用失败。CsvHelper支持跨平台使用

将值转换为 int