开源 - Ideal库 - Excel帮助类,ExcelHelper实现(四)

news/2024/12/4 0:11:08/文章来源:https://www.cnblogs.com/hugogoos/p/18585037

书接上回,前面章节已经实现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至DataSetreturn CreateDataSetWithStreamOfSheets(stream, isXlsx, isFirstRowAsColumnName);}//读取指定工作簿Sheet至DataSetreturn CreateDataSetWithStreamOfSheet(stream, isXlsx, isFirstRowAsColumnName, sheetName, sheetNumber ?? 1);
}
//读取所有工作簿Sheet至DataSet
private static DataSet CreateDataSetWithStreamOfSheets(Stream stream, bool isXlsx, bool isFirstRowAsColumnName)
{//根据Excel文件后缀创建IWorkbookusing var workbook = CreateWorkbook(isXlsx, stream);//根据Excel文件后缀创建公式求值器var evaluator = CreateFormulaEvaluator(isXlsx, workbook);var dataSet = new DataSet();for (var i = 0; i < workbook.NumberOfSheets; i++){//获取工作簿Sheetvar 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文件后缀创建IWorkbookusing var workbook = CreateWorkbook(isXlsx, stream);if (string.IsNullOrWhiteSpace(sheetName) && sheetIndex >= workbook.NumberOfSheets){//工作簿sheet索引非法则返回return dataSet;}//根据Excel文件后缀创建公式求值器var evaluator = CreateFormulaEvaluator(isXlsx, workbook);//优先通过工作簿名称获取工作簿sheetvar 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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/846395.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

攻防世界-OldDriver

一、题目二、解题 1、观察特征:低指数,多密文 使用低指数广播攻击2、中国剩余定理剩余定理求解代码: def crt(remainders, moduli):"""中国剩余定理实现:param remainders: 余数列表 [r1, r2, ...]:param moduli: 模数列表 [m1, m2, ...]:return: 满足所有条…

Cannot find a valid baseurl for repo: base/7/x86_64

001、yum报错(base) [root@PC1 yum.repos.d]# yum install httpd ## yum安装应用报错 。002、解决方法(base) [root@PC1 home]# cd /etc/yum.repos.d (base) [root@PC1 yum.repos.d]# ls CentOS-Base.repo CentOS-CR.repo CentOS-Debuginfo.repo CentOS-fasttrack.rep…

【Comsol 6.2软件下载与安装教程】

1、安装包 「COMSOL 6.2」: 下载地址 2、安装教程(建议关闭杀毒软件和系统防护) 1) 下载并解压下载的安装包,双击Setup.exe安装,弹窗安装对话框2) 选择简体中文,点击下一步3) 选择先安装6.24) 选择我接受,点击许可证格式-许可证文件-浏览 – C…

搭建eureka集群环境以及客户端配置

先来效果图 eureka集群节点一 eureka集群节点二 节点一显示的注册服务 节点二显示的注册服务 eureka服务端配置如下所示 现在将eureka服务端放到了测试环境 192.168.31.50 192.168.31.60 服务信息 最后给出全部的eureka代码 pom文件<?xml version="1.0" encodin…

【推荐算法】推荐系统的评估

这篇文章是笔者阅读《深度学习推荐系统》第五章推荐系统的评估的学习笔记,在原文的基础上增加了自己的理解以及内容的补充,在未来的日子里会不断完善这篇文章的相关工作。离线评估在离线环境中利用已有的数据划分训练集和测试集对模型进行评估划分数据集方法机器学习常用划分…

考研打卡(34)

开局(34) 开始时间 2024-12-03 22:36:03 结束时间 2024-12-03 23:17:57为什么昨天没写,因为昨天想死,但我jio得不能每天都想死吧,所以今天该写了数据结构如果一棵二叉树的先序序列是…a…b…,中序序列是…b…a…,则_______(北京师范大学 2015年) A 节点a和节点b分别在…

Educational Codeforces Round 172 (Rated for Div. 2)题解记录(A~D)

比赛链接:https://codeforces.com/contest/2042 这场爆了,卡死在C题了,QWQ.卡题得跳题啊!!! A.Greedy Monocarp 题面: 有 \(n\) 个箱子,第 \(i\) 个箱子最初包含 \(a_i\) 枚硬币。对于每个箱子,你可以选择任意非负数(0或更大)的硬币添加到该箱子中,有一个约束条件:…

Educational Codeforces Round 172 (Rated for Div. 2)(A~D)

比赛链接:https://codeforces.com/contest/2042 这场爆了,卡C题了,QWQ.卡题得跳一跳!!! A.Greedy Monocarp 题面: 有 \(n\) 个箱子,第 \(i\) 个箱子最初包含 \(a_i\) 枚硬币。对于每个箱子,你可以选择任意非负数(0或更大)的硬币添加到该箱子中,有一个约束条件:所有…

树上的最远距离

题目 题目描述 给定一棵树,对于每一个点,输出离它最远的点到它的距离。 输入格式 第一行包含整数 \(n\)。 接下来 \(n-1\) 行,每行包含两个整数 \(a_i,b_i\),表示点 \(a_i\) 和 \(b_i\) 之间存在一条边。 输出格式 输出一行 \(n\) 个整数,第 \(i\) 个数表示离节点 \(i\) 最…

go 多线程

goroutine以及gmp原理go 多线程 进程、线程、和协程进程分配系统资源(CPU 时间、内存等)基本单位 有独立的内存空间,切换开销大线程:进程的一个执行流,是 CPU 调度并能独立运行的的基本单位同一进程中的多线程共享内存空间,线程切换代价小 多线程通信方便 从内核层面来看…

第57篇 docker的常用命令

1 镜像管理2 容器管理3 容器运行4 网络管理5 插件管理6 数据卷管理7 日常操作8 常用dockerfile指令

技术美术学习路线

技术美术学习路线 理想路线 第一个阶段熟练一门编程语言(课设写个例如医院管理系统,图书管理系统之类的):C/python/C++/C#,并尝试用它写一个飞机大战小游戏养成良好的审美,每日收集图片,鉴赏各种美术风格从Unity客户端开始学习,之后逐步学习技术美术:如何不写代码却能…