开源 - 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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Android Studio 右侧工具栏 Gradle 不显示 Task 列表

问题&#xff1a; android studio 4.2.1版本更新以后AS右侧工具栏Gradle Task列表不显示&#xff0c;这里需要手动去设置 解决办法&#xff1a; android studio 2024.2.1 Patch 2版本以前的版本设置&#xff1a;依次打开 File -> Settings -> Experimental 选项&#x…

力扣hot100道【贪心算法后续解题方法心得】(三)

力扣hot100道【贪心算法后续解题方法心得】 十四、贪心算法关键解题思路1、买卖股票的最佳时机2、跳跃游戏3、跳跃游戏 | |4、划分字母区间 十五、动态规划什么是动态规划&#xff1f;关键解题思路和步骤1、打家劫舍2、01背包问题3、完全平方式4、零钱兑换5、单词拆分6、最长递…

计算机网络常见面试题总结(上)

计算机网络基础 网络分层模型 OSI 七层模型是什么&#xff1f;每一层的作用是什么&#xff1f; OSI 七层模型 是国际标准化组织提出的一个网络分层模型&#xff0c;其大体结构以及每一层提供的功能如下图所示&#xff1a; 每一层都专注做一件事情&#xff0c;并且每一层都需…

编译MT7620 OpenWrt的所有机型的固件

前置条件&#xff1a;准备VMware16Ubuntu16.04的编译环境 安装编译需要的插件 sudo apt-get install gcc g binutils patch bzip2 flex bison make autoconf gettext texinfo unzip sharutils libncurses5-dev ncurses-term zlib1g-dev gawk asciidoc libz-dev git-core uuid…

vue2+svg+elementui实现花瓣图自定义el-select回显色卡图片

项目需要实现花瓣图&#xff0c;但是改图表在echarts&#xff0c;highCharts等案例中均未出现&#xff0c;有类似的韦恩图&#xff0c;但是和需求有所差距&#xff1b; 为实现该效果&#xff0c;静态图表上采取svg来手动绘制花瓣&#xff1a; 确定中心点&#xff0c;以该点为中…

释放超凡性能,打造鸿蒙原生游戏卓越体验

11月26日在华为Mate品牌盛典上&#xff0c;全新Mate70系列及多款全场景新品正式亮相。在游戏领域&#xff0c;HarmonyOS NEXT加持下游戏的性能得到充分释放。HarmonyOS SDK为开发者提供了软硬协同的系统级图形加速解决方案——Graphics Accelerate Kit&#xff08;图形加速服务…

Docker应用

一、需要知道什么&#xff08;先去了解一下&#xff09; Docker 概述 Docker 安装&#xff08;配置阿里云镜像加速器&#xff0c;要不 pull 镜像下载的慢&#xff09; portainer 可视化界面 关于镜像、容器、仓库的命令等 数据卷 Docker 网络 DockerFile DockerCompose …

Qt桌面应用开发 第十天(综合项目二 翻金币)

目录 1.主场景搭建 1.1重载绘制事件&#xff0c;绘制背景图和标题图片 1.2设置窗口标题&#xff0c;大小&#xff0c;图片 1.3退出按钮对应关闭窗口&#xff0c;连接信号 2.开始按钮创建 2.1封装MyPushButton类 2.2加载按钮上的图片 3.开始按钮跳跃效果 3.1按钮向上跳…

VScode离线下载扩展安装

在使用VScode下在扩展插件时&#xff0c;返现VScode搜索不到插件&#xff0c;网上搜了好多方法&#xff0c;都不是常规操作&#xff0c;解决起来十分麻烦&#xff0c;可以利用离线下载安装的方式安装插件&#xff01;亲测有效&#xff01;&#xff01;&#xff01; 1.找到VScod…

浅谈网络 | 应用层之HTTPS协议

目录 对称加密非对称加密数字证书HTTPS 的工作模式重放与篡改 使用 HTTP 协议浏览新闻虽然问题不大&#xff0c;但在更敏感的场景中&#xff0c;例如支付或其他涉及隐私的数据传输&#xff0c;就会面临巨大的安全风险。如果仍然使用普通的 HTTP 协议&#xff0c;数据在网络传输…

MySQL有哪些日志?

MySQL主要有三种日志&#xff1a;undo log、redo log、binlog。前两种是InnoDB特有的&#xff0c;binlog是MySQL的Server层中的。 Buffer Pool buffer pool是MySQL的缓冲池&#xff0c;里面存储了数据页、索引页、undo页等&#xff08;与数据库不一致的即为脏页&#xff09;。…

Node.js 实战: 爬取百度新闻并序列化 - 完整教程

很多时候我们需要爬取一些公开的网页内容来做一些数据分析和统计。而多数时候&#xff0c;大家会用到python &#xff0c;因为实现起来很方便。但是其实Node.js 用来爬取网络内容&#xff0c;也是非常强大的。 今天我向大家介绍一下我自己写的一个百度新闻的爬虫&#xff0c;可…

csv文件的上传、解析和获得最后的数据

前端和node端解析、读取csv文件的区别 1、前端 运行环境为浏览器&#xff0c;受到浏览器安全策略的限制&#xff0c;例如跨域请求、文件访问权限等。对于大型CSV文件的处理可能会受到性能瓶颈的影响。前端运行在用户的浏览器中&#xff0c;受到浏览器安全策略的限制&#xff…

JavaScript(一)

1.JavaScript 基本使用 2.JavaScript简单事件 3.JavaScript修改样式 4.JavaScript数据类型 JavaScript和Java有什么关系 知识点一 JavaScript基本使用 JS写在哪 还有一种写在中间的&#xff0c;也就是<head>里面 JS一些注意事项 JS修改元素内容 #JS获取对象<…

QT实战-qt各种菜单样式实现

本文主要介绍了qt普通菜单样式、带选中样式、带子菜单样式、超过一屏幕菜单样式、自定义带有滚动条的菜单样式&#xff0c; 先上图如下&#xff1a; 1.普通菜单样式 代码&#xff1a; m_pmenu new QMenu(this);m_pmenu->setObjectName("quoteListMenu"); qss文…

基于Java Springboot校园论坛APP且微信小程序

一、作品包含 源码数据库设计文档万字PPT全套环境和工具资源部署教程 二、项目技术 前端技术&#xff1a;Html、Css、Js、Vue、Element-ui 数据库&#xff1a;MySQL 后端技术&#xff1a;Java、Spring Boot、MyBatis 三、运行环境 开发工具&#xff1a;IDEA/eclipse微信开…

深度学习基础03_BP算法(下)过拟合和欠拟合

目录 一、BP算法(下) 0、反向传播代码回顾 写法一&#xff1a; 写法二(更常用)&#xff1a; 1、BP中的梯度下降 1.数学描述 2.传统下降方式 3.优化梯度下降方式 指数加权平均 Momentum AdaGrad RMSProp Adam(常用) 总结 二、过拟合和欠拟合 1、概念 1.过拟合 …

实现PDF文档加密,访问需要密码

01. 背景 今天下午老板神秘兮兮的来问我&#xff0c;能不能做个文档加密功能&#xff0c;就是那种用户下载打开需要密码才能打开的那种效果。boss都发话了&#xff0c;那必须可以。 需求&#xff1a;将 pdf 文档经过加密处理&#xff0c;客户下载pdf文档&#xff0c;打开文档需…

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

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

1.Git安装与常用命令

前言 Git中会用到的一些基本的Linux命令 ls/ll 查看文件目录 (ll可以看隐藏文件)cat 查看文件内容touch 创建文件vi vi编辑器 1.下载与安装 安装成功后鼠标右键会出现Git Bash和Git GUI Git GUI&#xff1a;GUI图形化界面 Git Bash&#xff1a;Git提供的命令行工具 当安装…