对于EXCEL的导入、导出,我之前已分享过多次,比如:
第一种方案:《我写的一个ExcelHelper通用类,可用于读取或生成数据》这个主要是利用把EXCEL当成一个DB来进行获取数据,导出则是采用拼接成HTML TABLE的方式,这种在ASP.NET,ASP.NET CORE 中也是可以的,简单方便,依赖Office Excel组件,仅适用于网站服务端。 推荐指数:♥♥♥
第二种方案:《MVC导出数据到EXCEL新方法:将视图或分部视图转换为HTML后再直接返回FileResult》这个主要是实现导出EXCEL的方法,依赖MVC框架,利用视图引擎解析渲染View(view也主要是HTML表格模板)获得HTML TABLE,本质与第一种的导出相同,只是这里不用代码去拼接HTML TABLE而是写View HTML模板即可,简单方便,适用于ASP.NET MVC、ASP.NET MVC CORE; 推荐指数:♥♥♥♥
第三种方案:《NPOI导入导出EXCEL通用类,供参考,可直接使用在WinForm项目中》这个实现了EXCEL的导入、导出方法,依赖NPOI,优点是:无需安装EXCEL也能操作EXCEL,文中的实现方案及示例主要是用在C/S中,对B/S 支持不够友好,当然可以进行适当修改就能兼容WEB版。推荐指数:♥♥♥♥
第四种方案:《分享我基于NPOI+ExcelReport实现的导入与导出EXCEL类库》这个完美的灵活实现了EXCEL的各种导入、导出方法,支持基于模板导出,格式自定义等 特点,依赖:ExcelReport、NPOI、NPOI.Extend,支持C/S、B/S(C/S特有的方法除外),适用于企业级EXCEL操作比较复杂的场景:ExcelUtility,推荐指数:♥♥♥♥♥,git地址:https://gitee.com/zuowj/ExcelUtility
第五种方案:就是本文即将介绍的,依赖NPOI,实现EXCEL的导入、导出、上传等常见方法,充分借鉴第三种、第四种方案,抽出核心的实现方法,并进行适当改造,以满足绝大部份的EXCEL场景,轻量、简单、易用,支持链式操作,类库名:ExcelEasyUtil,故名思义:EXCEL简单实用工具类库。
该类库主要是基于NPOI的IWorkbook进行扩展,扩展方法实现了常见的几种导出EXCEL、导入EXCEL数据的方法:基于类型集合,基于DataTable,NPOIExtensions完整实现代码如下:
using Newtonsoft.Json.Linq; using NPOI.HSSF.UserModel; using NPOI.SS.UserModel; using NPOI.XSSF.UserModel; using System; using System.Collections.Generic; using System.Data; using System.IO; using System.Linq.Expressions; using System.Net.Http; using System.Net.Http.Headers; using System.Reflection; using System.Text; namespace ExcelEasyUtil { ////// NPOI扩展类 /// author:zuowenjun /// 2019-5-21 /// public static class NPOIExtensions { ////// 将一个实体数据对象填充到一个EXCEL工作表中(可连续填充多个sheet,如:FillSheet(...).FillSheet(..) ) /// ////// /// /// /// /// /// public static IWorkbook FillSheet (this IWorkbook book, string sheetName, IList excelData, IList headerColNames, Func > getCellValuesFunc, IDictionary colDataFormats = null) where T : class { var sheet = book.CreateSheet(sheetName); IRow rowHeader = sheet.CreateRow(0); var headerCellStyle = GetCellStyle(book, true); Dictionary colStyles = new Dictionary (); List colTypes = new List (); Type strType = typeof(string); for (int i = 0; i < headerColNames.Count; i++) { ICell headerCell = rowHeader.CreateCell(i); headerCell.CellStyle = headerCellStyle; string colName = headerColNames[i]; if (colName.Contains(":")) { var colInfos = colName.Split(':'); colName = colInfos[0]; colTypes.Add(GetColType(colInfos[1])); } else { colTypes.Add(strType); } headerCell.SetCellValue(colName); if (colDataFormats != null && colDataFormats.ContainsKey(colName)) { colStyles[i] = GetCellStyleWithDataFormat(book, colDataFormats[colName]); } else { colStyles[i] = GetCellStyle(book); } } //生成excel内容 for (int i = 0; i < excelData.Count; i++) { IRow irow = sheet.CreateRow(i + 1); var row = excelData[i]; var cellValues = getCellValuesFunc(row); for (int j = 0; j < headerColNames.Count; j++) { ICell cell = irow.CreateCell(j); string cellValue = string.Empty; if (cellValues.Count - 1 >= j && cellValues[j] != null) { cellValue = cellValues[j].ToString(); } SetCellValue(cell, cellValue, colTypes[j], colStyles); } } return book; } /// /// 将一个实体数据对象填充到一个EXCEL工作表中(可连续填充多个sheet,如:FillSheet(...).FillSheet(..) ) /// ////// /// /// /// /// public static IWorkbook FillSheet (this IWorkbook book, string sheetName, IList excelData, IDictionary >> colMaps, IDictionary colDataFormats = null ) where T : class { var sheet = book.CreateSheet(sheetName); var headerColNames = new List (); var propInfos = new List (); foreach (var item in colMaps) { headerColNames.Add(item.Key); var propInfo = GetPropertyInfo(item.Value); propInfos.Add(propInfo); } var headerCellStyle = GetCellStyle(book, true); Dictionary colStyles = new Dictionary (); IRow rowHeader = sheet.CreateRow(0); for (int i = 0; i < headerColNames.Count; i++) { ICell headerCell = rowHeader.CreateCell(i); headerCell.CellStyle = headerCellStyle; headerCell.SetCellValue(headerColNames[i]); if (colDataFormats != null && colDataFormats.ContainsKey(headerColNames[i])) { colStyles[i] = GetCellStyleWithDataFormat(book, colDataFormats[headerColNames[i]]); } else { colStyles[i] = GetCellStyle(book); } } //生成excel内容 for (int i = 0; i < excelData.Count; i++) { IRow irow = sheet.CreateRow(i + 1); var row = excelData[i]; for (int j = 0; j < headerColNames.Count; j++) { var prop = propInfos[j]; ICell cell = irow.CreateCell(j); string cellValue = Convert.ToString(propInfos[j].GetValue(row, null)); SetCellValue(cell, cellValue, prop.PropertyType, colStyles); } } return book; } /// /// 将一个数据表(DataTable)对象填充到一个EXCEL工作表中(可连续填充多个sheet,如:FillSheet(...).FillSheet(..) ) /// /// /// /// /// /// ///public static IWorkbook FillSheet(this IWorkbook book, string sheetName, DataTable excelData, IDictionary colMaps, IDictionary colDataFormats = null) { if (excelData.Rows.Count <= 0) return book; var sheet = book.CreateSheet(sheetName); var headerCellStyle = GetCellStyle(book, true); Dictionary colStyles = new Dictionary (); IRow rowHeader = sheet.CreateRow(0); int nIndex = 0; var headerColNames = new List (); foreach (var item in colMaps) { ICell headerCell = rowHeader.CreateCell(nIndex); headerCell.SetCellValue(item.Value); headerCell.CellStyle = headerCellStyle; if (colDataFormats != null && colDataFormats.ContainsKey(item.Value)) { colStyles[nIndex] = GetCellStyleWithDataFormat(book, colDataFormats[item.Value]); } else { colStyles[nIndex] = GetCellStyle(book); } headerColNames.Add(item.Key); nIndex++; } //生成excel内容 for (int i = 0; i < excelData.Rows.Count; i++) { IRow irow = sheet.CreateRow(i + 1); var row = excelData.Rows[i]; for (int j = 0; j < headerColNames.Count; j++) { ICell cell = irow.CreateCell(j); string colName = headerColNames[j]; string cellValue = row[colName].ToNotNullString(); SetCellValue(cell, cellValue, excelData.Columns[colName].DataType, colStyles); } } return book; } private static PropertyInfo GetPropertyInfo (Expression > select) { var body = select.Body; if (body.NodeType == ExpressionType.Convert) { var o = (body as UnaryExpression).Operand; return (o as MemberExpression).Member as PropertyInfo; } else if (body.NodeType == ExpressionType.MemberAccess) { return (body as MemberExpression).Member as PropertyInfo; } return null; } private static Type GetColType(string colTypeSimpleName) { colTypeSimpleName = colTypeSimpleName.ToUpper(); switch (colTypeSimpleName) { case "DT": { return typeof(DateTime); } case "BL": { return typeof(Boolean); } case "NUM": { return typeof(Int64); } case "DEC": { return typeof(Decimal); } default: { return typeof(String); } } } private static void SetCellValue(ICell cell, string value, Type colType, IDictionary colStyles) { if (colType.IsNullableType()) { colType = colType.GetGenericArguments()[0]; } string dataFormatStr = null; switch (colType.ToString()) { case "System.String": //字符串类型 cell.SetCellType(CellType.String); cell.SetCellValue(value); break; case "System.DateTime": //日期类型 DateTime dateV = new DateTime(); if (DateTime.TryParse(value, out dateV)) { cell.SetCellValue(dateV); } else { cell.SetCellValue(value); } dataFormatStr = "yyyy/mm/dd hh:mm:ss"; break; case "System.Boolean": //布尔型 bool boolV = false; if (bool.TryParse(value, out boolV)) { cell.SetCellType(CellType.Boolean); cell.SetCellValue(boolV); } else { cell.SetCellValue(value); } break; case "System.Int16": //整型 case "System.Int32": case "System.Int64": case "System.Byte": int intV = 0; if (int.TryParse(value, out intV)) { cell.SetCellType(CellType.Numeric); cell.SetCellValue(intV); } else { cell.SetCellValue(value); } dataFormatStr = "0"; break; case "System.Decimal": //浮点型 case "System.Double": double doubV = 0; if (double.TryParse(value, out doubV)) { cell.SetCellType(CellType.Numeric); cell.SetCellValue(doubV); } else { cell.SetCellValue(value); } dataFormatStr = "0.00"; break; case "System.DBNull": //空值处理 cell.SetCellType(CellType.Blank); cell.SetCellValue(string.Empty); break; default: cell.SetCellType(CellType.Unknown); cell.SetCellValue(value); break; } if (!string.IsNullOrEmpty(dataFormatStr) && colStyles[cell.ColumnIndex].DataFormat <= 0) //没有设置,则采用默认类型格式 { colStyles[cell.ColumnIndex] = GetCellStyleWithDataFormat(cell.Sheet.Workbook, dataFormatStr); } cell.CellStyle = colStyles[cell.ColumnIndex]; ReSizeColumnWidth(cell.Sheet, cell); } private static ICellStyle GetCellStyleWithDataFormat(IWorkbook workbook, string format) { ICellStyle style = GetCellStyle(workbook); var dataFormat = workbook.CreateDataFormat(); short formatId = -1; if (dataFormat is HSSFDataFormat) { formatId = HSSFDataFormat.GetBuiltinFormat(format); } if (formatId != -1) { style.DataFormat = formatId; } else { style.DataFormat = dataFormat.GetFormat(format); } return style; } private static ICellStyle GetCellStyle(IWorkbook workbook, bool isHeaderRow = false) { ICellStyle style = workbook.CreateCellStyle(); if (isHeaderRow) { style.FillPattern = FillPattern.SolidForeground; style.FillForegroundColor = NPOI.HSSF.Util.HSSFColor.Grey25Percent.Index; IFont f = workbook.CreateFont(); f.FontHeightInPoints = 11D; f.Boldweight = (short)FontBoldWeight.Bold; style.SetFont(f); } style.BorderBottom = NPOI.SS.UserModel.BorderStyle.Thin; style.BorderLeft = NPOI.SS.UserModel.BorderStyle.Thin; style.BorderRight = NPOI.SS.UserModel.BorderStyle.Thin; style.BorderTop = NPOI.SS.UserModel.BorderStyle.Thin; return style; } private static void ReSizeColumnWidth(ISheet sheet, ICell cell) { int cellLength = (Encoding.Default.GetBytes(cell.ToString()).Length + 2) * 256; const int maxLength = 60 * 256; //255 * 256; if (cellLength > maxLength) //当单元格内容超过30个中文字符(英语60个字符)宽度,则强制换行 { cellLength = maxLength; cell.CellStyle.WrapText = true; } int colWidth = sheet.GetColumnWidth(cell.ColumnIndex); if (colWidth < cellLength) { sheet.SetColumnWidth(cell.ColumnIndex, cellLength); } } private static ISheet GetSheet(IWorkbook workbook, string sheetIndexOrName) { int sheetIndex = 0; ISheet sheet = null; if (int.TryParse(sheetIndexOrName, out sheetIndex)) { sheet = workbook.GetSheetAt(sheetIndex); } else { sheet = workbook.GetSheet(sheetIndexOrName); } return sheet; } /// /// 从工作表中解析生成DataTable /// /// /// /// /// /// ///public static DataTable ResolveDataTable(this IWorkbook workbook, string sheetIndexOrName, int headerRowIndex, short startColIndex = 0, short colCount = 0) { DataTable table = new DataTable(); ISheet sheet = GetSheet(workbook, sheetIndexOrName); IRow headerRow = sheet.GetRow(headerRowIndex); int cellFirstNum = (startColIndex > headerRow.FirstCellNum ? startColIndex : headerRow.FirstCellNum); int cellCount = (colCount > 0 && colCount < headerRow.LastCellNum ? colCount : headerRow.LastCellNum); for (int i = cellFirstNum; i < cellCount; i++) { if (headerRow.GetCell(i) == null || headerRow.GetCell(i).StringCellValue.Trim() == "") { // 如果遇到第一个空列,则不再继续向后读取 cellCount = i; break; } DataColumn column = new DataColumn(headerRow.GetCell(i).StringCellValue); table.Columns.Add(column); } for (int i = (headerRowIndex + 1); i <= sheet.LastRowNum; i++) { IRow row = sheet.GetRow(i); if (row != null) { List cellValues = new List (); for (int j = cellFirstNum; j < cellCount; j++) { if (row.GetCell(j) != null) { cellValues.Add(row.GetCell(j).ToNotNullString()); } else { cellValues.Add(string.Empty); } } table.Rows.Add(cellValues.ToArray()); } } return table; } /// /// 从工作表中解析生成指定的结果对象列表 /// ////// /// /// /// /// /// /// public static List ResolveAs (this IWorkbook workbook, string sheetIndexOrName, int headerRowIndex, Func , T> buildResultItemFunc, short startColIndex = 0, short colCount = 0) { ISheet sheet = GetSheet(workbook, sheetIndexOrName); IRow headerRow = sheet.GetRow(headerRowIndex); int cellFirstNum = (startColIndex > headerRow.FirstCellNum ? startColIndex : headerRow.FirstCellNum); int cellCount = (colCount > 0 && colCount < headerRow.LastCellNum ? colCount : headerRow.LastCellNum); List
resultList = new List (); for (int i = (headerRowIndex + 1); i <= sheet.LastRowNum; i++) { IRow row = sheet.GetRow(i); if (row != null) { List cellValues = new List (); for (int j = cellFirstNum; j < cellCount; j++) { if (row.GetCell(j) != null) { cellValues.Add(row.GetCell(j).ToNotNullString()); } else { cellValues.Add(string.Empty); } } resultList.Add(buildResultItemFunc(cellValues)); } } return resultList; } public static MemoryStream ToExcelStream(this IWorkbook book) { if (book.NumberOfSheets <= 0) { throw new Exception("无有效的sheet数据"); } MemoryStream stream = new MemoryStream(); stream.Seek(0, SeekOrigin.Begin); book.Write(stream); return stream; } public static byte[] ToExcelBytes(this IWorkbook book) { using (MemoryStream stream = ToExcelStream(book)) { return stream.ToArray(); } } public static JObject HttpUpload(this IWorkbook book, string uploadUrl, IDictionary fieldData = null, string exportFileName = null) { using (HttpClient client = new HttpClient()) { MultipartFormDataContent formData = new MultipartFormDataContent(); ByteArrayContent fileContent = new ByteArrayContent(ToExcelBytes(book)); //StreamContent fileContent = new StreamContent(ToExcelStream(book)); 二者都可以 fileContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); fileContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data"); if (string.IsNullOrWhiteSpace(exportFileName)) { exportFileName = Guid.NewGuid().ToString("N") + ((book is XSSFWorkbook) ? ".xlsx" : ".xls"); } fileContent.Headers.ContentDisposition.FileName = exportFileName; fileContent.Headers.ContentDisposition.Name = "file"; formData.Add(fileContent); Func getStringContent = (str) => new StringContent(str, Encoding.UTF8); if (fieldData != null) { foreach (var header in fieldData) { formData.Add(getStringContent(header.Value.ToNotNullString()), header.Key); } } HttpResponseMessage res = client.PostAsync(uploadUrl, formData).Result; string resContent = res.Content.ReadAsStringAsync().Result; return JObject.Parse(resContent); } } public static string SaveToFile(this IWorkbook book, string filePath) { if (File.Exists(filePath)) { File.SetAttributes(filePath, FileAttributes.Normal); File.Delete(filePath); } using (FileStream fs = new FileStream(filePath, FileMode.OpenOrCreate, FileAccess.ReadWrite)) { book.Write(fs); } return filePath; } } }
如上代码都比较简单没有什么复杂要,故不再细述。还有一个Core类,这个只是一个入口帮助类,如果已有获得IWorkbook的自定义方法,该类可以不用,代码如下:
using NPOI.HSSF.UserModel; using NPOI.SS.UserModel; using NPOI.XSSF.UserModel; using System; using System.Collections.Generic; using System.Text; namespace ExcelEasyUtil { ////// NPOI 相关核心入口方法帮助类 /// author:zuowenjun /// 2019-5-21 /// public static class Core { ////// 创建一个基本XLSX格式的EXCEL工作薄对象 /// ///public static IWorkbook CreateXlsxWorkBook() { return new XSSFWorkbook(); } /// /// 创建一个基本XLS格式的EXCEL工作薄对象 /// ///public static IWorkbook CreateXlsWorkBook() { return new HSSFWorkbook(); } /// /// 打开指定文件的EXCEL工作薄对象 /// /// Excel文件路径 ///public static IWorkbook OpenWorkbook(string filePath) { bool isCompatible = filePath.EndsWith(".xls", StringComparison.OrdinalIgnoreCase); var fileStream = System.IO.File.OpenRead(filePath); if (isCompatible) { return new HSSFWorkbook(fileStream); } else { return new XSSFWorkbook(fileStream); } } } }
虽然代码简单,但方法命名稍有些讲究,易阅读与理解,CreateXlsxWorkBook就是表现创建一个新的基于xlsx格式的工作薄对象,而CreateXlsWorkBook就是表现创建一个新的基于xls格式的工作薄对象,我并没合成一个方法,然后使用一个bool参数或enum参数来区分,我觉得在这个追求极简、高效的开发理念中,方法名要易于理解与快速上手,不应产生歧义,也无需太多的参数。OpenWorkbook就是打开一个已有的EXCEL文件工作薄对象。
代码虽简单,但我通过合理的封装入参,委托参数等,实现了简单但又不失灵活的EXCEL操作方式,没有一味追求极简,把所有的复杂操作全部封装,从而导致本来简单的事情搞得复杂化,比如:将一个List
第一种填充sheet方式:(重点在表格头的映射设置,通过Lamba属性表达式与要生成的EXCEL表头进行关联映射)
var book= ExcelEasyUtil.Core.CreateXlsxWorkBook() .FillSheet("人员列表1", peoples,//填充第一个表格 //new Dictionary>> //设置表格头,原始类型 new PropertyColumnMapping //设置表格头,专用简化类型 { {"姓名",p=>p.Name },{"年龄",p=>p.Age },{"生日",p=>p.Birthday },{"住址",p=>p.Address },{"学历",p=>p.Education }, { "有工作否",p=>p.hasWork },{"备注",p=>p.Remark } }, new Dictionary //为指定的列设置单元格格式 { { "年龄","0岁"},{"生日","yyyy年mm月dd日"} });
第二种填充sheet方式:(重点在表格头类型设置【:XXX表示生成的EXCEL该列为某种格式的单元格,如:生日:DT表示是生日这列导出是日期类型格式】,第二个参数返回List
var book= ExcelEasyUtil.Core.CreateXlsxWorkBook() .FillSheet("人员列表2", peoples, //填充第二个表格 new List{ "姓名","年龄:NUM","生日:DT","住址","学历","有工作否:BL","备注","额外填充列" }, (p) => { return new List
第三种填充sheet方式:(重点仍然是在表头映射,由于这里的数据源是DataTable,故只是设置DataTable的列与EXCEL要导出的列名映射即可,无需指定列类型,第二个参数是为指定的列设置单元格的具体应用格式)
var book= ExcelEasyUtil.Core.CreateXlsxWorkBook() .FillSheet("人员列表3", peoplesTable, //填充第三个表格 new Dictionary{ {"Name","姓名" },{"Birthday","生日" },{"Address","住址" },{"Education","学历" }, {"hasWork","有工作否" },{"Remark","备注" } } , new Dictionary { { "生日","yyyy-mm-dd"} });
由于实现了FillSheet方法后仍返回IWorkBook实例本身,即可采用链式的方式来快速完成多个sheet导出的,合并代码如下:
string savedPath = ExcelEasyUtil.Core.CreateXlsxWorkBook() .FillSheet("人员列表1", peoples,//填充第一个表格 //new Dictionary>> //设置表格头,原始类型 new PropertyColumnMapping //设置表格头,专用简化类型 { {"姓名",p=>p.Name },{"年龄",p=>p.Age },{"生日",p=>p.Birthday },{"住址",p=>p.Address },{"学历",p=>p.Education }, { "有工作否",p=>p.hasWork },{"备注",p=>p.Remark } }, new Dictionary //为指定的列设置单元格格式 { { "年龄","0岁"},{"生日","yyyy年mm月dd日"} }) .FillSheet("人员列表2", peoples, //填充第二个表格 new List { "姓名","年龄:NUM","生日:DT","住址","学历","有工作否:BL","备注","额外填充列" }, (p) => { return new List
感觉如何,EXCEL导出既能设置列头,列格式,还能控制填充的数据值,认为是否够方便呢,我个人觉得还是可以满足日常大部份的使用EXCEL的需求。
说了导出EXCEL方法再来贴出导入EXCEL数据(这里称为解析EXCEL数据)的示例用法:
var xlsTable = ExcelEasyUtil.Core.OpenWorkbook(savedPath).ResolveDataTable("人员列表1", 0); foreach (DataRow row in xlsTable.Rows) { string rowStr = string.Join("\t", row.ItemArray); Console.WriteLine(rowStr); } var xlsPeoples = ExcelEasyUtil.Core.OpenWorkbook(savedPath).ResolveAs("人员列表2", 0, list => { return new People { Name = list[0], Birthday = ConvertToDate(list[2]),//日期处理相对较麻烦 Address = list[3] }; }, 0, 4); Console.WriteLine("-".PadRight(30,'-')); foreach (var p in xlsPeoples) { string rowStr = string.Join("\t", p.Name, p.Age, p.Birthday, p.Address); Console.WriteLine(rowStr); }
这里仅实现了解析到DataTable或解析到所需的对象集合,一般也能满足常见的导入数据的要求。
当然还有:HttpUpload方法,这个主要是把生成的内存EXCEL文件直接上传至指定的文件服务器的,如果是在ASP.NET,ASP.NET CORE服务端需要下载,那么则可以采用先使用:book.ToExcelBytes(),然后构造文件字节流Result即可,比如:MVC中的File,可以参照我本文一开始列举的第二中方法中的导出,在此就不再贴示例了。
好了,以上就是本文的全部内容,可能大神们认为这太简单了,但我认为简单高效的实用类库还是需要的,因为ExcelEasyUtil本身就是为简单、轻量、实用而封装的,故不会有太多复杂的东西,如果需要复杂的EXCEL操作就使用我上面说的第四种方案:ExcelUtility。
若有不足之处或好的建议,欢迎评论,谢谢!
本文ExcelEasyUtil源代码地址:https://github.com/zuowj/ExcelEasyUtil (文中的所有示例代码在github均有)
为了方便开发者使用,还封装成了NuGet包:
Packge Manager:Install-Package ExcelEasyUtil -Version 1.0.0
.NET CLI:dotnet add package ExcelEasyUtil --version 1.0.0 OR