1. 程式人生 > >c# NPOI 匯出23萬條記錄耗時12秒

c# NPOI 匯出23萬條記錄耗時12秒

 

 

先上測試程式碼:

            string connectionString = "Server=localhost;Initial Catalog=******;User ID=sa;Password=******;";
            List<TestData> datas = null;
            using (SqlConnection db = new SqlConnection(connectionString))
            {
                datas = db.Query<TestData>("SELECT * FROM TestData").ToList();
            }

            System.Console.WriteLine($"資料來源物件 {typeof(TestData).GetProperties().Length} 個欄位,共 {datas.Count} 條記錄,大小 {BinarySerializeHelper.SerializeToBytes(datas).Length/1000/1000} M");
            Task.Run(() =>
            {
                while (true)
                {
                    System.Console.WriteLine($"{DateTime.Now} 記憶體 : {GC.GetTotalMemory(false) / 1000 / 1000} M");
                    Thread.Sleep(1000);
                }
            });
            Stopwatch sw = new Stopwatch();
            sw.Start();
            byte[] bytes = ExcelHandlerFactory.CreateHandler(datas).CreateExcelBytes();
            sw.Stop();
            System.Console.WriteLine($"{DateTime.Now} 資料來源轉Excel檔案位元組陣列耗時 : "+sw.ElapsedMilliseconds / 1000 +" 秒");
            string path = @"C:\Users\Administrator\Desktop\1.xlsx";
            FileStream fs = new FileStream(path, FileMode.OpenOrCreate, FileAccess.Write);
            fs.Write(bytes);
            fs.Dispose();
            System.Console.ReadKey();

 

測試結果:

 

就是這記憶體佔用有點高......

原始碼:

using System.Collections.Generic;

namespace Wjire.Excel
{

    /// <summary>
    /// ExcelHandler工廠
    /// </summary>
    public static class ExcelHandlerFactory
    {

        /// <summary>
        /// 建立ExcelHandler
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="sources">資料來源</param>
        /// <param name="choosedFields">需要匯出的欄位,可不傳,則匯出所有欄位</param>
        /// <returns></returns>
        public static ExcelHandler<T> CreateHandler<T>(IEnumerable<T> sources, HashSet<string> choosedFields = null)
        {
            return new ExcelHandler<T>(sources, choosedFields);
        }
    }
}
View Code
using NPOI.HSSF.UserModel;
using NPOI.SS.UserModel;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace Wjire.Excel
{

    /// <summary>
    /// 報表匯出處理者
    /// </summary>
    public sealed class ExcelHandler<TSource> 
    {

        /// <summary>
        /// 資料來源
        /// </summary>
        private readonly IEnumerable<TSource> _sources;


        /// <summary>
        /// 需要匯出的列資訊
        /// </summary>
        private readonly ColumnInfo[] _columnInfos;


        /// <summary>
        /// 工作簿
        /// </summary>
        private IWorkbook _workbook;


        /// <summary>
        /// 工作頁
        /// </summary>
        private ISheet _sheet;


        /// <summary>
        /// 單元格樣式
        /// </summary>
        private ICellStyle _cellStyle;


        /// <summary>
        /// 單元格樣式提供器
        /// </summary>
        private ICellStyleProvider _provider;


        internal ExcelHandler(IEnumerable<TSource> sources, HashSet<string> choosedFields)
        {
            _sources = sources;
            _columnInfos = GetColumnInfosOfExport(choosedFields);
        }


        /// <summary>
        /// 資料來源轉位元組
        /// </summary>
        /// <returns></returns>
        public byte[] CreateExcelBytes()
        {
            using (var ms = CreateExcelStream())
            {
                return ms.ToArray();
            }
        }


        /// <summary>
        /// 資料來源轉excel流
        /// </summary>
        /// <returns></returns>
        public MemoryStream CreateExcelStream()
        {
            try
            {
                _workbook = new HSSFWorkbook();
                _cellStyle = (_provider ?? DefaultCellStyleProvider.Singleton.Value).CreateCellStyle(_workbook);
                int sheetIndex = 1;
                CreateSheetWithHeader(sheetIndex);

                int rowIndex = 1;
                foreach (TSource entity in _sources)
                {
                    //03版 excel 一個 _sheet 最多 65535 行
                    if (rowIndex == 65535)
                    {
                        sheetIndex++;
                        CreateSheetWithHeader(sheetIndex);
                        rowIndex = 1;
                    }
                    CreateDataRow(rowIndex, entity);
                    rowIndex++;
                }

                MemoryStream ms = new MemoryStream();
                _workbook.Write(ms);
                return ms;
            }
            finally
            {
                _workbook?.Close();
            }
        }



        /// <summary>
        /// 建立Sheet及列頭
        /// </summary>
        private void CreateSheetWithHeader(int sheetIndex)
        {
            _sheet = _workbook.CreateSheet("第 " + sheetIndex + " 頁");
            //凍結首行首列
            _sheet.CreateFreezePane(0, 1);

            IRow header = _sheet.CreateRow(0);
            for (int i = 0; i < _columnInfos.Length; i++)
            {
                ICell cell = header.CreateCell(i);
                cell.SetCellValue(_columnInfos[i].CellDisplayAttribute.Name);
                cell.CellStyle = _cellStyle;
                //自適應寬度
                _sheet.AutoSizeColumn(i);
            }
        }


        /// <summary>
        /// 建立資料行
        /// </summary>
        /// <param name="rowIndex">行索引</param>
        /// <param name="entity">資料</param>
        private void CreateDataRow(int rowIndex, object entity)
        {
            IRow dataRow = _sheet.CreateRow(rowIndex);
            for (int i = 0; i < _columnInfos.Length; i++)
            {
                ICell cell = dataRow.CreateCell(i);
                cell.CellStyle = _cellStyle;
                object value = _columnInfos[i].PropertyInfo.GetValue(entity, null);
                SetCellValue(value, cell);
            }
        }


        /// <summary>
        /// 設定單元格值
        /// </summary>
        /// <param name="value"></param>
        /// <param name="cell"></param>
        private void SetCellValue(object value, ICell cell)
        {
            if (value == null)
            {
                cell.SetCellValue(string.Empty);
                return;
            }
            Type type = value.GetType();
            switch (type.Name)
            {
                case "DateTime":
                case "String":
                case "Boolean":
                    cell.SetCellValue(value.ToString());
                    break;
                case "Byte":
                case "Int16":
                case "Int32":
                case "Int64":
                case "Single":
                case "Double":
                case "Decimal":
                    cell.SetCellValue(Convert.ToDouble(value));
                    break;
                default:
                    cell.SetCellValue(string.Empty);
                    break;
            }
        }




        /// <summary>
        /// 設定excel單元格樣式提供器
        /// </summary>
        /// <param name="provider"></param>
        /// <returns></returns>
        public ExcelHandler<TSource> SetCellStyleProvider(ICellStyleProvider provider)
        {
            _provider = provider;
            return this;
        }



        /// <summary>
        /// 獲取需要匯出的列資訊
        /// </summary>
        /// <param name="choosedFields"></param>
        /// <returns></returns>
        private ColumnInfo[] GetColumnInfosOfExport(HashSet<string> choosedFields)
        {
            ColumnInfo[] columnInfos = ColumnInfoContainer.GetColumnInfo(typeof(TSource));
            return choosedFields?.Count > 0
                ? columnInfos.Where(w => choosedFields.Contains(w.PropertyInfo.Name)).ToArray()
                : columnInfos;
        }
    }
}
View Code
using System.Reflection;

namespace Wjire.Excel
{

    /// <summary>
    /// 列資訊
    /// </summary>
    public class ColumnInfo
    {
        internal PropertyInfo PropertyInfo { get; set; }

        internal CellDisplayAttribute CellDisplayAttribute { get; set; }

    }
}
View Code
using System;


namespace Wjire.Excel
{

    /// <summary>
    /// excel 單元格資料顯示自定義特性類
    /// </summary>
    [AttributeUsage(AttributeTargets.Property)]
    public sealed class CellDisplayAttribute : Attribute
    {

        /// <summary>
        /// 自定義列名
        /// </summary>
        public string Name { get; set; }
        

        /// <summary>
        /// 建構函式
        /// </summary>
        /// <param name="name">自定義列名</param>
        public CellDisplayAttribute(string name)
        {
            Name = name;
        }
    }
}
View Code
using NPOI.SS.UserModel;

namespace Wjire.Excel
{

    /// <summary>
    /// 單元格樣式提供器介面
    /// </summary>
    public interface ICellStyleProvider
    {
        ICellStyle CreateCellStyle(IWorkbook workbook);
    }
}
View Code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;


namespace Wjire.Excel
{

    /// <summary>
    /// 資料來源列資訊容器
    /// </summary>
    internal static class ColumnInfoContainer
    {

        private static readonly Dictionary<Type, ColumnInfo[]> Container = new Dictionary<Type, ColumnInfo[]>();


        /// <summary>
        /// 獲取資料來源列資訊
        /// </summary>
        /// <param name="sourceType">資料來源類型別</param>
        /// <returns></returns>
        internal static ColumnInfo[] GetColumnInfo(Type sourceType)
        {
            if (Container.TryGetValue(sourceType, out ColumnInfo[] infos))
            {
                return infos;
            }

            infos = sourceType
                .GetProperties(BindingFlags.Public | BindingFlags.Instance)
                .Where(propertyInfo => propertyInfo.GetCustomAttribute<CellDisplayAttribute>(true) != null)
                .Select(propertyInfo => new ColumnInfo
                {
                    PropertyInfo = propertyInfo,
                    CellDisplayAttribute = propertyInfo.GetCustomAttribute<CellDisplayAttribute>()
                }).ToArray();
            Container.Add(sourceType, infos);
            return infos;
        }
    }
}
View Code
using NPOI.SS.UserModel;
using System;

namespace Wjire.Excel
{

    /// <summary>
    /// 預設單元格樣式提供器
    /// </summary>
    internal class DefaultCellStyleProvider : ICellStyleProvider
    {

        internal static Lazy<DefaultCellStyleProvider> Singleton = new Lazy<DefaultCellStyleProvider>(() => new DefaultCellStyleProvider());

        private DefaultCellStyleProvider()
        {

        }


        /// <summary>
        /// 建立單元格樣式
        /// </summary>
        /// <param name="workbook"></param>
        /// <returns></returns>
        public ICellStyle CreateCellStyle(IWorkbook workbook)
        {
            ICellStyle cellStyle = workbook.CreateCellStyle();
            cellStyle.Alignment = HorizontalAlignment.Center;
            //cellStyle.VerticalAlignment = VerticalAlignment.Center;//垂直居中非常影響效率,不建議開啟該功能
            IFont font = workbook.CreateFont();
            font.FontHeightInPoints = 11;
            //font.Boldweight = 700;
            cellStyle.SetFont(font);

            //邊框
            //cellStyle.BorderBottom = BorderStyle.Thin;
            //cellStyle.BorderLeft = BorderStyle.Thin;
            //cellStyle.BorderRight = BorderStyle.Thin;
            //cellStyle.BorderTop = BorderStyle.Thin;

            return cellStyle;
        }
    }
}
View Code

 

幾點說明:

1.NPOI 用的最新版本:2.4.1;

2.程式碼中用的 HSSFWorkbook ,不僅僅是為了相容 word2003,在測試的時候發現,如果用 XSSFWorkbook ,耗時慢了N個數量級,不知道是不是哪裡姿勢不對;

3.單元格的寬度只在標題欄設定了,所以匯出來的Excel可能比較醜.原因是:

  1)如果根據單元格內容的長度來調整的話,由於每一個單元格內容的長度都肯能不一樣,太耗時,沒必要,不如滑鼠點兩下來得快;

  2)雖然NPOI有個功能可以在一個sheet的資料填充完後,設定單元格的寬度自適應,但是測試了下,太太太太慢了.估計是在遍歷所有的單元格,一個一個計算;

  3)還有一個折中的辦法,就是根據第一行資料的各個單元格內容來調整寬度,因為有些時候,資料物件每個屬性的值的長度都不會差太多.但是當長度不一的時候,會讓人誤以為那些長的單元格的內容已經顯示完了,所以也捨棄了這個功能.

4.測試的時候發現,如果把某一列的單元格設定成超連結,點選可以開啟瀏覽器檢視那種,非常非常非常非常慢.慘不忍睹.不知道是不是姿勢不對,所以也捨棄了該功