SPARKSQL3.0-Antlr4由浅入深SparkSQL语法解析

news2025/2/26 19:34:16

一、前言

在开始剖析SparkSQL前,我们要先来了解一下Antlr4,这是因为spark-sql字符串解析工作是由Antlr4完成的,故需要先来了解Antlr4,如下:

image-20221111170815678

本文会着重介绍一下几点:

1、Antlr是什么?

2、如何使用?

3、SparkSql中如何使用?

二、Antlr4是什么?

Antlr4(Another Tool for Language Recognition)是一款强大的语法分析器生成工具,可用于读取、处理、执行和翻译结构化的文本,用户可根据需要自定义语法规则来实现相应功能

那么我们为何要用antlr4呢?

假设我们要自己发明一种特殊语言【例如以自己命名的sql语言: MeSQL】,下面是MeSQL的一条sql语句:

me a,b,c to tableName

相信市面上根本没有这种语言,我们自己编写的语言中肯定需要语法和关键字,并且关键字不仅仅只是 me \ to。肯定有很多关键字和不同语法组合成的语句;

针对这种自己发明的语法被称为:DSL领域特定语言

如果要我们要自己实现一套DSL领域特定语言,其过程会十分复杂,首先需要解析字符串,再形成语法树,再到节点处理等等步骤。

此时ANTLR就可以派上用场了,多说无益,接下来我们自己实现一个

三、使用

首先ANTLR是用Java编写的,因此你需要首先安装Java,下面将从实战的角度介绍如何使用

1、安装插件

首先需要在IDEA中安装antlr4插件,这个插件可以帮助我们提高工作效率,就像Maven的MavenHelper插件一样

image-20221112170551057

2、新建Maven工程

新建一个Maven项目,并在pom.xml中引入antlr4依赖:

注意:如果你已经有一个SparkSql的项目,则无需引用,因sparkSQL中已经包含antlr的依赖

 <dependencies> 
     <dependency>
            <groupId>org.antlr</groupId>
            <artifactId>antlr4-runtime</artifactId>
            <version>4.8-1</version>
        </dependency>
 </dependencies>  
  
 <plugins> 
         <plugin>
                <groupId>org.antlr</groupId>
                <artifactId>antlr4-maven-plugin</artifactId>
                <version>4.8-1</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>antlr4</goal>
                        </goals>
                        <phase>none</phase>
                    </execution>
                </executions>
                <configuration>
                    <outputDirectory>src/main/java</outputDirectory>
                    <!--<listener>true</listener>-->
                    <visitor>true</visitor>
                    <!--<treatWarningsAsErrors>true</treatWarningsAsErrors>-->
                </configuration>
          </plugin>
</plugins>

3、新建antlr4语法文件

语法文件是以.g4结尾的文件,例如MeSql.g4,antlr4是通过读取.g4语法文件来生成语法解析的类

一个完整的.g4语法文件是要包含如下两种元素:

1、语法规则 - 即语法,例如java的方法要写返回值和入参,等固定的语法搭配

2、词法规则 - 即关键字规则,例如java的public, private等关键字词法

如下:

// 语法文件通常以 granmar 关键宇开头 这是一个名为 MeSql 的语法 它必须和文件名 MeSql.g4相匹配
grammar MeSql;

// 定义一条名为 me 的语法规则,它匹配一对花括号[START, STOP为词法关键词]、逗号分隔的 value [另一条语法规则,在下面], 以及 * 匹配多个 value
me : START value (',' value)* STOP ;

// 定义一条value的语法规则,正是上面me语法中的value,该value的值应该是 INT 或者继续是 me [代表嵌套], | 符号代表或
value : me
      |INT
      ;

// 以下所有词法符号都是根据正则表达式判断
// 定义一个INT的词法符号, 只能是正整数
INT : [0-9]+ ;

// 定义一个START的词法符号, 只包含{
START : '{' ;

// 定义一个STOP的词法符号, 只包含}
STOP : '}' ;

// 定义一个AND的词法符号, 只包含,
AND : ',' ;

// 定义一个WS的词法符号,后面跟正则表达式,意思是空白符号丢弃
WS  : [\t\n\r]+ -> skip ;

4、运行antlr4插件测试

接下来我们要通过运行语法文件,将用户输入的字符串转化为语法树,过程如下:

语法解析基本流程

首先选中me右键点击Test Rule me

image-20221112173913555

此时会弹出Antlr Preview窗口,在左侧窗口中输入:

{3,4,{3,4}}

此时右侧会展示插件解析好的语法树【也叫AST抽象语法树】

image-20221112174113241

可以看到树中的根节点就是me,{ 转换成了START,数值3,4都转换成了value,并且指向INT词法

我们再将MeSql文法改一改:将INT改为[0-2],此时输入3,4 会解析失败

image-20221112174431233

5、文件分层

在实际使用中,我们的语法文件中会包含非常多的语法和词法,为了更好的解耦,通常是有两个文件:

1、语法文件

2、词法文件

语法文件中通过关键字指向词法文件,例如在spark中,也是将语法文件分成了两个,如下:

https://github.com/apache/spark/tree/master/sql/catalyst/src/main/antlr4/org/apache/spark/sql/catalyst/parser

image-20221114160651843

所以我们将上面示例更改成如下两个文件:

image-20221114154623338

MeSqlLexer词法文件:

// 语法文件通常以 granmar 关键宇开头 这是一个名为 MeSql 的语法 它必须和文件名 MeSql.g4相匹配
lexer grammar MeSqlLexer;

// 以下所有词法符号都是根据正则表达式判断
// 定义一个INT的词法符号, 只能是正整数
INT : [0-9]+ ;

// 定义一个START的词法符号, 只包含{
START : '{' ;

// 定义一个STOP的词法符号, 只包含}
STOP : '}' ;

// 定义一个AND的词法符号, 只包含,
AND : ',' ;

// 定义一个WS的词法符号,后面跟正则表达式,意思是空白符号丢弃
WS  : [\t\n\r]+ -> skip ;

MeSqlParser语法文件:

// 语法文件通常以 granmar 关键宇开头 这是一个名为 MeSql 的语法 它必须和文件名 MeSql.g4相匹配
parser grammar MeSqlParser;

options {
    // 表示解析token的词法解析器使用SearchLexer
    tokenVocab = MeSqlLexer;
}

// 定义一条名为 me 的语法规则,它匹配一对花括号[START, STOP为词法关键词]、逗号分隔的 value [另一条语法规则,在下面], 以及 * 匹配多个 value
me : START value (',' value)* STOP ;

// 定义一条value的语法规则,正是上面me语法中的value,该value的值应该是 INT 或者继续是 me [代表嵌套], | 符号代表或
value : me
      |INT
      ;

在MeSqlParser语法文件中选中me 右键执行:

image-20221114154834435

检验成功:

image-20221114154935761

6、编译语法文件生成java语法解析类

验证完语法文件是正确的,接下来就要用antlr4的工具将语法文件编译成java解析类,最终落地到代码层面

首先配置生成java类的路径,右键MeSqlParser -> Configure

image-20221114161236922

image-20221114161837329

生成java文件:

image-20221114161545257

image-20221114161927632

7、使用解析类

虽然我们在IDEA的antlr4插件中可以看到语句转换成AST语法树:

image-20221115145346773

但这是antlr插件帮我们生成的,在实际使用中我们需要将语法树转换成真正的类,类似下图:me类中包含各个子类,同时包含自己

image-20221115145406201

生成的java类便是Antlr4所提供的核心功能,将AST语法树转化成类的表达方式,接下来我们试一下

新建一个Test类复制如下代码:

import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.tree.ParseTree;


public class Test {
    public static void main(String[] args) {
        ANTLRInputStream input = new ANTLRInputStream("{1,2,{3,4}");
        //词法解析器,处理input
        MeSqlLexer lexer = new MeSqlLexer(input);
        //词法符号的缓冲器,存储词法分析器生成的词法符号
        CommonTokenStream tokens = new CommonTokenStream(lexer);
        //语法分析器,处理词法符号缓冲区的内容
        MeSqlParser parser = new MeSqlParser(tokens);

        ParseTree tree = parser.me();
        System.out.println(tree.toStringTree(parser));
    }
}

我们在输出一行打一个断点,debug模式运行下,如下:

可以看出在ParseTree中包含着children集合,在集合中抱着各个节点,每个节点又可以向下展开,从而形成类形式的语法树!

image-20221115145931010

8、自定义处理规则

在上一步中Antlr4帮我们将{1,2,{3,4}}字符串转化成了语法树,接下来我们需要自定义处理逻辑,从而让语法书按照我们设定的规则进行处理

比如我们现在的规则是需要将{}中的所有数值相加求和,最后得到总和,那么该如何自定义呢?

Antlr4给我们提供了两种遍历树的方式:

1、监听器–antlr4内部控制遍历语法树规则

2、访问者—用户可以手动控制遍历语法树规则

这两种方式在此示例中的体现是两个接口【antlr4帮我们生成的】:

image-20221115155635170

我们只需要在两种接口中选择实现一种接口即可,不过antlr4已经帮我们生成了两个实现类:所以我们只需要直接补充接口函数即可

image-20221115155715634

8.1、监听器模式

监听器模式的特点是用户无需关心语法树的递归,统一由antlr提供的ParseTreeWalker类进行递归即可。

我们先自行实现ParseTreeListener接口,在其中填充自己的逻辑代码(通常是调用程序的其他部分),从而构建出我们自己的语言类应用程序。

MeSqlParserBaseListener:通过map将各个节点分开,最后进行汇总累加

import java.util.HashMap;
import java.util.Map;

import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.tree.ErrorNode;
import org.antlr.v4.runtime.tree.TerminalNode;

public class MeSqlParserBaseListener implements MeSqlParserListener {

	Map<String, Integer> map = new HashMap<>();

	 public void enterMe(MeSqlParser.MeContext ctx) {
	 	if (!map.containsKey(ctx.getText())) {
			map.put(ctx.getText(), 0);
		}
	 }

	 public void exitMe(MeSqlParser.MeContext ctx) {
	 	if (ctx.parent == null) {
	 		int sum = map.values().stream().mapToInt(i -> i).sum();
			System.out.println(" result = " + sum);
		}
	 }

	 public void enterValue(MeSqlParser.ValueContext ctx) {
		 if (ctx.INT() != null && map.containsKey(ctx.parent.getText())) {
			 map.put(ctx.parent.getText(), map.get(ctx.parent.getText()) + Integer.parseInt(ctx.INT().getText()));
		 }
	 }


	 public void exitValue(MeSqlParser.ValueContext ctx) {
	 }

	 public void enterEveryRule(ParserRuleContext ctx) {
	 }

	 public void exitEveryRule(ParserRuleContext ctx) {
	 }

	 public void visitTerminal(TerminalNode node) {
	 }

	 public void visitErrorNode(ErrorNode node) {
	 }
}

主程序:

import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.tree.ParseTree;
import org.antlr.v4.runtime.tree.ParseTreeWalker;


public class Test {
    public static void main(String[] args) {
        ANTLRInputStream input = new ANTLRInputStream("{1,2,{3,4}}");
        //词法解析器,处理input
        MeSqlLexer lexer = new MeSqlLexer(input);
        //词法符号的缓冲器,存储词法分析器生成的词法符号
        CommonTokenStream tokens = new CommonTokenStream(lexer);
        //语法分析器,处理词法符号缓冲区的内容
        MeSqlParser parser = new MeSqlParser(tokens);

        ParseTree tree = parser.me();
				// ParseTreeWalker类将实现的MeSqlParserBaseListener监听器放入
        new ParseTreeWalker().walk(new MeSqlParserBaseListener(), tree);
    }
}

这里说一下执行流程:

在MeSqlParserBaseListener类中,语法中的每条规则都有对应的enter方法和exit方法。

例如,当遍历器访问到me规则对应的节点时,它就会调用enterMe()方法,然后将对应的AST语法树节点 MeContext的实例当作参数传递给它。在遍历器访问了me节点的全部子节点之后,它会调用exitMe();

如果执行到叶子节点,它会调用enterValue()方法,将对应的语法树节点 ValueContext的实例当作参数传递给它,执行完成后执行exitValue()方法。

下图用标识了 ParseTreeWalker对AST语法树进行深度优先遍历的过程:

image-20221115194408500

上面的程序结果是通过最后一次exitMe函数来将map中存储的各个节点的总和累加得出,如下:

image-20221115194752047

至此监听器程序结束。

8.2、访问者模式

访问者模式的特点是需要用户自己手动控制语法树节点的调用,优点是灵活,sparksql也是使用这一模式来实现sql语法解析

在MeSqlParserBaseVisitor中,语法里的每条规则对应接口中的一个visit方法

MeSqlParserBaseVisitor2:

import java.util.List;

// 用户自己控制语法树节点遍历,十分灵活
public class MeSqlParserBaseVisitor2 extends MeSqlParserBaseVisitor<Integer> {
	
  // 循环me节点的所有子节点,调用visitValue函数
	@Override
	public Integer visitMe(MeSqlParser.MeContext ctx) {
		final List<MeSqlParser.ValueContext> value = ctx.value();
		return value.stream().mapToInt(this::visitValue).sum();
	}
	
  // visitValue函数中判断如果是me节点则调用visitMe,否则返回INT值
	@Override
	public Integer visitValue(MeSqlParser.ValueContext ctx) {
		if (ctx.me() != null) {
			return visitMe(ctx.me());
		}
		if (ctx.INT() != null) {
			return Integer.parseInt(ctx.INT().getText());
		}
		return 0;
	}
}

主函数:

import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.CommonTokenStream;


public class TestVisitor {
    public static void main(String[] args) {
        ANTLRInputStream input = new ANTLRInputStream("{1,2,{3,4}}");
        //词法解析器,处理input
        MeSqlLexer lexer = new MeSqlLexer(input);
        //词法符号的缓冲器,存储词法分析器生成的词法符号
        CommonTokenStream tokens = new CommonTokenStream(lexer);
        //语法分析器,处理词法符号缓冲区的内容
        MeSqlParser parser = new MeSqlParser(tokens);

        // 创建自定义访问器
        MeSqlParserBaseVisitor2 visitor = new MeSqlParserBaseVisitor2();
        // 将parser语法树头节点放入
        Integer sum = visitor.visitMe(parser.me());
        System.out.println(sum);
    }
}

结果:通过debug,可以看到结果符合预期:

image-20221115202825200

至此访问者模式结束。

9、使用总结

至此我们用两种方式实现了一个简单的DSL语言,回过头来再看一下开篇定义:

ANTLR是一款强大的语法分析器生成工具,可用于读取、处理、执行和翻译结构化的文本,用户可根据需要自定义语法规则来实现相应功能

是不是感觉清晰了很多

四、SparkSql中如何使用

在sparksql源码中是有语法文件的,如下:

https://github.com/apache/spark/tree/master/sql/catalyst/src/main/antlr4/org/apache/spark/sql/catalyst/parser

image-20221114160651843

接下来我们将这两个文件复制到IDEA中,打开SqlBaseParser.g4,右键执行Test Rule

image-20221115205101068

然后我们随便输入一条sql,查看右侧语法树:可以看到右侧生成了庞大的语法树,这就是SparkSQL的语法树

image-20221115205222479

接下来我们可以根据语法文件来生成相关配置类:

image-20221115205358276

然后我们试着做一个好玩的,新建一个类来自定义访问器:

MyVisitor

/**
 * 自定义SparkSQL
 */
public class MyVisitor extends SqlBaseParserBaseVisitor<String> {
	@Override
	public String visitSingleStatement(SqlBaseParser.SingleStatementContext ctx) {
		System.out.println(" ...MyVisitor... "); // 打印
		return visitChildren(ctx);
	}
}

主函数:

import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.CommonTokenStream;


public class TestSpark {
    public static void main(String[] args) {
        String query = "SELECT * FROM STUDENT WHERE ID > 10;";
        SqlBaseLexer lexer = new SqlBaseLexer(new ANTLRInputStream(query.toUpperCase()));
        SqlBaseParser parser = new SqlBaseParser(new CommonTokenStream(lexer));

        // 创建自定义访问器
        MyVisitor visitor = new MyVisitor();
        // 将parser语法树头节点放入
        visitor.visitSingleStatement(parser.singleStatement());
    }
}

此时运行会打印…MyVisitor…,由此我们自定义实现了一个sparksql处理的demo

那么spark内部肯定有自己的访问者,位置在spark-catalyst包中,如下:

image-20221115204013045

由于sparksql是通过访问器模式实现递归调用语法树,故这里看SqlBaseBaseVisitor

发现真正实现的是子类:AstBuilder、SparkSqlAstBuilder,其内部实现函数便是sparksql各个节点的执行逻辑

image-20221115204238213

至此SparkSql中涉及antlr4的知识点就结束了,后面就是unresovle阶段,将在下一节讲解

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

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

相关文章

C. Mortal Kombat Tower(DP)

Problem - 1418C - Codeforces 题意: 你和你的朋友正在玩《真人快打11》这个游戏。你们正试图通过一个挑战塔。这个塔里有n个老板&#xff0c;编号从1到n&#xff0c;第i个老板的类型是ai。如果第i个boss是简单的&#xff0c;那么它的类型是ai0&#xff0c;否则这个boss是困难…

HarmonyOS鸿蒙学习笔记(15)Swiper实现抖音切换视频播放效果

Swiper实战1、项目结构2、PageVideo和PlayView简单说明2.1 State变量的使用2.2 Link和Watch变量的使用2.3、Swiper的使用和PlayView的初始化2.4、页面可见状态发生改变时对视频进行暂停和播放2.5 PlayView和PageVidew源码&#xff1a;参考资料&#xff1a;1、项目结构 前面写了…

22服务-ReadDataByIdentifier

诊断协议那些事儿 诊断协议那些事儿专栏系列文章&#xff0c;本文介绍数据传输服务下的22服务ReadDataByIdentifier&#xff0c;允许客户端通过一个或多个dataldentifier向标识的服务器请求dataRecord&#xff08;数据记录值&#xff09;。 文章目录诊断协议那些事儿一、22服…

技术宅星云的Mac系统使用经验分享

技术宅星云的Mac系统使用经验分享系统维护1.1 Mac OSX 卡顿严重1.2 开启/禁止.DS_store文件生成1.3 显示/隐藏 系统文件夹系统维护 这篇博文分享使用Mac 系统中的一些优化经验。 1.1 Mac OSX 卡顿严重 今天不知道怎么回事&#xff0c;系统突然卡得不要不要的&#xff0c;各种…

Airtest新手升级:一个相对完整的纯.py脚本是怎样子的

1. 前言 一直以来&#xff0c;Airtest的教程都倾向于编写 .air 脚本&#xff0c;但本质上&#xff0c;它还是python脚本来的。今天我们就来补上这个缺口&#xff0c;一起来看下一个相对完整的纯 .py 脚本是什么样子的。 2. 例子一&#xff1a;纯py的Airtest脚本 有时候&…

Python-新建-Django项目-调试-显示mysql数据库表内容-HelloWorld

文章目录1.Pycharm-开发编辑器2.HelloWorld程序范例3.代码调试4.连接数据库-mysql4.1.安装好mysql数据库4.2.创建项目4.3.数据库表转模型4.4.前端展示5.总结1.Pycharm-开发编辑器 文件->新建项目->选择Django。接着在控制台输入命令&#xff1a; python -m django --ver…

【C语言】分支语句 循环语句 _训练题型加深理解

1.分支语句 自从学习编程以来每天都在写分支语句&#xff0c;那么什么是分支语句呢&#xff1f; 下面举两个生动的例子来更好的理解分支语句&#xff1a; 比如我们买东西&#xff0c;要么支付现金&#xff0c;要么使用微信或者支付宝。在大学如果你好好学习&#xff0c;校招…

SpringBoot简单使用MongoDB

SpringBoot简单使用MongoDB一、配置步骤1、application.yml2、pom3、entity4、mapper二、案例代码使用1、库前期准备上一篇安装MongoDB地址http://t.csdn.cn/G4oYJ 一、配置步骤 进入mongodb中创建数据库和用户 # &#xff08;1&#xff09;授权 # 我的管理员是root&#xf…

umi项目本地开发环境远程打开的问题

qiankun主应用加载子应用时&#xff0c;url指定了localhost const getEntry (base: string, port: number) > {const host: string location.hostnamereturn process.env.NODE_ENV development? http://${host}:${port}${base}: ${base}/index.html }而getEntry是用于q…

Cloud Keys Delphi Edition安全地存储

Cloud Keys Delphi Edition安全地存储 使用流行的基于云的密钥管理服务安全地管理密钥和机密。 云密钥可以轻松地将基于云的密钥和秘密管理与任何支持的平台或开发技术集成。这些易于使用的组件可用于与流行的云密钥管理提供商(如Amazon KMS、Amazon AWS Secrets、Azure key Va…

初识 Node.js 与内置模块:初识 Node.js及Node.js 环境的安装

回顾与思考 1. 已经掌握了哪些技术 2. 浏览器中的 JavaScript 的组成部分 3. 思考&#xff1a;为什么 JavaScript 可以在浏览器中被执行 4. 思考&#xff1a;为什么 JavaScript 可以操作 DOM 和 BOM 每个浏览器都内置了 DOM、BOM 这样的 API 函数&#xff0c;因此&#xff0c;…

【云原生之k8s】k8s资源限制以及探针检查

文章目录一、资源限制1、资源限制的使用2、reuqest资源&#xff08;请求&#xff09;和limit资源&#xff08;约束&#xff09;3、Pod和容器的资源请求和限制4、官方文档示例5、资源限制实操5.1 编写yaml资源配置清单5.2 释放内存&#xff08;node节点&#xff0c;以node01为例…

Moonbeam Illuminate/22线上生态盛会|Derek开场演讲

TL;DR Derek&#xff1a;Moonbeam是我认为最佳的实现Web3梦想的平台。一年中近300个项目已经部署在了Moonbeam生态&#xff0c;发展显著优于行业平均。Moonbeam正在构建被成为“Connected Contracts”的原生跨链方案。Moonbeam基金会新设立Moonbeam加速器&#xff0c;帮助Moon…

时间序列预测之为何舍弃LSTM而选择Informer?(Informer模型解读)

LSTM的劣势 Figure 1: (a) LSTF can cover an extended period than the short sequence predictions, making vital distinction in policy-planning and investment-protecting. (b) The prediction capacity of existing methods limits LSTF’s performance. E.g., startin…

Nginx快速入门及配置文件结构

Nginx快速入门教程Nginx 简介Nginx 特性Nginx 架构Nginx 相比Apache的优点Nginx 的安装启动、停止和重新加载 Nginx 配置Nginx 配置文件结构Nginx 工作流程总结后言Nginx 简介 Nginx是 HTTP 和反向代理服务器&#xff0c;邮件代理服务器&#xff0c;以及 Igor Sysoev 最初编写…

传统防火墙与Web应用程序防火墙(WAF)的区别

前言 由于WEB应用防火墙&#xff08;WAF&#xff09;的名字中有“防火墙”三个字&#xff0c;因此很多人都会将它与传统防火墙混淆。实际上&#xff0c;二者之间的有着很大的差别。传统防火墙专注在网络层面&#xff0c;提供IP、端口防护。而WAF是专门为保护基于Web的应用程序…

学生用白炽灯好还是led灯好?2022最专业学生护眼灯推荐

现阶段的学生视力都普遍出现近视低龄化&#xff0c;所以在护眼方面&#xff0c;家长都非常重视的&#xff0c;有人问&#xff1a;学生用白炽灯好还是led灯好&#xff1f; 我的回答是LED灯更适合现在家庭使用&#xff0c;给大家分析一下。 白炽灯是由灯丝发热产生光亮&#xff…

多层串联拼接网络

🍿*★,*:.☆欢迎您/$:*.★* 🍿 目录 背景 正文 总结 背景描述

Pytorch ——特征图的可视化

文章目录前言一、torchvision.models._utils.IntermediateLayerGetter*注意&#xff1a;torcvision的最新版本0.13&#xff0c;已经取消了pretrainedTrue这个参数&#xff0c;并且打算在0.15版正式移除&#xff0c;如果用pretrained这个参数会出现warring警告。现在加载与训练权…

【项目实战】springboot+vue舞蹈课程在线学习系统-java舞蹈课程学习打卡系统的设计与实现

注意&#xff1a;该项目只展示部分功能&#xff0c;如需了解&#xff0c;评论区咨询即可。 本文目录1.开发环境2 系统设计2.1 背景意义2.2 技术路线2.3 主要研究内容3 系统页面展示3.1 学生3.2 教师页面3.3 管理员页面4 更多推荐5 部分功能代码5.1 查看学生打卡5.2 文件上传下载…