2024年12月

技术背景

Numpy是一个Python库中最经常被用于执行计算任务的一个包,得益于其相比默认列表的高性能表现,以及易用性和可靠性,深受广大Python开发者的喜爱。这里介绍的是使用Numpy计算矩阵本征值和本征矩阵的方法。

求解问题

本征问题是求解形如:
\(\mathbf{A}\mathbf{v}=\lambda\mathbf{v}\)
的方程,其中
\(\mathbf{A}\)
为已知矩阵,
\(\mathbf{v}\)
为其中一个本征向量,
\(\lambda\)
是其中一个本征值。求解这个本征方程,就是找到所有符合条件的本征向量和对应的本征值。如果把所有的本征向量用一个本征矩阵
\(\mathbf{V}\)
来表示,那么就得到了一个特征值分解(EVD):

\[\mathbf{A} = \mathbf{V}\Sigma\mathbf{V}^{-1}
\]

其中
\(\Sigma\)
是由所有的特征值
\(\lambda\)
组成的对角矩阵。该形式的分解与另外一种SVD奇异值分解,在各种数据降维和稀疏化中经常会用到。

代码示例

这里用IPython做一个简单的功能演示:

In [1]: import numpy as np

In [2]: x = np.random.random((3,3)) # 生成一个随机3x3矩阵

In [3]: x
Out[3]: 
array([[0.85976743, 0.98470964, 0.93286037],
       [0.4988825 , 0.36451386, 0.68983566],
       [0.01818865, 0.27647914, 0.86250282]])

In [4]: vals, vecs = np.linalg.eig(x) # 求解本征值和本征矩阵

In [5]: vals # 得到的本征值
Out[5]: array([-0.17227694,  1.59456701,  0.66449404])

In [6]: vecs # 得到的本征列向量构成的矩阵
Out[6]: 
array([[-0.57443338,  0.86571324, -0.83117188],
       [ 0.79327234,  0.46077017, -0.28656555],
       [-0.20185463,  0.19552861,  0.47648032]])

In [7]: np.allclose(vecs @ np.diag(vals) @ np.linalg.inv(vecs), x) # 测试本征值分解EVD
Out[7]: True

In [13]: np.allclose(x @ vecs[:, 0], vecs[:, 0] * vals[0]) # 测试本征向量
Out[13]: True

In [14]: np.allclose(x @ vecs[:, 1], vecs[:, 1] * vals[1]) # 测试本征向量
Out[14]: True

In [15]: np.allclose(x @ vecs[:, 2], vecs[:, 2] * vals[2]) # 测试本征向量
Out[15]: True

可以看到,EVD分解还原之后的矩阵跟原矩阵是保持一致的。这里逆矩阵的运算,也是用到了numpy的另外一个操作:矩阵求逆函数
numpy.linalg.inv

总结概要

本文介绍了一下使用Numpy计算矩阵的特征值求解和特征值分解问题。Numpy的eig特征求解函数可以直接输出给定矩阵所有的特征值,和对应的所有特征列向量所构成的矩阵。再使用Numpy的矩阵求逆函数,即可得到相关矩阵的EVD特征值分解。

版权声明

本文首发链接为:
https://www.cnblogs.com/dechinphy/p/numpy-eig.html

作者ID:DechinPhy

更多原著文章:
https://www.cnblogs.com/dechinphy/

请博主喝咖啡:
https://www.cnblogs.com/dechinphy/gallery/image/379634.html


Streamlit
中,
Form
组件是一种特殊的UI元素,允许用户输入数据而不立即触发应用的重新运行。

这对于创建需要用户输入多个参数后再进行处理的交互式表单非常有用。

1. 概要

Form
组件的主要作用是在一个表单内集中处理多个用户输入,使得数据收集和验证更加高效和直观。

通过
Form
组件,开发者可以创建包含多个输入控件(如文本输入框、下拉选择框等)的表单,用户可以在表单内一次性填写所有必要的信息,然后提交。

这避免了传统表单提交时每次输入都会触发页面刷新的问题,从而提高了用户体验和应用的交互性。

根据
Form
组件的特点,在类似下面这些场景中,我们可以考虑使用
Form

  1. 用户注册与登录
    :通过
    Form
    组件构建一个包含用户名、密码、邮箱等多个输入组件,以及一个提交按钮的页面,并在用户点击提交按钮后才开始进行验证和处理。
  2. 数据查询与筛选
    :通过
    Form
    组件可以包含多个选择框、输入框等组件,用于收集用户的查询或筛选条件。
  3. 参数配置与设置
    :在构建复杂的Web应用程序时,可能需要用户配置或设置一些参数,这些参数可能包括算法参数、界面样式等。通过
    Form
    组件,可以集中展示和配置这些参数。
  4. 多步骤表单处理
    :通过
    Form
    组件,开发者可以创建包含多个步骤的表单,并在用户完成每个步骤后收集相应的数据。
  5. 动态表单生成
    :在某些高级应用场景中,可能需要根据用户的选择或输入动态生成表单。例如,在构建在线问卷时,可能需要根据用户的选择展示不同的问题。

总之,
Streamlit

Form
组件在很多应用场景中都发挥着重要作用,特别是在需要收集和处理多个用户输入的场景中表现尤为突出。

2. 主要参数

Form
组件的参数很简单,主要用来简单的控制样式和提交的行为。

名称 类型 说明
key str 组件名称,具有唯一性
clear_on_submit bool 用户提交后,表单内的所有组件是否都重置为默认值
enter_to_submit bool 当用户在与表单内的组件交互时,按
Enter
键时是否提交表单
border bool 是否在窗体周围显示边框

Form
组件本身并不直接接受各种组件来作为参数,但表单内部可以包含多种输入组件,如文本框(
st.text_input
)、选择框(
st.selectbox
)、滑块(
st.slider
)等。

此外,
Form
组件需要配合
st.form_submit_button
来创建一个提交按钮。

3. 使用示例

下面通过一些根据实际场景来简化的示例,演示
Form
组件的使用方式。

3.1. 数据预处理参数设置

在数据分析或机器学习项目中,数据预处理是一个关键步骤。

我们可以使用
Form
组件来让用户选择数据预处理的参数,如缺失值处理方法和特征缩放方法。

import streamlit as st
import pandas as pd
from sklearn.preprocessing import StandardScaler, MinMaxScaler

# 加载示例数据集
data = pd.DataFrame(
    {"feature1": [1, 2, None, 4, 5], "feature2": [10, 20, 30, None, 50]}
)


# 定义表单提交后的回调函数
def preprocess_data(fill_method, scale_method):
    if fill_method == "mean":
        data.fillna(data.mean(), inplace=True)
    elif fill_method == "median":
        data.fillna(data.median(), inplace=True)
    else:
        data.dropna(inplace=True)

    if scale_method == "standard":
        scaler = StandardScaler()
    elif scale_method == "minmax":
        scaler = MinMaxScaler()
    else:
        scaler = None

    if scaler:
        data_scaled = pd.DataFrame(
            scaler.fit_transform(data),
            columns=data.columns,
        )
        st.write(data_scaled)
    else:
        st.write(data)


# 创建表单
with st.form(key="preprocess_form"):
    fill_method = st.selectbox(label="缺失值处理", options=["mean", "median", "drop"])
    scale_method = st.selectbox(
        label="特征缩放", options=["standard", "minmax", "none"]
    )
    submitted = st.form_submit_button(label="提交")
    if submitted:
        preprocess_data(fill_method, scale_method)

运行效果如下,【提交】按钮点击后才会刷新页面。

3.2. 机器学习模型超参数调优

在训练机器学习模型时,超参数的选择对模型性能有很大影响。

我们可以使用
Form
组件来让用户选择模型的超参数,并展示模型在验证集上的性能。

import streamlit as st
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score

# 加载示例数据集
data = load_iris()
X, y = data.data, data.target
X_train, X_val, y_train, y_val = train_test_split(
    X,
    y,
    test_size=0.2,
    random_state=42,
)


# 定义表单提交后的回调函数
def train_model(n_estimators, max_depth):
    model = RandomForestClassifier(
        n_estimators=n_estimators,
        max_depth=max_depth,
        random_state=42,
    )
    model.fit(X_train, y_train)
    y_pred = model.predict(X_val)
    accuracy = accuracy_score(y_val, y_pred)
    st.write(f"准确率: {accuracy:.2f}")


# 创建表单
with st.form(key="model_form"):
    n_estimators = st.number_input(
        label="Estimators 数量",
        min_value=10,
        max_value=200,
        step=10,
        value=100,
    )
    max_depth = st.number_input(
        label="最大深度",
        min_value=1,
        max_value=20,
        step=1,
        value=10,
    )

    submitted = st.form_submit_button(label="开始训练")
    if submitted:
        train_model(n_estimators, max_depth)

运行界面如下,点击【开始训练】按钮后显示训练后模型的准确率。

4. 总结

总的来说,
Streamlit

Form
组件能够帮助我们简化表单的创建和数据收集的过程,使我们能够轻松构建具有复杂交互功能的数据应用。

做全文搜索,es比较好用,安装可能有点费时费力。mysql安装就不说了。主要是elastic8.4.0+kibana8.4.0+logstash-8.16.1,可视化操作及少量netcore查询代码。

安装elastic8.4.0+kibana8.4.0使用docker-desktop,logstash-8.16.1是线程解压执行文件。

  • 1.
    docker-compose.yml 如下: 首先使用docker network创建一个es-net内部通讯网络,这样kibana连接es可以通过容器名ELASTICSEARCH_HOSTS=http://elasticsearch:9200,此作为单机测试使用单机的es.
services:

elasticsearch:
container_name: elasticsearch
image: docker.elastic.co
/elasticsearch/elasticsearch:8.4.0environment:- discovery.type=single-node
ulimits:
memlock:
soft:
-1hard:-1cap_add:-IPC_LOCK
ports:
- "9200:9200"networks:- es-net

kibana:
container_name: kibana
image: docker.elastic.co
/kibana/kibana:8.4.0environment:- ELASTICSEARCH_HOSTS=http://elasticsearch:9200 ports:- "5601:5601"networks:- es-net

networks:
es
-net:
driver: bridge

作为es的8以上版本是有账号密码和crt证书的,需要做如下配置:

安装好es后默认给一个elastic账号,需要重置一下密码,进入es容器执行重置密码命令,会给你一个密码。

docker exec  -it -u root elasticsearch /bin/bash
bin
/elasticsearch-reset-password -u elastic

这里登录的其实是https带证书的,但是kibana使用的是http的,所以在容器内部,config/elasticsearch.yml中需要把下面的两个参数置为false ,生产环境不建议这么操作。

因为es带账号密码,所以kibana连接es也需要账号密码信息,但是默认的elastic是超级管理员,kibana默认是不支持的,需要自己新建账号。但是es默认是给了账号的,用他的就行。自己新建es账号给一个超级管理员角色依然没有重建所应权限,导致kibana起不来,用kibana_system就行。

进入es容器内部给kibana_system重置一个密码,用下面的命令在内部调用也行,我设置的elastic和kibana_system的密码一样,方便使用。

curl -u elastic:DiVnR2F6OGYmP+Ms+n2o -X POST "http://localhost:9200/_security/user/kibana_system/_password" -H 'Content-Type: application/json' -d'{"password": "DiVnR2F6OGYmP+Ms+n2o"}'

  • 2.
    然后在kibana容器中,加上账号密码信息即可,重启。还有最后一行加上i18n.locale: zh-CN  ,改变ui为中文。

然后通过开发工具就可以做es的调试了,这里注意下需要中文分词的可以去 https://github.com/infinilabs/analysis-ik/releases 下载对应版本8.4.0的中文分词器 ,改个名放到es容器内plugins中去。也可以自定义分词文件丢进去

  • 3. 下面就是logstash安装跟mysql的同步了,测试数据如下:

首先去logstash官网下载对应的包,我选的版本是8.16.1,目录如下是可以通过控制台执行的。

这里只需要配置好mysql-connector的驱动和链接信息即可。

jdbc.conf文件内容如下:

input {
stdin {}
jdbc {
type
=> "jdbc"# 数据库连接地址
jdbc_connection_string
=> "jdbc:mysql://192.168.200.2:3306/bbs?characterEncoding=UTF-8&autoReconnect=true"# 数据库连接账号密码;
jdbc_user
=> "admin"jdbc_password=> "这是密码"# MySQL依赖包路径;
jdbc_driver_library
=> "D:\software\logstash-8.16.1\mysql\mysql-connector-j-8.0.32.jar"# the name of the driverclass formysql
jdbc_driver_class
=> "com.mysql.jdbc.Driver"# 数据库重连尝试次数
connection_retry_attempts
=> "3"# 判断数据库连接是否可用,默认false不开启
jdbc_validate_connection
=> "true"# 数据库连接可用校验超时时间,默认3600S
jdbc_validation_timeout
=> "3600"# 开启分页查询(默认false不开启);
jdbc_paging_enabled
=> "true"# 单次分页查询条数(默认100000,若字段较多且更新频率较高,建议调低此值);
jdbc_page_size
=> "500"# statement为查询数据sql,如果sql较复杂,建议配通过statement_filepath配置sql文件的存放路径;
# sql_last_value为内置的变量,存放上次查询结果中最后一条数据tracking_column的值,此处即为ModifyTime;
# statement_filepath
=> "mysql/jdbc.sql"statement=> "SELECT ArticleID,UserID,ArticleTitle,ArticleContent,ImageAddress,StandPoint,PublishTime,`Status`,Likes, Shares,Comments,Reports, Sort,PublishingMode,SourceType,Reply,IsTop,TopEndTime,Hot,EditUserId,CreatedTime,EditTime,UserType,UserNickname,ForbiddenState,PublishDateTime,TopArea,SubscribeType,CollectionCount,Articletype,NewsID,CommentUserCount,TopStartTime,`View`,ViewDuration,Forwardings,ForwardingFId,Freshness,Shelf_Reason,AuditTime FROM bbs_articles"# 是否将字段名转换为小写,默认true(如果有数据序列化、反序列化需求,建议改为false);
lowercase_column_names
=> false# Value can be any of: fatal,error,warn,info,debug,默认info;
sql_log_level
=>warn
#
# 是否记录上次执行结果,true表示会将上次执行结果的tracking_column字段的值保存到last_run_metadata_path指定的文件中;
record_last_run
=> true# 需要记录查询结果某字段的值时,此字段为true,否则默认tracking_column为timestamp的值;
use_column_value
=> true# 需要记录的字段,用于增量同步,需是数据库字段
tracking_column
=> "PublishTime"# Value can be any of: numeric,timestamp,Default valueis "numeric"tracking_column_type=>timestamp
# record_last_run上次数据存放位置;
last_run_metadata_path
=> "mysql/last_id.txt"# 是否清除last_run_metadata_path的记录,需要增量同步时此字段必须为false;
clean_run
=> false#
# 同步频率(分 时 天 月 年),默认每分钟同步一次;
schedule
=> "* * * * *"}
}

filter {
json {
source
=> "message"remove_field=> ["message"]
}
# convert 字段类型转换,将字段TotalMoney数据类型改为float;
mutate {
convert
=>{
#
"TotalMoney" => "float"}
}
}
output {
elasticsearch {
# host
=> "127.0.0.1"# port=> "9200"# 配置ES集群地址
# hosts
=> ["192.168.1.1:9200", "192.168.1.2:9200", "192.168.1.3:9200"]
hosts
=> ["127.0.0.1:9200"]
user
=> "elastic"password=> "DiVnR2F6OGYmP+Ms+n2o"ssl=> false# 索引名字,必须小写
index
=> "bbs_act"# 数据唯一索引(建议使用数据库KeyID)
document_id
=> "%{ArticleID}"}
stdout {
codec
=>json_lines
}
}

配置文成后执行该命令,数据实时同步开始

bin\logstash.bat -f mysql\jdbc.conf

可以通过kibana的discover查看数据,也可以通过开发工具查询,elk日志就是这么玩。

  • 4. 下面就是代码,这里的实体没给全,注意实体需要给Text的Name属性,否则会解析不到数据的:
 public class ArticleEsContext : EsBase<ArticleDto>{public ArticleEsContext(EsConfig esConfig) : base(esConfig)
{
}
public override string IndexName => "bbs_act";public async Task<List<ArticleDto>>GetArticles(ArticleParameter parameter)
{
var client =_esConfig.GetClient(IndexName);//计算分页的起始位置 var from = (parameter.PageNumber - 1) *parameter.PageSize;var searchResponse = await client.SearchAsync<ArticleDto>(s =>s
.Index(IndexName)
.Query(q
=>q
.Bool(b
=>b
.Should(
sh
=> sh.Match(m =>m
.Field(f
=> f.ArticleTitle) //查询 ArticleTitle .Query(parameter.KeyWords)
.Fuzziness(Fuzziness.Auto)
//启用模糊查询 ),
sh
=> sh.Match(m =>m
.Field(f
=> f.ArticleContent) //查询 ArticleContent .Query(parameter.KeyWords)
.Fuzziness(Fuzziness.Auto)
//启用模糊查询 )
)
.MinimumShouldMatch(
1) //至少一个条件必须匹配 )
)
.From(
from) //设置分页的起始位置 .Size(parameter.PageSize) //设置每页大小 );if (!searchResponse.IsValid)
{
Console.WriteLine(searchResponse.DebugInformation);
return new List<ArticleDto>();
}
returnsearchResponse.Documents.ToList();
}
}
public classArticleDto
{
[Text(Name
= "ArticleID")]public int ArticleId { get; set; }
[Text(Name
= "ArticleTitle")]public string ArticleTitle { get; set; }
[Text(Name
= "ArticleContent")]public string ArticleContent { get; set; }
[Date(Name
= "CreatedTime")]public DateTime CreatedTime { get; set; }
}

代码调用结果如下:

一、数据库

linux下登录:

mysql -u root -p

查看数据库:

show databases;

可以在phpmyadmin面板点击SQL进行操作

1. 增加/创建

创建xxx数据库,并使用utf-8编码

create database xxx charset utf8;

2. 删除

删除xxx数据库

drop database xxx;

3. 选择进入数据库

进入xxx数据库

use xxx;

二、数据表

1. 增加/创建表

create table xxx;

定义表属性

varchar(40)字段可以存储的最大字符数为40个字符

(id int,
name varchar(40),
sex char(4),
birthday date,
job varchar(100)
);

这么使用

create table track(id int,
name varchar(40),
sex char(4),
birthday date,
job varchar(100)
);

形式如下:

2. 查看

查看数据表信息

show full columns from xxx;

结果:

查看数据表列表,* 代表所有列表

select * from xxx;

结果:

3. 删除

删除数据表

drop table xxx;
delete from xxx;

4. 修改

修改数据表名xxx为yyy

rename table xxx to yyy;

三、数据列和数据行

1. 增加/创建

增加一行

insert into xxx(id,name,sex,birthday,job)
values(1,'track','男','2000-00-00','IT');

结果:

增加一列

在xxx表中增加一列名为zenjia,可以存储最多8位数字,其中2位是小数点后的数字,-99999999.99 到 99999999.99

alter table xxx add zenjia decimal(8,2);

结果:

2. 修改

修改xxx表zenjia列所有值为5000

update xxx set zenjia=5000;

结果:

修改xxx表id=1的行,name值为name1

update xxx set name='name1' where id=1;

结果:

修改xxx表id=1的行,name=name2,zenjia列第一行的值为2000

update xxx set name='name2',zenjia=2000 where id=1;

结果:

3. 删除

删除列

删除zenjia列

alter table xxx drop zenjia;

删除行

删除job列值为it的行,不区分大小写

delete from xxx where job='it';

结果:

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