开源 - Ideal库 - Excel帮助类,ExcelHelper实现(四)
书接上回,前面章节已经实现Excel帮助类的第一步TableHeper的对象集合与DataTable相互转换功能,今天实现进入其第二步的核心功能ExcelHelper实现。

01、接口设计
下面我们根据第一章中讲解的核心设计思路,先进行接口设计,确定ExcelHelper需要哪些接口即可满足我们的要求,然后再一个一个接口实现即可。
先简单回顾一下核心设计思路,主要涉及两类操作:读和写,两种转换:DataTable与Excel转换和对象集合与Excel转换。
下面先看看设计的所有接口:
//根据文件路径读取Excel到DataSet
//指定sheetName,sheetNumber则读取相应工作簿Sheet
//如果不指定则读取所有工作簿Sheet
public static DataSet Read(string path, bool isFirstRowAsColumnName = false, string? sheetName = null, int? sheetNumber = null);
//根据文件流读取Excel到DataSet
//指定sheetName,sheetNumber则读取相应工作簿Sheet
//如果不指定则读取所有工作簿Sheet
public static DataSet Read(Stream stream, string fileName, bool isFirstRowAsColumnName = false, string? sheetName = null, int? sheetNumber = null);
//根据文件流读取Excel到DataSet
//指定sheetName,sheetNumber则读取相应工作簿Sheet
//如果不指定则读取所有工作簿Sheet
public static DataSet Read(Stream stream, bool isXlsx, bool isFirstRowAsColumnName = false, string? sheetName = null, int? sheetNumber = null);
//根据文件流读取Excel到对象集合
//指定sheetName,sheetNumber则读取相应工作簿Sheet
//如果不指定则默认读取第一个工作簿Sheet
public static IEnumerable<T> Read<T>(string path, bool isFirstRowAsColumnName = false, string? sheetName = null, int? sheetNumber = null);
//根据文件流读取Excel到对象集合
//指定sheetName,sheetNumber则读取相应工作簿Sheet
//如果不指定则默认读取第一个工作簿Sheet
public static IEnumerable<T> Read<T>(Stream stream, string fileName, bool isFirstRowAsColumnName = false, string? sheetName = null, int? sheetNumber = null);
//根据文件流读取Excel到对象集合
//指定sheetName,sheetNumber则读取相应工作簿Sheet
//如果不指定则默认读取第一个工作簿Sheet
public static IEnumerable<T> Read<T>(Stream stream, bool isXlsx, bool isFirstRowAsColumnName = false, string? sheetName = null, int? sheetNumber = null);
//把表格数组写入Excel文件流
public static MemoryStream Write(DataTable[] dataTables, bool isXlsx, bool isColumnNameAsData);
//把表格数组写入Excel文件
public static void Write(DataTable[] dataTables, string path, bool isColumnNameAsData);
//把对象集合写入Excel文件流
public static MemoryStream Write<T>(IEnumerable<T> models, bool isXlsx, bool isColumnNameAsData, string? sheetName = null);
//把对象集合写入Excel文件
public static void Write<T>(IEnumerable<T> models, string path, bool isColumnNameAsData, string? sheetName = null);
02、根据文件路径读取Excel到DataSet
该方法是通过Excel完全路径直接读取Excel文件,因此我们首先读取到文件流,然后再调用具体处理文件流实现方法。
因为Excel中工作簿Sheet正好对应DataSet中表格DataTable,因此在不指定读取某个工作簿Sheet的情况下,默认是读取Excel中所有工作簿Sheet。
指定工作簿方式也很简单,只要传参数指定工作簿名称sheetName或者工作簿编号sheetNumber即可,提供两个参数是考虑到可能名字不好记,但是第几个工作簿Sheet会比较好记,也因此工作簿编号sheetNumber是从1开始。两者会优先处理工作簿名称sheetName。
因为表格DataTable是有列名的,通过这个列名我们可以把它和对象属性关联上,最后实现相互映射转换,而工作簿Sheet则没有这个概念,因此我们要想最终实现对象和工作簿Sheet的相互转换,就需要人为指定这样的数据。
通常的做法是以工作簿Sheet中第一行数据作为表格DataTable列名,因此我们在接口中设计了这个参数用来指定是否需要把第一行数据作为表格列名。
具体代码实现如下:
//根据文件路径读取Excel到DataSet
//指定sheetName,sheetNumber则读取相应工作簿Sheet
//如果不指定则读取所有工作簿Sheet
public static DataSet Read(string path, bool isFirstRowAsColumnName = false, string? sheetName = null, int? sheetNumber = null)
{
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read);
return Read(stream, IsXlsxFile(path), isFirstRowAsColumnName, sheetName, sheetNumber);
}
03、根据文件流、文件名读取Excel到DataSet
在有些场景下,不需要我们直接读取Excel文件,而是直接给一个Excel文件流。比如说文件上传,前端上传文件后,后端接收到的就是一个文件流。
同时该方法还需要传一个文件名的参数,这是因为我们Excel有两种后缀格式即“.xls”和“.xlsx”,而两种格式处理方式又不相同,因此我们需要通过名字来说识别Excel文件流的具体格式,当然如果调用方法时已经明确知道文件流是什么格式,也可以直接调用下一个重载方法。
其他参数解释上节以及详细讲解了,实现代码如下:
//根据文件流读取Excel到DataSet
//指定sheetName,sheetNumber则读取相应工作簿Sheet
//如果不指定则读取所有工作簿Sheet
public static DataSet Read(Stream stream, string fileName, bool isFirstRowAsColumnName = false, string? sheetName = null, int? sheetNumber = null)
{
return Read(stream, IsXlsxFile(fileName), isFirstRowAsColumnName, sheetName, sheetNumber);
}
04、根据文件流、文件后缀读取Excel到DataSet
该方法是上面两个方法的最终实现,该方法首先会识别读取所有工作簿Sheet还是读取指定工作簿Sheet,然后调不同的方法。而两者差别也这是读一个还是读多个工作簿Sheet的差别,具体代码如下:
//根据文件流读取Excel到DataSet
public static DataSet Read(Stream stream, bool isXlsx, bool isFirstRowAsColumnName = false, string? sheetName = null, int? sheetNumber = null)
{
if (sheetName == null && sheetNumber == null)
{
//读取所有工作簿Sheet至DataSet
return CreateDataSetWithStreamOfSheets(stream, isXlsx, isFirstRowAsColumnName);
}
//读取指定工作簿Sheet至DataSet
return CreateDataSetWithStreamOfSheet(stream, isXlsx, isFirstRowAsColumnName, sheetName, sheetNumber ?? 1);
}
//读取所有工作簿Sheet至DataSet
private static DataSet CreateDataSetWithStreamOfSheets(Stream stream, bool isXlsx, bool isFirstRowAsColumnName)
{
//根据Excel文件后缀创建IWorkbook
using var workbook = CreateWorkbook(isXlsx, stream);
//根据Excel文件后缀创建公式求值器
var evaluator = CreateFormulaEvaluator(isXlsx, workbook);
var dataSet = new DataSet();
for (var i = 0; i < workbook.NumberOfSheets; i++)
{
//获取工作簿Sheet
var sheet = workbook.GetSheetAt(i);
//通过工作簿Sheet创建表格
var table = CreateDataTableBySheet(sheet, evaluator, isFirstRowAsColumnName);
dataSet.Tables.Add(table);
}
return dataSet;
}
//读取指定工作簿Sheet至DataSet
private static DataSet CreateDataSetWithStreamOfSheet(Stream stream, bool isXlsx, bool isFirstRowAsColumnName, string? sheetName = null, int sheetNumber = 1)
{
//把工作簿sheet编号转为索引
var sheetIndex = sheetNumber - 1;
var dataSet = new DataSet();
if (string.IsNullOrWhiteSpace(sheetName) && sheetIndex < 0)
{
//工作簿sheet索引非法则返回
return dataSet;
}
//根据Excel文件后缀创建IWorkbook
using var workbook = CreateWorkbook(isXlsx, stream);
if (string.IsNullOrWhiteSpace(sheetName) && sheetIndex >= workbook.NumberOfSheets)
{
//工作簿sheet索引非法则返回
return dataSet;
}
//根据Excel文件后缀创建公式求值器
var evaluator = CreateFormulaEvaluator(isXlsx, workbook);
//优先通过工作簿名称获取工作簿sheet
var sheet = !string.IsNullOrWhiteSpace(sheetName) ? workbook.GetSheet(sheetName) : workbook.GetSheetAt(sheetIndex);
if (sheet != null)
{
//通过工作簿sheet创建表格
var table = CreateDataTableBySheet(sheet, evaluator, isFirstRowAsColumnName);
dataSet.Tables.Add(table);
}
return dataSet;
}
通过上图实现工作簿Sheet转换DataSet过程,可以发现大致分为三步:
第一步首先根据文件格式以及文件流获取IWorkbook;
第二步再通过文件格式以及IWorkbook获取公式求值器;
第三步再实现把工作簿Sheet转换为表格DataTable;
我们一起看看这三个代码实现:
//根据Excel文件后缀创建IWorkbook
private static IWorkbook CreateWorkbook(bool isXlsx, Stream? stream = null)
{
if (stream == null)
{
return isXlsx ? new XSSFWorkbook() : new HSSFWorkbook();
}
return isXlsx ? new XSSFWorkbook(stream) : new HSSFWorkbook(stream);
}
//根据Excel文件后缀创建公式求值器
private static IFormulaEvaluator CreateFormulaEvaluator(bool isXlsx, IWorkbook workbook)
{
return isXlsx ? new XSSFFormulaEvaluator(workbook) : new HSSFFormulaEvaluator(workbook);
}
//工作簿Sheet转换为表格DataTable
private static DataTable CreateDataTableBySheet(ISheet sheet, IFormulaEvaluator evaluator, bool isFirstRowAsColumnName)
{
var dataTable = new DataTable(sheet.SheetName);
//获取Sheet中最大的列数,并以此数为新的表格列数
var maxColumnNumber = GetMaxColumnNumber(sheet);
if (isFirstRowAsColumnName)
{
//如果第一行数据作为表头,则先获取第一行数据
var firstRow = sheet.GetRow(sheet.FirstRowNum);
for (var i = 0; i < maxColumnNumber; i++)
{
//尝试读取第一行每一个单元格数据,有值则作为列名,否则忽略
string? columnName = null;
var cell = firstRow?.GetCell(i);
if (cell != null)
{
cell.SetCellType(CellType.String);
if (cell.StringCellValue != null)
{
columnName = cell.StringCellValue;
}
}
dataTable.Columns.Add(columnName);
}
}
else
{
for (var i = 0; i < maxColumnNumber; i++)
{
dataTable.Columns.Add();
}
}
//循环处理有效行数据
for (var i = isFirstRowAsColumnName ? sheet.FirstRowNum + 1 : sheet.FirstRowNum; i <= sheet.LastRowNum; i++)
{
var row = sheet.GetRow(i);
var newRow = dataTable.NewRow();
//通过工作簿sheet行数据填充表格新行数据
FillDataRowBySheetRow(row, evaluator, newRow);
//检查每单元格是否都有值
var isNullRow = true;
for (var j = 0; j < maxColumnNumber; j++)
{
isNullRow = isNullRow && newRow.IsNull(j);
}
if (!isNullRow)
{
dataTable.Rows.Add(newRow);
}
}
return dataTable;
}
在实现工作簿Sheet转换为表格DataTable过程中,大致可以分为两步:
第一步求出工作簿Sheet中所有有效行中最宽的列编号,并以此为列数创建表格;
第二步把工作簿Sheet中所有有效行数据填充至表格中;
下面我们看看具体实现代码:
//获取工作簿Sheet中最大的列数
private static int GetMaxColumnNumber(ISheet sheet)
{
var maxColumnNumber = 0;
//在有效的行数据中
for (var i = sheet.FirstRowNum; i <= sheet.LastRowNum; i++)
{
var row = sheet.GetRow(i);
//找到最大的列编号
if (row != null && row.LastCellNum > maxColumnNumber)
{
maxColumnNumber = row.LastCellNum;
}
}
return maxColumnNumber;
}
//通过工作簿sheet行数据填充表格行数据
private static void FillDataRowBySheetRow(IRow row, IFormulaEvaluator evaluator, DataRow dataRow)
{
if (row == null)
{
return;
}
for (var j = 0; j < dataRow.Table.Columns.Count; j++)
{
var cell = row.GetCell(j);
if (cell != null)
{
switch (cell.CellType)
{
case CellType.Blank:
dataRow[j] = DBNull.Value;
break;
case CellType.Boolean:
dataRow[j] = cell.BooleanCellValue;
break;
case CellType.Numeric:
if (DateUtil.IsCellDateFormatted(cell))
{
dataRow[j] = cell.DateCellValue;
}
else
{
dataRow[j] = cell.NumericCellValue;
}
break;
case CellType.String:
dataRow[j] = !string.IsNullOrWhiteSpace(cell.StringCellValue) ? cell.StringCellValue : DBNull.Value;
break;
case CellType.Error:
dataRow[j] = cell.ErrorCellValue;
break;
case CellType.Formula:
dataRow[j] = evaluator.EvaluateInCell(cell).ToString();
break;
default:
throw new NotSupportedException("Unsupported cell type.");
}
}
}
}
注:测试方法代码以及示例源码都已经上传至代码库,有兴趣的可以看看。https://gitee.com/hugogoos/Ideal
开源 - Ideal库 - Excel帮助类,ExcelHelper实现(四)的更多相关文章
- C# Excel操作类 ExcelHelper
实现C#与Excel文件的交互操作,实现以下功能: 1.DataTable 导出到 Excel文件 2.Model数据实体导出到 Excel文件[List<Model>] 3.导出数据到模 ...
- [.Net] C# Excel操作类 ExcelHelper
实现C#与Excel文件的交互操作,实现以下功能: 1.DataTable 导出到 Excel文件 2.Model数据实体导出到 Excel文件[List<Model>] 3.导出数据到模 ...
- C#EXCEL 操作类--C#ExcelHelper操作类
主要功能如下1.导出Excel文件,自动返回可下载的文件流 2.导出Excel文件,转换为可读模式3.导出Excel文件,并自定义文件名4.将数据导出至Excel文件5.将指定的集合数据导出至Exce ...
- Python Excel工具类封装, 给excel表头搞点颜色
封装Excel工具类 我们常用的excel工具类,读有xlrd,写有xlwt.有读有写,新一代库有pandas,openpyxl等等. 大家用法都差不多,今天博主就介绍新手最爱,我也爱的xlrd和xl ...
- Pugixml一种快速解析XML文件的开源解析库
Pugixml是一个轻量级的C++ XML开源解析库,DOM形式的解析器.接口和丰富的遍历和修改操作,快速的解析,此外支持XPath1.0实现数据查询,支持unicode编码: 使用Pugixml可通 ...
- 导入导出Excel工具类ExcelUtil
前言 前段时间做的分布式集成平台项目中,许多模块都用到了导入导出Excel的功能,于是决定封装一个ExcelUtil类,专门用来处理Excel的导入和导出 本项目的持久化层用的是JPA(底层用hibe ...
- MVC NPOI Linq导出Excel通用类
之前写了一个模型导出Excel通用类,但是在实际应用中,可能不是直接导出模型,而是通过Linq查询后获取到最终结果再导出 通用类: public enum DataTypeEnum { Int = , ...
- asp.net(C#) Excel导出类 导出.xls文件
---恢复内容开始--- using Microsoft.Office.Interop.Excel; 针对office 2003需添加引用Microsoft Excel 11.0 Obje ...
- 从Google开源RE2库学习到的C++测试方案
最近因为科研需求,一直在研究Google的开源RE2库(正则表达式识别库),库源码体积庞大,用C++写的,对于我这个以前专供Java的人来说真的是一件很痛苦的事,每天只能啃一点点.今天研究了下里面用到 ...
- 爆料喽!!!开源日志库Logger的剖析分析
导读 Logger类提供了多种方法来处理日志活动.上一篇介绍了开源日志库Logger的使用,今天我主要来分析Logger实现的原理. 库的整体架构图 详细剖析 我们从使用的角度来对Logger库抽茧剥 ...
随机推荐
- 【JS设计模式笔记】给我一张名片-工厂方法模式(创建型)
广告展现 例如,关于计算机培训广告资源需要投放,一批是Java的用绿色字体,一批是PHP的,用黄色字体,红色背景. // 创建Java学科类 var Java = function (content) ...
- OData – 基础语法 Basic
前言 有时候太久没有写真的会忘记,官网又太罗里吧嗦,还是写一篇帮助以后快速复习进入状况吧. Request URL: "/root/version/entities" OData ...
- C# – 冷知识 (新手)
替 Action/Func Parameter 设置名字 public static void MatchBracket(string value, string bracket, Action< ...
- 深入理解 Nuxt.js 中的 app:error:cleared 钩子
title: 深入理解 Nuxt.js 中的 app:error:cleared 钩子 date: 2024/9/28 updated: 2024/9/28 author: cmdragon exce ...
- 【解决方案】基于数据库驱动的自定义 TypeHandler 处理器
目录 前言 一.TypeHandler 简介 1.1转换步骤 1.2转换规则 二.JSON 转换 三.枚举转换 四.文章小结 前言 笔者在最近的项目开发中,频繁地遇到了 Java 类型与 JDBC 类 ...
- CF1659 Codeforces Round #782 (Div. 2) 题解
之前说过的题解,E应该不会补了(大概) A Red Versus Blue 题意非常简单,构造题.给定\(r\)个红色气球和\(b\)个蓝色气球,将它们排成一排,要求使得连续出现的最多的同色气球最少, ...
- 什么是AI网关?AI网关在企业系统中承担什么角色?
AI 大模型的发展正在推动各行业的增长,据有关报告显示:"未来十年内预计年均增长率将达到37.3%,全球企业预计在2027年之前将在AI领域投入8000亿美元".这一迅猛发展促使许 ...
- day09-类型转换
类型转换 由于Java是强类型语言,所以要进行有些运算的时候,需要用到类型转换 低-----------------------------------------------------高 byte ...
- Litmus 实践:让群魔在混沌中乱舞,看 K8s 能撑到何时
对于云服务而言,如果系统出现异常,将会带来很大的损失.为了最大程度地降低损失,我们只能不断探寻系统何时会出现异常,甚至缩小到某些特定参数变化是否会造成系统异常.然而随着云原生的发展,不断推进着微服务的 ...
- Nuxt.js 应用中的 pages:extend 事件钩子详解
title: Nuxt.js 应用中的 pages:extend 事件钩子详解 date: 2024/10/25 updated: 2024/10/25 author: cmdragon excerp ...