Apache Calcite - 查询优化之自定义优化规则

RelOptRule简介

为了自定义优化规则,我们需要继承RelOptRule类。org.apache.calcite.plan.RelOptRule 是 Apache Calcite 中的一个抽象类,用于定义优化规则。优化规则是用于匹配查询计划中的特定模式,并将其转换为更优化的形式的逻辑。通过继承 RelOptRule,你可以创建自定义的优化规则,以满足特定的优化需求。

RelOptRule 的主要作用包括:

  • 定义匹配模式:通过构造函数中的 RelOptRuleOperand,定义规则所匹配的关系表达式树的模式。
  • 实现匹配逻辑:通过实现 onMatch 方法,定义当匹配模式被识别时,如何转换关系表达式树。
  • 管理规则应用:RelOptRule 还负责管理规则的应用,包括检查规则是否适用以及如何应用规则。

一个常见的自定义规则实现如下:

    class CustomFilterProjectTransposeRule extends RelOptRule {protected CustomFilterProjectTransposeRule(RelOptRuleOperand operand) {// 定义匹配模式super(operand);}@Overridepublic void onMatch(RelOptRuleCall call) {// 触发匹配后,执行优化动作}}

RelOptRule 的关键组成部分

1. 构造函数

RelOptRule 的构造函数用于定义规则的匹配模式。匹配模式是通过 RelOptRuleOperand 来描述的,RelOptRuleOperand 定义了规则所匹配的关系表达式树的结构。

RelOptRule(RelOptRuleOperand operand, String description)

  • operand:描述优化规则匹配模式的一个关键类。它定义了规则所匹配的关系表达式树的结构,并且在 RelOptRule 中被用来指定规则的匹配条件。
  • description:规则的描述信息,通常用于调试和日志记录。

RelOptRuleOperand 的主要作用包括:

  • 描述匹配模式:通过指定关系表达式的类型和层次结构,描述优化规则所匹配的模式。
  • 支持递归匹配:允许定义嵌套的匹配模式,从而支持复杂的关系表达式树的匹配。
  • 配置匹配条件:可以通过各种配置选项来精确控制匹配行为,例如是否匹配子节点、是否匹配某些特定属性等。

2. onMatch 方法

onMatch 方法是一个抽象方法,必须在子类中实现。当规则匹配时,onMatch 方法会被调用,以执行具体的转换逻辑。

public abstract void onMatch(RelOptRuleCall call);

  • call:RelOptRuleCall 对象,包含了匹配的关系表达式节点,并提供了一些方法来转换这些节点。

RelOptRuleOperand

RelOptRuleOperand 是用于描述规则匹配模式的类。它定义了规则所匹配的关系表达式树的结构。

构造函数如下

<R extends RelNode> RelOptRuleOperand(Class<R> clazz,@Nullable RelTrait trait,Predicate<? super R> predicate,RelOptRuleOperandChildPolicy childPolicy,ImmutableList<RelOptRuleOperand> children)
  • Class clazz: 这是一个泛型参数,指定了匹配的关系表达式节点的类型。例如,Filter.class 或 Project.class。R 是继承自 RelNode 的类型。
  • @Nullable RelTrait trait: 这是一个可选参数,指定了匹配节点的特性(trait)。特性可以用来描述节点的一些额外属性,例如排序、分区等。
    如果不需要特定的特性,可以传入 null。
  • Predicate<? super R> predicate: 这是一个谓词,用于进一步限制匹配的节点。只有当节点满足这个谓词时,才会匹配成功。例如,你可以使用谓词来检查节点的某些属性或状态。
  • RelOptRuleOperandChildPolicy childPolicy: 这是一个枚举类型,指定了子节点的匹配策略。常见的策略包括:
    • ANY:匹配任意数量的子节点。
    • SOME:匹配至少一个子节点。
    • LEAF:匹配没有子节点的节点。
    • UNORDERED:匹配子节点的顺序不重要。这个参数用于控制匹配模式中子节点的数量和顺序。
  • ImmutableList children:这是一个不可变列表,包含了子节点的匹配模式。每个子节点的匹配模式也是一个 RelOptRuleOperand 实例。通过嵌套定义,可以描述复杂的关系表达式树的匹配模式。

实际创建RelOptRuleOperand我们通过工厂方法来完成,这个工厂方法的主要功能是创建一个 RelOptRuleOperand 实例。它通过调用 RelOptRuleOperand 的构造函数,传递必要的参数来初始化匹配模式。

  public static <R extends RelNode> RelOptRuleOperand operand(Class<R> clazz,RelOptRuleOperand first,RelOptRuleOperand... rest) {return operand(clazz, some(first, rest));}
  • Class clazz: 这是一个泛型参数,指定了匹配的关系表达式节点的类型。例如,Filter.class 或 Project.class。R 是继承自 RelNode 的类型。
  • RelOptRuleOperand first:这是第一个子节点的匹配模式,类型为 RelOptRuleOperand。
  • RelOptRuleOperand… rest: 这是一个可变参数,表示零个或多个额外的子节点的匹配模式,类型为 RelOptRuleOperand。

这里的节点均是指关系表达式树种的节点。

RelOptRuleCall

类是优化规则(RelOptRule)应用过程中非常关键的一个类。它用于表示在优化过程中某个规则的匹配和应用状态,并提供了相关方法来处理匹配到的关系表达式(RelNode)树。

方法

  • transformTo(RelNode rel):这个方法用于将变换后的关系表达式节点提交给优化器。参数 rel 是变换后的关系表达式节点。
    调用这个方法后,优化器会将新的节点纳入进一步的优化过程中。
  • getPlanner():返回当前的优化器实例(RelOptPlanner)。
  • getRule():返回当前正在应用的优化规则(RelOptRule)。
  • rel(int ordinal): 返回匹配到的关系表达式节点。参数 ordinal 是节点在匹配模式中的位置索引。
  • getChild(int ordinal):返回匹配到的关系表达式节点的子节点。参数 ordinal 是子节点在匹配模式中的位置索引。

rel(int ordinal)方法 ordinal参数
假设我们定义了一个匹配模式,用于匹配一个 Filter 节点,其子节点是一个 Project 节点,而 Project 节点的子节点是一个 TableScan 节点。这个匹配模式可以通过以下方式定义:

  • Filter 节点的位置索引为 0。
  • Project 节点的位置索引为 1。
  • TableScan 节点的位置索引为 2。

这些索引是根据匹配模式中节点的定义顺序确定的。

节点的输入(inputs)

通常指的是这个节点的子节点。在关系表达式树中,每个节点都可以有一个或多个输入节点,这些输入节点就是它的子节点。输入节点的结构和关系决定了整个关系表达式树的结构。

在关系表达式树中,每个节点代表一个关系操作(如筛选、投影、连接等),而这些节点通过输入节点(子节点)连接在一起,形成一个树形结构。根节点表示最终的查询结果,而叶节点通常表示数据源(如表扫描)。

输入节点(子节点)是关系表达式树中每个节点的直接子节点。它们定义了当前节点的操作对象。例如:

  • Filter 节点:其输入节点是需要过滤的数据源,可以是一个表扫描节点(TableScan)或另一个操作节点(如投影节点)。
  • Project 节点:其输入节点是需要投影的数据源,可以是一个表扫描节点或另一个操作节点(如过滤节点)。
  • Join 节点:其输入节点是需要进行连接的两个数据源,可以是表扫描节点或其他操作节点。

一个自定义优化规则的例子

输入sql

SELECT name, age
FROM (SELECT name, age, salaryFROM employees
) AS subquery
WHERE age > 30

我们生成对应的关系表达式树

LogicalProject(department=[$1])LogicalFilter(condition=[>($0, 10)])LogicalProject(id=[$0], department=[$1])LogicalTableScan(table=[[MY_SCHEMA, department_table]])

自定义优化规则,自定义规则的作用就是将过滤下推到投影之前。最终调用transform方法
将整个匹配到的子树(即 Filter 节点及其子节点 Project)替换为新的子树(即 newProject 节点及其子节点)。

    public class CustomFilterProjectTransposeRule extends RelOptRule {int invoke = 0;private CustomFilterProjectTransposeRule() {super(operand(Filter.class, operand(Project.class, any())), "CustomFilterProjectTransposeRule");// 定义子节点的匹配模式}@Overridepublic void onMatch(RelOptRuleCall call) {// 获取匹配的 Filter 和 Project 节点final Filter filter = call.rel(0);final Project project = call.rel(1);// 创建一个新的 Project 节点,其子节点为原 Project 节点的输入final RelNode newFilter = filter.copy(filter.getTraitSet(), Lists.newArrayList(project.getInput()));// 创建一个新的 Filter 节点,其输入为新的 Project 节点final RelNode newProject = project.copy(project.getTraitSet(), Lists.newArrayList(newFilter));// 将新生成的节点替换原来的节点call.transformTo(newProject);}}

输出的关系表达式树为

优化前:
LogicalProject(id=[$0], department=[$1])LogicalFilter(condition=[>($0, 10)])LogicalProject(id=[$0], department=[$1])LogicalTableScan(table=[[MY_SCHEMA, department_table]])优化后: 
LogicalProject(id=[$0], department=[$1])LogicalProject(id=[$0], department=[$1])LogicalFilter(condition=[>($0, 10)])LogicalTableScan(table=[[MY_SCHEMA, department_table]])

优化后的SQL:

SELECT "id", "department"
FROM "MY_SCHEMA"."department_table"
WHERE "id" > 10

完整代码

package calcite.optimization;/*** @author xxx*/import java.sql.Connection;
import java.sql.DriverManager;
import java.util.Properties;
import org.apache.calcite.config.Lex;
import org.apache.calcite.jdbc.CalciteConnection;
import org.apache.calcite.plan.RelOptRule;
import org.apache.calcite.plan.RelOptRuleCall;
import org.apache.calcite.plan.hep.HepPlanner;
import org.apache.calcite.plan.hep.HepProgram;
import org.apache.calcite.plan.hep.HepProgramBuilder;
import org.apache.calcite.rel.RelNode;
import org.apache.calcite.rel.RelRoot;
import org.apache.calcite.rel.core.Filter;
import org.apache.calcite.rel.core.Project;
import org.apache.calcite.rel.rel2sql.RelToSqlConverter;
import org.apache.calcite.rel.rel2sql.SqlImplementor.Result;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rel.type.RelDataTypeFactory;
import org.apache.calcite.schema.Schema;
import org.apache.calcite.schema.SchemaPlus;
import org.apache.calcite.schema.Table;
import org.apache.calcite.schema.impl.AbstractSchema;
import org.apache.calcite.schema.impl.AbstractTable;
import org.apache.calcite.sql.SqlNode;
import org.apache.calcite.sql.dialect.MysqlSqlDialect;
import org.apache.calcite.sql.parser.SqlParser;
import org.apache.calcite.sql.pretty.SqlPrettyWriter;
import org.apache.calcite.sql.validate.SqlConformanceEnum;
import org.apache.calcite.tools.FrameworkConfig;
import org.apache.calcite.tools.Frameworks;
import org.apache.calcite.tools.Planner;
import org.assertj.core.util.Lists;
import org.junit.Test;/*** 自定义规则优化SQL演示* @ClassName SqlToRelNode* @Author xxx* @Date 2024/10/15 20:20**/
public class OptFromCustomRule {public class CustomFilterProjectTransposeRule extends RelOptRule {private CustomFilterProjectTransposeRule() {super(operand(Filter.class, operand(Project.class, any())), "CustomFilterProjectTransposeRule");// 定义子节点的匹配模式}@Overridepublic void onMatch(RelOptRuleCall call) {// 获取匹配的 Filter 和 Project 节点final Filter filter = call.rel(0);final Project project = call.rel(1);// 创建一个新的 Project 节点,其子节点为原 Project 节点的输入final RelNode newFilter = filter.copy(filter.getTraitSet(), Lists.newArrayList(project.getInput()));// 创建一个新的 Filter 节点,其输入为新的 Project 节点final RelNode newProject = project.copy(project.getTraitSet(), Lists.newArrayList(newFilter));// 将新生成的节点替换原来的节点call.transformTo(newProject);}}/*** 创建配置的时候应该建什么配置* Sql转关系代数表达式*/@Testpublic void testSqlToRelNode() throws Exception{// 1. 设置内存数据库连接Properties info = new Properties();Connection connection = DriverManager.getConnection("jdbc:calcite:", info);CalciteConnection calciteConnection = connection.unwrap(CalciteConnection.class);// 2. 创建自定义SchemaSchemaPlus rootSchema = calciteConnection.getRootSchema();Schema schema = new AbstractSchema() {};rootSchema.add("MY_SCHEMA", schema);// 3. 添加表到自定义SchemaTable yourTable = new AbstractTable() {@Overridepublic RelDataType getRowType(RelDataTypeFactory typeFactory) {// 如果要动态分析表,那么就自己去创建return typeFactory.builder().add("id", typeFactory.createJavaType(int.class)).add("name", typeFactory.createJavaType(String.class)).add("age", typeFactory.createJavaType(int.class)).build();}};// 3. 添加表到自定义SchemaTable department_table = new AbstractTable() {@Overridepublic RelDataType getRowType(RelDataTypeFactory typeFactory) {// 如果要动态分析表,那么就自己去创建return typeFactory.builder().add("id", typeFactory.createJavaType(int.class)).add("department", typeFactory.createJavaType(String.class)).add("location", typeFactory.createJavaType(String.class)).build();}};rootSchema.getSubSchema("MY_SCHEMA").add("your_table", yourTable);rootSchema.getSubSchema("MY_SCHEMA").add("department_table", department_table);// 4. 配置SQL解析器SqlParser.Config parserConfig = SqlParser.config().withLex(Lex.MYSQL).withConformance(SqlConformanceEnum.MYSQL_5);// 5. 配置框架FrameworkConfig config = Frameworks.newConfigBuilder().parserConfig(parserConfig).defaultSchema(rootSchema.getSubSchema("MY_SCHEMA")) // 使用自定义Schema.build();// 6. 创建Planner实例Planner planner = Frameworks.getPlanner(config);// 7. 解析SQLString sql = "SELECT id,department FROM (SELECT id, department FROM department_table) as d WHERE id > 10 ";
//        String sql = "SELECT * FROM your_table where id = 1 and name = 'you_name'";SqlNode sqlNode = planner.parse(sql);// 8. 验证SQLSqlNode validatedSqlNode = planner.validate(sqlNode);// 9. 转换为关系表达式RelRoot relRoot = planner.rel(validatedSqlNode);// 10. 获取RelNodeRelNode rootRelNode = relRoot.rel;// 打印RelNode的信息System.out.println(rootRelNode.explain());// 创建HepProgramHepProgram hepProgram = new HepProgramBuilder().addRuleInstance(new CustomFilterProjectTransposeRule()).build();// 创建HepPlannerHepPlanner hepPlanner = new HepPlanner(hepProgram);// 设置根RelNodehepPlanner.setRoot(rootRelNode);// 进行优化RelNode optimizedRelNode = hepPlanner.findBestExp();// 输出优化后的RelNodeSystem.out.println("优化后的RelNode: \n" + optimizedRelNode.explain());// 10. 使用RelToSqlConverter将优化后的RelNode转换回SQLRelToSqlConverter relToSqlConverter = new RelToSqlConverter(MysqlSqlDialect.DEFAULT);Result result = relToSqlConverter.visitRoot(optimizedRelNode);SqlNode sqlNodeConverted = result.asStatement();// 11. 使用SqlPrettyWriter格式化SQLSqlPrettyWriter writer = new SqlPrettyWriter();String convertedSql = writer.format(sqlNodeConverted);// 输出转换后的SQLSystem.out.println("优化后的SQL: " + convertedSql);// 关闭连接connection.close();}
}

总结

自定义优化规则需要定义好匹配模式来匹配关系表达式树种个某一个部分,最终应用转换规则调整关系表达式树种子树的位置。

  • 定义匹配模式:在自定义优化规则中,需要定义一个匹配模式,用于匹配关系表达式树中的特定部分。匹配模式通常是通过定义操作数(operand)来实现的,这些操作数指定了需要匹配的关系表达式节点及其层次结构。
  • 匹配关系表达式树:优化器会使用定义的匹配模式在关系表达式树中查找符合条件的子树。当找到匹配的子树时,优化器会触发相应的优化规则。
  • 应用转换规则:在匹配到关系表达式树的特定部分后,优化规则会应用转换逻辑。转换逻辑通常涉及创建新的关系表达式节点,并调整这些节点的位置或结构,以实现优化目的。
    调整关系表达式树:

最终,应用转换规则会调整关系表达式树中的子树位置。这可能包括将某些节点下推、上拉、合并或分解等操作,从而优化查询计划,提高查询执行效率。

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

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

相关文章

2024网鼎杯青龙组wp:Crypto1

题目 附件内容如下 from Crypto.Util.number import * from secret import flag from Cryptodome.PublicKey import RSAp getPrime(512) q getPrime(512) n p * q d getPrime(299) e inverse(d,(p-1)*(q-1)) m bytes_to_long(flag) c pow(m,e,n) hint1 p >> (51…

Python 单元测试中的 Mocking 与 Stubbing:提高测试效率的关键技术

在软件开发过程中&#xff0c;单元测试是确保代码质量的重要环节。为了实现高效的单元测试&#xff0c;我们常常需要隔离待测试的代码与其外部依赖。这时候&#xff0c;Mocking&#xff08;模拟&#xff09;和 Stubbing&#xff08;桩&#xff09;技术就显得尤为重要。这两种技…

Golang | Leetcode Golang题解之第528题按权重随机选择

题目&#xff1a; 题解&#xff1a; type Solution struct {pre []int }func Constructor(w []int) Solution {for i : 1; i < len(w); i {w[i] w[i-1]}return Solution{w} }func (s *Solution) PickIndex() int {x : rand.Intn(s.pre[len(s.pre)-1]) 1return sort.Searc…

3D打印机 屏幕的固定挂钩断后的一次自己修复经历

引子 3D打印机的屏幕固定挂钩断了 这次确实不知道咋断的&#xff0c;这可咋办呢&#xff0c;到网上看了一下&#xff0c;一个屏幕要2佰多&#xff0c;有些小贵&#xff0c;要不就自己修修吧&#xff0c;打个挂钩按上&#xff0c;说干就干。 正文 freecad的设计图如下【其中各…

PHP合成图片,生成海报图,poster-editor使用说明

之前写过一篇使用Grafika插件生成海报图的文章&#xff0c;但是当我再次使用时&#xff0c;却发生了错误&#xff0c;回看Grafika文档&#xff0c;发现很久没更新了&#xff0c;不兼容新版的GD&#xff0c;所以改用了intervention/image插件来生成海报图。 但是后来需要对海报…

Java基于微信小程序的美食推荐系统(附源码,文档)

博主介绍&#xff1a;✌程序猿徐师兄、8年大厂程序员经历。全网粉丝15w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;…

Linux的IP网路命令: 用于显示和操作网络接口(网络设备)的命令ip link详解

目录 一、概述 二、用法 1、基本语法 2、常用选项 3、常用参数 4、获取帮助 三、示例 1. 显示所有网络接口的信息 &#xff08;1&#xff09;命令 &#xff08;2&#xff09;输出示例 &#xff08;3&#xff09;实际操作 2. 启动网络接口 3. 停止网络接口 4. 更改…

【驱动】地平线X3交叉编译工具搭建、源码下载

1、交叉编译工具搭建 1)安装依赖包 sudo apt-get install -y build-essential make cmake libpcre3 libpcre3-dev bc bison \ flex python3-numpy mtd-utils zlib1g-dev debootstrap \ libdata-hexdumper-perl libncurses5-dev zip qemu-user-static \ curl repo git liblz4…

服务器上清理Docker容器运行日志的正确方法

为啥要清理服务器上docker容器的日志 因为是服务器的磁盘空间资源有限&#xff0c;由于docker容器在启动的时候没有限制&#xff0c;导致运行的docker容器随着时间的推移产生的日志越来越多&#xff0c;最后把服务磁盘资源耗尽&#xff0c;服务器的磁盘满了会导致服务器的应用无…

C语言 | Leetcode C语言题解之第526题优美的排列

题目&#xff1a; 题解&#xff1a; int countArrangement(int n) {int f[1 << n];memset(f, 0, sizeof(f));f[0] 1;for (int mask 1; mask < (1 << n); mask) {int num __builtin_popcount(mask);for (int i 0; i < n; i) {if (mask & (1 <<…

SpringBoot篇(自动装配原理)

目录 一、自动装配机制 1. 简介 2. 自动装配主要依靠三个核心的关键技术 3. run()方法加载启动类 4. 注解SpringBootApplication包含了多个注解 4.1 SpringBootConfiguration 4.2 ComponentScan 4.3 EnableAutoConfiguration 5. SpringBootApplication一共做了三件事 …

3D Gaussian Splatting代码详解(二):模型构建

3 模型构建 gaussians GaussianModel(dataset.sh_degree) 3.1 初始化函数 __init__ 构造函数 构造函数 __init__ 的主要作用是初始化 3D 高斯模型的各项参数和激活函数&#xff0c;用于生成 3D 空间中的高斯表示。 初始化球谐函数的参数&#xff1a; self.active_sh_degre…

HarmonyOS-权限管理

一. 权限分类 1. system_grant system_grant 为系统授权&#xff0c;无需询问用户&#xff0c;常用的权限包括网络请求、获取网络信息、获取wifi信息、获取传感器数据等。 /* system_grant&#xff08;系统授权&#xff09;*/static readonly INTERNET ohos.permission.INTE…

如何在 linux 中使用 /etc/fstab 挂载远程共享 ?

在 Linux 领域&#xff0c;高效的管理文件系统和数据存储对于用户和管理员来说&#xff0c;是一项基本技能。 有一种特别有用的技术涉及自动建立远程共享&#xff0c;允许无缝访问网络存储&#xff0c;就好像是本地的一样。 本指南将引导您完成使用 /etc/fstab 文件以自动远程…

Discussion can be found at https://github.com/pypa/pip/issues/10825

这个警告是说你正在使用的 PyPI 镜像&#xff08;在这个例子中是阿里云的镜像&#xff09;返回了一个不符合 HTML 5 标准的网页。这在未来的 pip 版本中可能会导致问题&#xff0c;因为 pip 计划在 22.2 版本中强制执行这一标准。 简单来说&#xff0c;虽然现在这个警告不会阻…

iOS用rime且导入自制输入方案

iPhone 16 的 cantonese 只能打传统汉字&#xff0c;没有繁简转换&#xff0c;m d sh d。考虑用「仓」输入法 [1] 使用 Rime 打字&#xff0c;且希望导入自制方案 [2]。 仓输入法有几种导入方案的方法&#xff0c;见 [3]&#xff0c;此处记录 wifi 上传法。准备工作&#xff1…

ts:常见的运算符

ts&#xff1a;常见的运算符 1 主要内容说明2 表格2.1 算数运算符2.2 赋值运算符2.3 比较运算符2.4 逻辑运算符2.5 位运算符2.6 三元运算符 3 例子3.1 位运算符3.1.1 源码1 &#xff08;位运算符&#xff09;3.1.2 源码1运行效果 3.结语4.定位日期 1 主要内容说明 ts中的各种运…

unity搭建场景学习

unity搭建场景学习 创建场景创建gameobject创建材质&#xff0c;用于给gameobject上色拖拽材质球上色上色原理设置多个材质方式设置贴图的方式 效果设置光滑度一些预览设置菜单渲染模型与碰撞模型网格渲染参数1. materials(材质)2. lighting(光照)3. reflection probes(反射探针…

『Linux学习笔记』如何在 Ubuntu 22.04 上安装和配置 VNC

『Linux学习笔记』如何在 Ubuntu 22.04 上安装和配置 VNC 文章目录 一. 『Linux学习笔记』如何在 Ubuntu 22.04 上安装和配置 VNC1. 介绍 二. 参考文献 一. 『Linux学习笔记』如何在 Ubuntu 22.04 上安装和配置 VNC 如何在 Ubuntu 22.04 上安装和配置 VNChttps://hub.docker.c…

xlwings,让excel飞起来!

excel已经成为必不可少的数据处理软件&#xff0c;几乎天天在用。python有很多支持操作excel的第三方库&#xff0c;xlwings是其中一个。 关于xlwings xlwings开源免费&#xff0c;能够非常方便的读写Excel文件中的数据&#xff0c;并且能够进行单元格格式的修改。 xlwings还…