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

news2024/12/26 20:29:21

书接上回,前面章节已经实现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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

嵌入式系统应用-LVGL的应用-平衡球游戏 part1

平衡球游戏 part1 1 平衡球游戏的界面设计2 界面设计2.1 背景设计2.2 球的设计2.3 移动球的坐标2.4 用鼠标移动这个球2.5 增加边框规则2.6 效果图2.7 游戏失败重启游戏 3 为小球增加增加动画效果3.1 增加移动效果代码3.2 具体效果图片 平衡球游戏 part2 第二部分文章在这里 1 …

《Python基础》之Pandas库

目录 一、简介 二、Pandas的核心数据结构 1、Series 2、DataFrame 三、数据读取与写入 1、数据读取 2、数据写入 四、数据清洗与处理 1、处理缺失值 2、处理重复值 3、数据转换 五、数据分析与可视化 1、统计描述 2、分组聚合 3、数据可视化 六、高级技巧 1、时…

网络安全-夜神模拟器如何通过虚拟机的Burp Suite代理应用程序接口

第一步、查看虚拟机的IP地址 我们可以通过ifconfig命令来查看虚拟机的IP地址,如下图所示。 第二步、在Burp Suite上设置代理 打开虚拟机上的Burp Suite,进入到代理模块中,进入到代理设置中心 打开系统代理设置中心之后,将我们虚拟机的地址添加到上面,作为新的代理。 第…

PyTorch 2.5.1: Bugs修复版发布

一&#xff0c;前言 在深度学习框架的不断迭代中&#xff0c;PyTorch 社区始终致力于提供更稳定、更高效的工具。最近&#xff0c;PyTorch 2.5.1 版本正式发布&#xff0c;这个版本主要针对 2.5.0 中发现的问题进行了修复&#xff0c;以提升用户体验。 二&#xff0c;PyTorch 2…

SpringAi整合大模型(进阶版)

进阶版是在基础的对话版之上进行新增功能。 如果还没弄出基础版的&#xff0c;请参考 https://blog.csdn.net/weixin_54925172/article/details/144143523?sharetypeblogdetail&sharerId144143523&sharereferPC&sharesourceweixin_54925172&spm1011.2480.30…

Python实现网站资源批量下载【可转成exe程序运行】

Python实现网站资源批量下载【可转成exe程序运行】 背景介绍解决方案转为exe可执行程序简单点说详细了解下 声明 背景介绍 发现 宣讲家网 的PPT很好&#xff0c;作为学习资料使用很有价值&#xff0c;所以想下载网站的PPT课件到本地&#xff0c;但是由于网站限制&#xff0c;一…

CSS函数

目录 一、背景 二、函数的概念 1. var()函数 2、calc()函数 三、总结 一、背景 今天我们就来说一说&#xff0c;常用的两个css自定义属性&#xff0c;也称为css函数。本文中就成为css函数。先来看一下官方对其的定义。 自定义属性&#xff08;有时候也被称作CSS 变量或者级…

UG NX二次开发(C#)-选择对象居中(不是全部居中)

文章目录 1、前言2、什么是对象居中3、功能实现代码3.1 对象居中3.1 恢复原视图1、前言 在UG NX二次开发过程中,我们经常会用到居中以查看完整的模型,但是对于如果想展示某些对象,而不是全部模型时,那么我们就想将选择的对象(如体对象)居中查看,当查看结束后还能恢复到…

动态规划-----路径问题

动态规划-----路径问题 下降最小路径和1&#xff1a;状态表示2&#xff1a;状态转移方程3 初始化4 填表顺序5 返回值6 代码实现 总结&#xff1a; 下降最小路径和 1&#xff1a;状态表示 假设&#xff1a;用dp[i][j]表示&#xff1a;到达[i,j]的最小路径 2&#xff1a;状态转…

[C++设计模式] 为什么需要设计模式?

文章目录 什么是设计模式&#xff1f;为什么需要设计模式&#xff1f;GOF 设计模式再次理解面向对象软件设计固有的复杂性软件设计复杂性的根本原因如何解决复杂性&#xff1f;分解抽象 结构化 VS 面向对象(封装)结构化设计代码示例&#xff1a;面向对象设计代码示例&#xff1…

级联树结构TreeSelect和上级反查

接口返回结构 前端展示格式 前端组件 <template><div ><el-scrollbar height"70vh"><el-tree :data"deptOptions" :props"{ label: label, children: children }" :expand-on-click-node"false":filter-node-me…

Figma入门-自动布局

Figma入门-自动布局 前言 在之前的工作中&#xff0c;大家的原型图都是使用 Axure 制作的&#xff0c;印象中 Figma 一直是个专业设计软件。 最近&#xff0c;很多产品朋友告诉我&#xff0c;很多原型图都开始用Figma制作了&#xff0c;并且很多组件都是内置的&#xff0c;对…

【Unity基础】使用InputSystem实现物体跳跃

要在Unity中使用 InputSystem 实现小球按空格键跳起的效果&#xff0c;可以按照以下步骤进行&#xff1a; 1. 安装 InputSystem 包 首先&#xff0c;确保你已经安装了 Input System 包。你可以通过以下步骤安装&#xff1a; 打开 Unity 编辑器&#xff0c;点击菜单 Window -…

【ArkTS】使用AVRecorder录制音频 --内附录音机开发详细代码

系列文章目录 【ArkTS】关于ForEach的第三个参数键值 【ArkTS】“一篇带你读懂ForEach和LazyForEach” 【小白拓展】 【ArkTS】“一篇带你掌握TaskPool与Worker两种多线程并发方案” 【ArkTS】 一篇带你掌握“语音转文字技术” --内附详细代码 【ArkTS】技能提高–“用户授权”…

一种多功能调试工具设计方案开源

一种多功能调试工具设计方案开源 设计初衷设计方案具体实现HUB芯片采用沁恒微CH339W。TF卡功能网口功能SPI功能IIC功能JTAG功能下行USB接口 安路FPGA烧录器功能Xilinx FPGA烧录器功能Jlink OB功能串口功能RS232串口RS485和RS422串口自适应接口 CAN功能烧录器功能 目前进度后续计…

浏览器的事件循环机制

浏览器和Node的事件循环机制 引言浏览器的事件循环机制 引言 由于JS是单线程的脚本语言&#xff0c;所以在同一时间只能做一件事情&#xff0c;当遇到多个任务时&#xff0c;我们不可能一直等待任务完成&#xff0c;这会造成巨大的资源浪费。为了协调时间&#xff0c;用户交互…

Zabbix添加防火墙温度监控值实战

我们在Zabbix监控系统会监控诸如Server、network device、application等实例&#xff0c;通常我们在监控某个具体产品时&#xff0c;我们会找到具体的监控模板&#xff0c;在设备添加到平台以后&#xff0c;将模板链接到该设备&#xff0c;但很多时候我们企业内部的设备是没有标…

【k8s】创建基于sa的token的kubeconfig

需求 创建一个基于sa的token的kubeconfig文件&#xff0c;并用这个文件来访问集群。 具体创建sa 和sa的token请参考文章: 【k8s】给ServiceAccount 创建关联的 Secrets-CSDN博客 创建sa apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata:namespace: jtkjdevnam…

Fastapi + vue3 自动化测试平台---移动端App自动化篇

概述 好久写文章了&#xff0c;专注于新框架&#xff0c;新UI界面的实践&#xff0c;废话不多说&#xff0c;开搞 技术架构 后端&#xff1a; Fastapi Airtest multiprocessing 前端&#xff1a; 基于 Vue3、Vite、TypeScript、Pinia、Pinia持久化插件、Unocss 和 Elemen…

Apache Doris 现行版本 Docker-Compose 运行教程

特别注意&#xff01;Doris On Docker 部署方式仅限于开发环境或者功能测试环境&#xff0c;不建议生产环境部署&#xff01; 如有生产环境或性能测试集群部署诉求&#xff0c;请使用裸机/虚机部署或K8S Operator部署方案&#xff01; 原文阅读&#xff1a;Apache Doris 现行版…