.Net 大數(shù)據(jù)量導(dǎo)出Excel方案

前言

當(dāng)要導(dǎo)出的 DataTable 數(shù)據(jù)量很大時(shí)(比如行數(shù)幾十萬的情況下)翩蘸,NPOI 內(nèi)存占用非常高防嗡,這里研究一下性能更好的excel導(dǎo)出方式

一变汪、使用 closedXML

image.png

0、工具類-生成DataTable

using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ExcelExportTest
{
    public class GenerateDataTable
    {
        public DataTable GetNewTable(int rowCount=600000)
        {
            Stopwatch sw = new Stopwatch();
            sw.Start();
            var data = new DataTable("測試表格");
            data.Columns.Add("Library", typeof(string));
            data.Columns.Add("Name", typeof(string));
            data.Columns.Add("Description", typeof(string));
            data.Columns.Add("Type", typeof(string));
            data.Columns.Add("Definer", typeof(string));
            data.Columns.Add("Definer_Description", typeof(string));
            data.Columns.Add("Creation_Date", typeof(DateTime));
            data.Columns.Add("Days_Since_Creation", typeof(string));
            data.Columns.Add("Size", typeof(string));
            data.Columns.Add("Last_Used", typeof(DateTime));
            data.Columns.Add("Attribute", typeof(string));
            data.Columns.Add("Count_Of_Objects_Referenced", typeof(string));
            data.Columns.Add("Possibly_Referenced", typeof(string));

            for (var i = 0; i < rowCount; i++)
            {
                data.Rows.Add(
                    "xxxxxxxxx",
                    "xxxxxxxxx",
                    "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
                    "*USRSPC",
                    "xxxxxxxxx",
                    "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
                    DateTime.Now,
                    "789",
                    "16384",
                    DateTime.Now.AddDays(120),
                    "GRC",
                    "0",
                    "0"
                );
            }
            sw.Stop();
            Console.WriteLine($"Generate datable used [{sw.ElapsedMilliseconds}] ms");
            return data;
        }
    }
}

1蚁趁、一次導(dǎo)入全部數(shù)據(jù)并保存

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using ClosedXML.Excel;

namespace ExcelExportTest
{
    public class ClosedXmlTest
    {
        public void DoTest_GenerateDirectly()
        {
            var gdt = new GenerateDataTable();
            var table = gdt.GetNewTable();
            Thread.Sleep(5000);

            Stopwatch sw = new Stopwatch();
            sw.Start();
            using (var workbook = new XLWorkbook())
            {
                var filePath = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss")+ ".xlsx");
                var worksheet = workbook.Worksheets.Add(table, table.TableName);
                sw.Stop();
                Console.WriteLine($"Add workbook used {sw.ElapsedMilliseconds} ms");
                sw.Restart();
                workbook.SaveAs(filePath);
            }
            sw.Stop();
            Console.WriteLine($"Save used {sw.ElapsedMilliseconds} ms");
        }
    }
}
  • 耗時(shí)及內(nèi)存占用


    image.png

    image.png

    內(nèi)存占用 3000 MB左右裙盾,耗時(shí) 100 秒左右,生成的文件大小 23 MB


    image.png

2他嫡、每次導(dǎo)入8000筆數(shù)據(jù)番官,保存,再打開文件追加

        public void DoTest_GenerateThenAttach()
        {
            var gdt = new GenerateDataTable();
            var table = gdt.GetNewTable(1);

            Stopwatch sw = new Stopwatch();
            sw.Start();
            var filePath = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss") + ".xlsx");
            int looRowCount = 8000;
            using (var workbook = new XLWorkbook())
            {
                workbook.Worksheets.Add(gdt.GetTableWithNRows(table, looRowCount), table.TableName);
                workbook.SaveAs(filePath);
            }

            int i = 2;
            int loop = 300000 / looRowCount;
            while (i <= loop)
            {
                using (var workbook = new XLWorkbook(filePath))
                {
                    IXLWorksheet Worksheet = workbook.Worksheet(table.TableName);
                    int NumberOfLastRow = Worksheet.LastRowUsed().RowNumber();
                    IXLCell CellForNewData = Worksheet.Cell(NumberOfLastRow + 1, 1);
                    CellForNewData.InsertTable(gdt.GetTableWithNRows(table, looRowCount));
                    if (i == loop)
                    {
                        Worksheet.Columns().AdjustToContents();
                    }
                    workbook.Save();
                    Console.WriteLine($"Loop {i} work done...");
                    i++;
                }

                GC.Collect();
                GC.WaitForPendingFinalizers();
                GC.Collect();
            }

            sw.Stop();
            Console.WriteLine($"Save all rows used {sw.ElapsedMilliseconds} ms");
        }

        public DataTable GetTableWithNRows(DataTable dataIn, int rowCount)
        {
            DataTable data = dataIn.Clone();
            for (var i = 0; i < rowCount; i++)
            {
                data.Rows.Add(
                    "xxxxxxxxx",
                    "xxxxxxxxx",
                    "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
                    "*USRSPC",
                    "xxxxxxxxx",
                    "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
                    DateTime.Now,
                    "789",
                    "16384",
                    DateTime.Now.AddDays(120),
                    "GRC",
                    "0",
                    "0"
                );
            }

            return data;
        }
  • 但是這個(gè)運(yùn)行非常慢钢属,耗時(shí)嚴(yán)重徘熔,而且越往后讀取excel后內(nèi)存占用還是會(huì)很大


    image.png

二、使用 OpenXML

image.png

1淆党、使用高級(jí)API

        public void DoTest()
        {
            var gdt = new GenerateDataTable();
            var table = gdt.GetNewTable();

            Stopwatch sw = new Stopwatch();
            sw.Start();
            var filename = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss") + ".xlsx");
            WriteExcelFile(filename, table);
            sw.Stop();
            Console.WriteLine($"Save used {sw.ElapsedMilliseconds} ms");
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();
        }

        private static void WriteExcelFile(string filename, DataTable table)
        {
            using (SpreadsheetDocument document = SpreadsheetDocument.Create(filename, SpreadsheetDocumentType.Workbook))
            {
                WorkbookPart workbookPart = document.AddWorkbookPart();
                workbookPart.Workbook = new Workbook();

                WorksheetPart worksheetPart = workbookPart.AddNewPart<WorksheetPart>();
                var sheetData = new SheetData();
                worksheetPart.Worksheet = new Worksheet(sheetData);

                Sheets sheets = workbookPart.Workbook.AppendChild(new Sheets());
                Sheet sheet = new Sheet() { Id = workbookPart.GetIdOfPart(worksheetPart), SheetId = 1, Name = table.TableName };

                sheets.Append(sheet);

                Row headerRow = new Row();

                List<String> columns = new List<string>();
                foreach (System.Data.DataColumn column in table.Columns)
                {
                    columns.Add(column.ColumnName);

                    Cell cell = new Cell();
                    cell.DataType = CellValues.String;
                    cell.CellValue = new CellValue(column.ColumnName);
                    headerRow.AppendChild(cell);
                }

                sheetData.AppendChild(headerRow);

                foreach (DataRow dsrow in table.Rows)
                {
                    Row newRow = new Row();
                    foreach (String col in columns)
                    {
                        Cell cell = new Cell();
                        cell.DataType = CellValues.String;
                        cell.CellValue = new CellValue(dsrow[col].ToString());
                        newRow.AppendChild(cell);
                    }

                    sheetData.AppendChild(newRow);
                }

                workbookPart.Workbook.Save();
            }
        }
  • 內(nèi)存占用峰值情況【1300MB】


    image.png
  • 耗時(shí)【22秒】


    image.png
  • 生成文件大小【2.33MB】


    image.png

2酷师、使用OpenXmlWriter

參考文章

        public void DoTest2()
        {
            var gdt = new GenerateDataTable();
            var table = gdt.GetNewTable();

            Stopwatch sw = new Stopwatch();
            sw.Start();
            var filename = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss") + ".xlsx");
            LargeExport(filename, table);
            sw.Stop();
            Console.WriteLine($"Save used {sw.ElapsedMilliseconds} ms");
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();
        }
        public static void LargeExport(string filename, DataTable table)
        {
            using (SpreadsheetDocument document = SpreadsheetDocument.Create(filename, SpreadsheetDocumentType.Workbook))
            {
                //this list of attributes will be used when writing a start element
                List<OpenXmlAttribute> attributes;
                OpenXmlWriter writer;

                document.AddWorkbookPart();
                WorksheetPart workSheetPart = document.WorkbookPart.AddNewPart<WorksheetPart>();

                writer = OpenXmlWriter.Create(workSheetPart);
                writer.WriteStartElement(new Worksheet());
                writer.WriteStartElement(new SheetData());

                // 表頭列信息
                //create a new list of attributes
                attributes = new List<OpenXmlAttribute>();
                // add the row index attribute to the list
                attributes.Add(new OpenXmlAttribute("r", null, "1"));
                //write the row start element with the row index attribute
                writer.WriteStartElement(new Row(), attributes);
                for (int columnNum = 0; columnNum < table.Columns.Count; ++columnNum)
                {
                    //reset the list of attributes
                    attributes = new List<OpenXmlAttribute>();
                    // add data type attribute - in this case inline string (you might want to look at the shared strings table)
                    attributes.Add(new OpenXmlAttribute("t", null, "str"));
                    //add the cell reference attribute
                    attributes.Add(new OpenXmlAttribute("r", "", $"{GetColumnName(columnNum+1)}1"));

                    //write the cell start element with the type and reference attributes
                    writer.WriteStartElement(new Cell(), attributes);
                    //write the cell value
                    writer.WriteElement(new CellValue(table.Columns[columnNum].ColumnName));

                    // write the end cell element
                    writer.WriteEndElement();
                }
                // write the end row element
                writer.WriteEndElement();

                for (int rowNum = 1; rowNum <= table.Rows.Count; ++rowNum)
                {
                    int docRowNum = rowNum + 1;
                    //create a new list of attributes
                    attributes = new List<OpenXmlAttribute>();
                    // add the row index attribute to the list
                    attributes.Add(new OpenXmlAttribute("r", null, docRowNum.ToString()));

                    //write the row start element with the row index attribute
                    writer.WriteStartElement(new Row(), attributes);

                    for (int columnNum = 1; columnNum <= table.Columns.Count; ++columnNum)
                    {
                        //reset the list of attributes
                        attributes = new List<OpenXmlAttribute>();
                        // add data type attribute - in this case inline string (you might want to look at the shared strings table)
                        attributes.Add(new OpenXmlAttribute("t", null, "str"));
                        //add the cell reference attribute
                        attributes.Add(new OpenXmlAttribute("r", "", $"{GetColumnName(columnNum)}{docRowNum}"));

                        //write the cell start element with the type and reference attributes
                        writer.WriteStartElement(new Cell(), attributes);
                        var cellValue = table.Rows[rowNum - 1][columnNum - 1];
                        string cellStr = cellValue == null ? "" : (cellValue is DateTime?((DateTime)cellValue).ToString("yyyy-MM-dd HH:mm:ss.fff") : cellValue.ToString());
                        //write the cell value
                        writer.WriteElement(new CellValue(cellStr));

                        // write the end cell element
                        writer.WriteEndElement();
                    }

                    // write the end row element
                    writer.WriteEndElement();
                }

                // write the end SheetData element
                writer.WriteEndElement();
                // write the end Worksheet element
                writer.WriteEndElement();
                writer.Close();

                writer = OpenXmlWriter.Create(document.WorkbookPart);
                writer.WriteStartElement(new Workbook());
                writer.WriteStartElement(new Sheets());

                writer.WriteElement(new Sheet()
                {
                    Name = table.TableName,
                    SheetId = 1,
                    Id = document.WorkbookPart.GetIdOfPart(workSheetPart)
                });

                // End Sheets
                writer.WriteEndElement();
                // End Workbook
                writer.WriteEndElement();

                writer.Close();

                document.Close();
            }
        }

        //A simple helper to get the column name from the column index. This is not well tested!
        private static string GetColumnName(int columnIndex)
        {
            int dividend = columnIndex;
            string columnName = String.Empty;
            int modifier;

            while (dividend > 0)
            {
                modifier = (dividend - 1) % 26;
                columnName = Convert.ToChar(65 + modifier).ToString() + columnName;
                dividend = (int)((dividend - modifier) / 26);
            }

            return columnName;
        }
  • 內(nèi)存占用【不超過 190MB】


    image.png
  • 耗時(shí)【17 秒】


    image.png
  • 生成的文件大小【26 MB】


    image.png

將數(shù)據(jù)量增大到100萬

內(nèi)存占用不超過【260 MB】,耗時(shí)【30秒】染乌,生成文件大小【48.1MB】


image.png

image.png

三山孔、使用 npoi

        public void DoTest()
        {
            var gdt = new GenerateDataTable();
            var table = gdt.GetNewTable();

            Stopwatch sw = new Stopwatch();
            sw.Start();
            var filename = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss") + ".xlsx");
            ExportDataTableToExcel(table, filename);
            sw.Stop();
            Console.WriteLine($"Save used {sw.ElapsedMilliseconds} ms");
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();
        }

        public static Tuple<bool, string> ExportDataTableToExcel(DataTable dt, string saveTopath)
        {
            bool result = false;
            string message = "";
            IWorkbook workbook = null;
            FileStream fs = null;
            IRow row = null;
            ISheet sheet = null;
            ICell cell = null;
            try
            {
                if (dt != null && dt.Rows.Count > 0)
                {
                    if (saveTopath.IndexOf(".xlsx", StringComparison.Ordinal) > 0) // 2007版本
                        workbook = new XSSFWorkbook();
                    else //if (saveTopath.IndexOf(".xls", StringComparison.Ordinal) > 0) // 2003版本
                        workbook = new HSSFWorkbook();
                    sheet = workbook.CreateSheet(dt.TableName);
                    int rowCount = dt.Rows.Count;//行數(shù)  
                    int columnCount = dt.Columns.Count;//列數(shù)  

                    //設(shè)置列頭  
                    row = sheet.CreateRow(0);//excel第一行設(shè)為列頭  
                    for (int c = 0; c < columnCount; c++)
                    {
                        cell = row.CreateCell(c);
                        cell.SetCellValue(dt.Columns[c].ColumnName);
                    }

                    //設(shè)置每行每列的單元格,  
                    for (int i = 0; i < rowCount; i++)
                    {
                        row = sheet.CreateRow(i + 1);
                        for (int j = 0; j < columnCount; j++)
                        {
                            cell = row.CreateCell(j);//excel第二行開始寫入數(shù)據(jù)  
                            cell.SetCellValue(dt.Rows[i][j].ToString());
                        }
                    }
                    using (fs = File.OpenWrite(saveTopath))
                    {
                        workbook.Write(fs);//向打開的這個(gè)xls文件中寫入數(shù)據(jù)  
                        result = true;
                    }
                }
                else
                {
                    message = "沒有解析到數(shù)據(jù)!";
                }
                return new Tuple<bool, string>(result, message);
            }
            catch (Exception ex)
            {
                if (fs != null)
                {
                    fs.Close();
                }
                return new Tuple<bool, string>(false, ex.Message);
            }
        }
  • 內(nèi)存占用【2600MB】


    image.png
  • 耗時(shí)【50 秒】

  • 生成的文件大小【29 MB】


    image.png

四慕匠、結(jié)論

顯然饱须,在只考慮生成文件的情況下,應(yīng)該選擇使用OpenXmlWriter的方式

代碼

GitHub Page-LargeDataExportExcel

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末台谊,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子譬挚,更是在濱河造成了極大的恐慌锅铅,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,198評(píng)論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件减宣,死亡現(xiàn)場離奇詭異盐须,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)漆腌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門贼邓,熙熙樓的掌柜王于貴愁眉苦臉地迎上來阶冈,“玉大人,你說我怎么就攤上這事塑径∨樱” “怎么了?”我有些...
    開封第一講書人閱讀 167,643評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵统舀,是天一觀的道長匆骗。 經(jīng)常有香客問我,道長誉简,這世上最難降的妖魔是什么碉就? 我笑而不...
    開封第一講書人閱讀 59,495評(píng)論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮闷串,結(jié)果婚禮上瓮钥,老公的妹妹穿的比我還像新娘。我一直安慰自己烹吵,他們只是感情好骏庸,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,502評(píng)論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著年叮,像睡著了一般具被。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上只损,一...
    開封第一講書人閱讀 52,156評(píng)論 1 308
  • 那天一姿,我揣著相機(jī)與錄音,去河邊找鬼跃惫。 笑死叮叹,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的爆存。 我是一名探鬼主播蛉顽,決...
    沈念sama閱讀 40,743評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼先较!你這毒婦竟也來了携冤?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,659評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤闲勺,失蹤者是張志新(化名)和其女友劉穎曾棕,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體菜循,經(jīng)...
    沈念sama閱讀 46,200評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡翘地,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,282評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片衙耕。...
    茶點(diǎn)故事閱讀 40,424評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡昧穿,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出橙喘,到底是詐尸還是另有隱情时鸵,我是刑警寧澤,帶...
    沈念sama閱讀 36,107評(píng)論 5 349
  • 正文 年R本政府宣布渴杆,位于F島的核電站寥枝,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏磁奖。R本人自食惡果不足惜囊拜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,789評(píng)論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望比搭。 院中可真熱鬧冠跷,春花似錦、人聲如沸身诺。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評(píng)論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽霉赡。三九已至橄务,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間穴亏,已是汗流浹背蜂挪。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評(píng)論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留嗓化,地道東北人棠涮。 一個(gè)月前我還...
    沈念sama閱讀 48,798評(píng)論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像刺覆,于是被迫代替她去往敵國和親严肪。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,435評(píng)論 2 359