ProGuard 进阶系列(二)配置解析

news2024/11/25 4:48:35

书接上文,从开源库中把代码下载到本地后,就可以在 IDE 中进行运行了。从 main 方法入手,可以看到 ProGuard 执行的第一步就是去解析参数。本文的内容主要分析源码中我们配置的规则解析的实现。

在上一篇文章末尾,在 IDE 中,添加了 @/Users/xxx/debug_proguard.pro 作为函数运行的入参,将配置文件的路径传递给 ProGuard 使用。先来看一下 Main 函数中的代码:

13b5f2e1a15b545c00663a5769dd61c2.png
ProGuard 的 Main 函数代码

从这几行代码可以看出,ProGuard 的大体运行逻辑。在代码 518 行中,通过入参 args系统属性配置 创建了一个配置解析器 ConfigurationParser ,随后调用其 parse 方法,解析传入的参数,并将结果放到 configuration 中,以供后续混淆逻辑使用。

try-with-resources 语法

在代码 518 行处,创建 ConfigurationParser 时,使用了 Java 1.7 中提供的 try-with-resources 语法。此语法可以帮助我们关闭流。举个例子,我们现在需要从一个文件中读取第一行内容。在 Java 1.7 之前,代码将会如下:

static String readFirstLineFromFile(String path) throws IOException {
    FileReader fr = new FileReader(path);
    BufferedReader br = new BufferedReader(fr);
    try {
        return br.readLine();
    } finally {
        br.close();
        fr.close();
    }
}

从代码中可以看到,在 finally 代码块中,需要手动对 FileReaderBufferedReader 进行关闭。而使用 try-with-resources 语法后,就无需手动调用 close 方法。示例代码如下:

static String readFirstLineFromFile(String path) throws IOException {
    try (FileReader fr = new FileReader(path); BufferedReader br = new BufferedReader(fr)) {
        return br.readLine();
    }
}

这样,代码能精简很多,close 也不会因为开发者的疏忽而被遗漏。

配置读取

为了更好地理解整个读取与解析的内容,我画了一个简单的流程图。在 ProGuard 中读取配置文件的逻辑中,会按照一个个单词 为单位进行读取,根据代码中的流程,绘制如下流程图,能够更好地理解代码内容。

c78ea029a100fcceedb8ee191fada3c0.png
解析流程图

根据上面的流程图,在来看源码实现。首先是 ConfigurationParser 的构造方法,实现代码如下:

public ConfigurationParser(String[] args, Properties properties) throws IOException {
  this(args, null, properties);
}

public ConfigurationParser(String[] args, File baseDir, Properties properties) throws IOException {
 this(new ArgumentWordReader(args, baseDir), properties);
}

public ConfigurationParser(WordReader reader, Properties properties) throws IOException{
  this.reader     = reader;
  this.properties = properties;
 readNextWord();
}

在构造方法中,使用入参中的 args系统属性配置 创建了一个 ArgumentWordReader。顾名思义,它是用来读取运行代码时传入的程序参数的。

9f55d679b05bf9c83772c1b190508b3d.png
WordReader 类图

WordReader 的设计中,内容读取是按行读取的。在 LineWordReaderFileWordReader 中,直接使用 LineNumberReader 按行读取。而对于 ArgumentWordReader,实现逻辑会更简单一些,直接将前面提到的 args 数组中的每一个 String 作为一行字符串处理。

接下来,在看构造方法的最末尾:调用了 readNextWord() 方法,此为流程中开始读取下一个单词,也是为了获取第一个 「单词」。来看一下代码是如何实现的:

private void readNextWord() throws IOException {
  readNextWord(false, false);
}

private void readNextWord(boolean isFileName, boolean expectSingleFile) throws IOException {
  nextWord = reader.nextWord(isFileName, expectSingleFile);
}

代码的逻辑里最终调用了 reader.nextWord,此处的 reader 就是刚才提到的 ArgumentWordReader。运行时会使用它去读取第一个「单词」。讲到这里,不由得让我想起了大学时编译原理中讲的 词法分析器。有感兴趣的同学可以去巩固一下《编译原理》。因为 ProGuard 定义的规则相对简单,所以此处的逻辑比一门编程语言简单许多。在运行代码时,只传了一个参数:@/Users/xxx/debug_proguard.pro。在解析时,它会作为一行直接进行处理。先来看一下代码:

public String nextWord(boolean isFileName, boolean expectSingleFile) throws IOException {
  currentWord = null;
  // 省略部分代码
  while (currentLine == null || currentIndex == currentLineLength) {
    // 读取有效的参数行
    currentLine = nextLine();
  }

  // Find the word starting at the current index.
  int startIndex = currentIndex;
  int endIndex;

  // 取第一个字符
  char startChar = currentLine.charAt(startIndex);

 // 省略部分代码
  
  else if (isDelimiter(startChar)) {
  // 如果是分格符,如 @, {, }, (, )等符号
    endIndex = ++currentIndex;
  }
  else {
    // 其它情况处理逻辑
  }

  // 截取,此处的 currentWord 就是解析出来的 @ 符号
  currentWord = currentLine.substring(startIndex, endIndex);
 
  return currentWord;
}


// 是否为分隔符,如果是,则返回 true
private boolean isDelimiter(char character) {
  return isStartDelimiter(character) || isNonStartDelimiter(character);
}
private boolean isStartDelimiter(char character) {
  return character == '@';
}
private boolean isNonStartDelimiter(char character) {
  return character == '{' ||
          character == '}' ||
          character == '(' ||
          character == ')' ||
          character == ',' ||
          character == ';' ||
          character == File.pathSeparatorChar;
}

在读取的过程中,首先将整行数据存储在 currentLine 中,当前此处为 @/Users/xxx/debug_proguard.pro,紧接着会从 currentLine 中取 第一个 字符,因为 @  是分隔符,因此会将它作为第一个 「单词」。代码执行到这里,构造方法里面涉及的逻辑也执行结束,ConfigurationParser 创建完成。下一步就是调用 parse 方法,去执行解析操作,代码如下:

public void parse(Configuration configuration) throws ParseException, IOException {
  while (nextWord != null) {
    // 是 @ 或者是 -include 执行
   if (ConfigurationConstants.AT_DIRECTIVE.startsWith(nextWord) || ConfigurationConstants.INCLUDE_DIRECTIVE.startsWith(nextWord))
     configuration.lastModified = parseIncludeArgument(configuration.lastModified);
    
    // 省略其它代码
  }
}

parse 方法中,会循环遍历所有的 「单词」,直到所有单词都处理完毕。现在只需要看 @ 的处理逻辑,在代码中,如果当前 「单词」为 @-include 时,会调用 parseIncludeArgument 去实现解析的逻辑。  @ 符号的定义是 以递归的方式从给定的文件中读取配置选项 , 从它的定义就可以看出来, parseIncludeArgument 会去解析 @ 后指的文件名称,并读取文件内容。

private long parseIncludeArgument(long lastModified) throws ParseException, IOException{
  // 读取 @ 后面跟着的文件名
  readNextWord("configuration file name", true, true, false);
  URL url = null;
  try {
    // Check if the file name is a valid URL.
    url = new URL(nextWord);
  } catch (MalformedURLException ex) {
  }
  if (url != null) {
    // 给当前 reader 设置一个 includeWordReader
    reader.includeWordReader(new FileWordReader(url));
  }
 // 省略部分代码
  readNextWord();
  return lastModified;
}

代码中可以看到,在执行时,首先会调用 readNextWord 去获取文件名。与前面 @ 获取类似,从 currentLine 中读取出剩下的部分,作为文件名称。获取到文件名称后,就会直接使用这个名称去创建一个 FileWordReader,用于读取此文件中的内容。当然,这里创建的 FileWordReader 还需要赋值给 ArgumentWordReader 的成员变量 includeWordReader。调用 ArgumentWordReadernextWord 方法时,会先调用 includeWordReader.nextWord(xx, xx) 方法,以此来实现递归读取配置文件,实现 @ 符号所定义的功能,如前面的流程图所示。调用 includeWordReader 去获取下一个「单词」的逻辑如下:

if (includeWordReader != null) {
  // 读取下一个字符
  currentWord = includeWordReader.nextWord(isFileName, expectSingleFile);
  if (currentWord != null) {
   return currentWord;
  }
  // 读取完成后,将 reader 关掉,并且置空
  includeWordReader.close();
  includeWordReader = null;
}

此处的 FileWordReaderArgumentWordReader 的核心逻辑基本相似。在 FileWordReader 中,nextLine 方法从指定的文件中读取真实的数据行,而文件行读取使用的是 JDK 中的 LineNumberReader。逻辑不复杂,有兴趣的朋友可自行查阅原文。

配置解析

从文件中读取到配置信息后,需要解析当前的「单词」,并按照固定的逻辑进行处理。在前面的内容中,已经涉及到了配置解析。在 ProGuard 中,有许多配置和不同的规则,可以通过查看源代码来了解。在 ProGuard 中,配置规则分为多个类别。下面将从 输入/输出选项-keep 选项 这两个部分进行分析,以点带面,了解 ProGuard 中配置解析的逻辑。

输入/输出选项

先来看 -injars-outjars-libraryjars 的实现,当读取到这几个单词时,解析的执行如下:

2f69b4961f1910ac195bef3e42046dcd.png
jars 相关解析

可以看到,这三个参数的解析,都是调用的 parseClassPathArgument  来实现的,且 -injars-outjars 都是放在 configuration.programJars 中的。 以 Android 的项目为例,编译结束时,会生成 R.jar 文件,以及一个 classes 文件夹,因此 -injars 的配置如下:

-injars /project_dir/build/intermediates/compile_r_class_jar/release/R.jar
-injars /project_dir/build/intermediates/javac/release/classes

因此,在解析方法中,按文件路径读取下一个「单词」,然后添加到对应的 classpath 中即可。在源代码中,还会存在文件分割符等逻辑,直接上代码:

private ClassPath parseClassPathArgument(ClassPath classPath, boolean isOutput, boolean allowFeatureName) {
    // 读取第一个文件路径
    readNextWord("jar or directory name", true, false, false);
    while (true) {
        // 创建一个 ClassPathEntry
        ClassPathEntry entry = new ClassPathEntry(file(nextWord), isOutput, featureName);
        // 读取下一个单词,可能是文件分隔符,在 mac os 中为 :
        readNextWord();

        // …… 省略读取 filter 的代码  ……

        // 将 ClassPathEntry 添加到 classpath 中
        classPath.add(entry);
        // 是否已经读取完成了? 如果只有一个文件名,如示例中的,就直接结束了。
        if (configurationEnd()) {
            return classPath;
        }
        // 如果不为 路径分隔符 ,直接抛异常
        if (!nextWord.equals(System.getProperty("path.separator"))) {
            throw new ParseException("Expecting class path separator '" + ConfigurationConstants.JAR_SEPARATOR_KEYWORD + "' before " + reader.locationDescription());
        }
        // 读取下一个文件路径
        readNextWord("jar or directory name", true, false, false);
    }
}

-keep 选项

在写 ProGuard 规则中, keep 的规则是相对比较复杂的,根据个人的理解,将 keep 的解析规则用 EBNF 进行描述,如下所示,能够更好的理解其逻辑。

ffcafbcc6e7f93fcf004370379c8bbf4.png
ProGuard EBNF 描述

解析思路与 输入/输出选项 类似,先根据当前的单词判断是否为 keep_keywords ,代码如下:

ce502b021b2d361a221d1dc7e1738180.png
keep 解析分支代码

从代码中可以看到,所有 keep_keywords 的解析都调用到了 parseKeepClassSpecificationArguments 中,些方法的解析逻辑,与 EBNF 中描述的基本一致,先看代码执行的流程图:

8f40b4202f221fc1d6091f7aa9d3f2c0.png
解析 Keep 后的描述符

代码中实现逻辑与上述流程图一致, 源码如下:

while (true) {
    // 1. 读取 -keep 后的单词,
    // 例如配置规则为: -keep class com.example.MainClass 
    // 则此时读取的单词为 class
    readNextWord("keyword ...", false, false, true);

    // 2. 判断读了的单词是否为 「,」 号,如果是,后面会跟其它命令,
    // 例如配置规则为:
    // -keep, allowobfuscation class Test
    // 此时 nextWord 的值就为 「,」
    if (!ConfigurationConstants.ARGUMENT_SEPARATOR_KEYWORD.equals(nextWord)) {
       // 如果不为 「,」 则直接退出循环
        break;
    }

    // 3. 读取后面的 allowshrinking / allowoptimization / allowobfuscation 等
    readNextWord("keyword '" + ConfigurationConstants.ALLOW_SHRINKING_SUBOPTION + "'");

    // 4. 标记参数
    if (ConfigurationConstants.ALLOW_SHRINKING_SUBOPTION.startsWith(nextWord)) {
        allowShrinking = true;
    } else if (ConfigurationConstants.ALLOW_OPTIMIZATION_SUBOPTION.startsWith(nextWord)) {
        allowOptimization = true;
    } else if (ConfigurationConstants.ALLOW_OBFUSCATION_SUBOPTION.startsWith(nextWord)) {
        allowObfuscation = true;
    } else {
        throw new ParseException("Expecting keyword ...");
    }
}

// 5. 解析配置规则后的 class_specification
ClassSpecification classSpecification = parseClassSpecificationArguments(false, true, false);

有前面的 EBNF 描述以及流程图,代码逻辑看起来就会非常的简单。紧接着是解析 class_specification ,先来看一下它的 EBNF 描述,如下图所示:

56d4b3747e3aeea56b81e28db4094175.png
class EBNF 描述

PS: 在 ProGuard 的使用文档中,也有描述 class_specification 的信息,但是并非是 EBNF 格式,有兴趣的同学可以看看: https://www.guardsquare.com/manual/configuration/usage#classspecification

根据 EBNF 的描述,就可以按照其描述规则进行解析。但上面的描述中,还有 annotation_nameclass_namemethod_namereturn_typeargument_typefield_type 等标识符的描述并没有写出来。这里,写需要对他们进行简单的梳理。因为这个名称都是用来描述 Java 中相应的 类名方法名变量名 的信息,所以:

  1. 这些名称一定是符合 java 标识符的规则,即它们由数字(0~9)、字母(a~z 和 A~Z)以及 $ _ 组成,且第一个符号只能是字母、 $_ 中的一个。

  2. annotation_nameclass_namefield_type 等实际描述的是 Java 类名时,使用的是全路径信息,其中包含包名路径,因此名称会出现 . 这个符号 , 例如: com.example.Testjava.lang.Object

  3. 在描述方法返回值(retrun_type)、方法参数(argument_type)或变量类型(field_type) 时,可能会有数组存在,所以 [] 也可能会出现,例如: public java.lang.Object[] getList();

  4. 在 ProGuard 规则中,名称还能使用通配符,其中包括 *.<n>%

基于这些规则,先来看一下代码实现:

private void checkJavaIdentifier(String expectedDescription, boolean allowGenerics) throws ParseException {
    if (!isJavaIdentifier(nextWord)) {
        throw new ParseException("Expecting ...");
    }

    if (!allowGenerics && containsGenerics(nextWord)) {
        throw new ParseException("Generics are not allowed (erased) in ..."));
    }
}

public boolean isJavaIdentifier(String word) {
    if (word.length() == 0) {
        return false;
    }
    for (int index = 0; index < word.length(); index++) {
        char c = word.charAt(index);
        if (!(Character.isJavaIdentifierPart(c) || c == '.' || 
        c == '[' || c == ']' || c == '<' || c == '>' || c == '-' || 
        c == '!' || c == '*' || c == '?' || c == '%')) {
            return false;
        }
    }

    return true;
}

看完标识符的匹配规则,在来看完整定义的 annotation_nameclass_name 等名称的读取逻辑,在代码中,都会调用到 parseCommaSeparatedList 里面去,顾名思义,此方法会根据 , 解析一个列表出来,直接看代码:

6ea493c497d7e00ba9cf8856b6ef352c.png
annotation type 读取

代码中仅保留了关键代码,从注释中可以看到,拿到「单词」后,会先检查是否为一个合法标识符,如果符合,就添加到列表中去,并读取下一个「单词」,如果是 , 会继续上述逻辑进行添加,反之返回列表。

在回到上层的解析逻辑中来,根据 EBNF 的描述, 需要先判断是否有 annotation_type  和  access_flag, 这一块的逻辑如下:

09f7b97dea6b90d6e78e6ec20310539c.png
annotation type 解析

其中当解析到当前单词为 @ 时,会去解析 annotation_name 的列表, 并且重新用 , 拼接成一个字符串存储起来。 剩下的 access_flag 就简单多了,直接使用一个 int 型的值,按位将存起来就可以了,当然,我们还需要注意 ! ,因此,在存储的时候,会有两个变量:

if (!negated) {
  requiredSetClassAccessFlags |= accessFlag;
} else {
  requiredUnsetClassAccessFlags |= accessFlag;
}

后面的 class_nameextends  等逻辑读取就比较简单了,如下:

7865e1d657a8d43af2220f31c093ab5c.png
解析 ClassName 以及 extends 父类

当父类相关信息解析完成后,下一步便是解析方法和变量相关的信息,也就是 EBNF 中描述 field_specificationmethod_specification 的内容。在 ProGuard 中,这两部份类容统一放到了 parseMemberSpecificationArguments 中去实现了,先来看一下代码逻辑:

a2ddca6ec563fbe1a7e9ac7ddef863e8.png
类成员解析入口

剩下解析类成员的逻辑与前面类解析的逻辑相似,按照 EBNF 格式进行解析即可, 感兴趣的同学可以自行阅读源码。

结语

ProGuard 配置文件解析是非常重要的一部分内容,在 ProGuard 后续的执行逻辑中,会经常使用到本文中解析出来配置信息,可从官方文档详细了解一下各配置选项的作用级使用方法,以便能更好的理解后面的内容。在解析配置文件中,提到的 EBNF 是描述计算机编程语言的上下文无关文法的符号表示法,在编程语言开发中可能会经常遇到,此语法不复杂,可以去百科上读一读,相信你会有很多的收获。

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

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

相关文章

Vue Router4

后端路由 客户端请求不同的URL服务器匹配URL并给一个Controller处理Controller处理完返回渲染好的HTML页面或数据给前端 优点&#xff1a; 不需要单独加载js和css&#xff0c;直角交给浏览器展示&#xff0c;有利于SEO优化 缺点&#xff1a; 页面有后端人员编写或由前端人员…

告别里程焦虑:深蓝S7超级增程打造超长续航

提起新能源汽车&#xff0c;估计许多人第一时间都会想要查看它的续航里程。 虽然如今的新能源汽车在续航里程上较过去已经有了很大改进&#xff0c;但是稀缺的充电桩和漫长的充电时间&#xff0c;仍然无法让需要长途出行的用户摆脱里程焦虑。 那么问题就来了&#xff1a;有没有…

基于协同过滤算法的外贸出口电子电器产品的推荐系统的设计与实现源码+文档

博主介绍&#xff1a;✌在职Java研发工程师、专注于程序设计、源码分享、技术交流、专注于Java技术领域和毕业设计✌ 项目名称 基于协同过滤算法的外贸出口电子电器产品的推荐系统的设计与实现源码文档 视频演示 https://www.bilibili.com/video/BV1HW4y197Fe/ 系统介绍 摘 要 …

dubbo源码之-ExtensionInjector

dubbo源码之-ExtensionInjector 概述源码入口Extension 是如何获取到&#xff1f;SpiExtensionInjector 概述 其实ExtensionInjector 非常简单&#xff0c; 我们知道dubbo有ioc注入的功能&#xff0c; 是靠的set方法注入&#xff0c;对应的底层源码主要是ExtensionInjector 如…

MySQL数据库语言一、DDL

&#x1f618;作者简介&#xff1a;正在努力的99年打工人。 &#x1f44a;宣言&#xff1a;人生就是B&#xff08;birth&#xff09;和D&#xff08;death&#xff09;之间的C&#xff08;choise&#xff09;&#xff0c;做好每一个选择。 &#x1f64f;创作不易&#xff0c;动…

华为OD机试真题B卷 JavaScript 实现【分班】,附详细解题思路

一、题目描述 幼儿园两个班的小朋友在排队时混在了一起&#xff0c;每位小朋友都知道自己是否与前面一位小朋友是否同班&#xff0c;请你帮忙把同班的小朋友找出来。 小朋友的编号为整数&#xff0c;与前一位小朋友同班用Y表示&#xff0c;不同班用N表示。 二、输入描述 输…

uniapp/手机APP使用支付宝支付(服务端)

博主介绍&#xff1a;✌全网粉丝4W&#xff0c;全栈开发工程师&#xff0c;从事多年软件开发&#xff0c;在大厂呆过。持有软件中级、六级等证书。可提供微服务项目搭建与毕业项目实战、定制、远程&#xff0c;博主也曾写过优秀论文&#xff0c;查重率极低&#xff0c;在这方面…

chatgpt赋能python:Python接口应用于SEO的指南

Python接口应用于SEO的指南 Python成为了web开发中最流行的语言之一&#xff0c;而且尤其在SEO领域中被广泛应用。一些Python库和框架可帮助SEO团队实现其目标&#xff0c;如排名跟踪&#xff0c;爬取数据&#xff0c;进行网站分析&#xff0c;等等。在本文中&#xff0c;我们…

基于Hexo和Butterfly创建个人技术博客,(9) 优化butterfly主题配置文章版本

Butterfly官方网站&#xff0c;请 点击进入 本章目标&#xff1a; 掌握butterfly主题对文章的配置&#xff0c;熟悉并可按需配置到个人的博客站点中&#xff0c;本章内容是一个必会章节&#xff0c;不仅包括文章的UI美化、SEO相关配置还包括其它增加的功能&#xff0c;内容不多…

英语不好能不能学编程?

入门教程、案例源码、学习资料、读者群 请访问&#xff1a; python666.cn 大家好&#xff0c;欢迎来到 Crossin的编程教室 &#xff01; 常有人问我&#xff1a;我英语不好&#xff0c;想学编程行不行&#xff1f; 这个问题需要分情况讨论。 1. 可以学 如果你因为担心自己英语不…

chatgpt赋能python:Python怎么用?Python编程的入门指南

Python怎么用&#xff1f;Python编程的入门指南 Python是一种流行的高级编程语言&#xff0c;它被广泛应用于数据分析、机器学习、Web开发、自动化测试等领域。Python语言非常容易学习和使用&#xff0c;因此非常适合初学者和有经验的开发人员。在这篇文章中&#xff0c;我们将…

手把手教你在CentOS7.9上使用docker 安装MySQL5.7

前言 大家好&#xff0c;又见面了&#xff0c;我是沐风晓月&#xff0c;本文主要讲解如何用docker在centos7.9系统上安装MySQL5.7&#xff0c;以及如何设置MySQL的远程登录。 文章收录到【容器管理】和【数据库入门到精通专栏】&#xff0c;此专栏是沐风晓月对linux云计算架构…

chatgpt赋能python:Python怎么清除动点轨迹?

Python怎么清除动点轨迹&#xff1f; 引言 在数据科学和可视化的领域中&#xff0c;动点轨迹是很有用的工具。动点轨迹可以轻松地显示数据点的时间序列&#xff0c;这可以帮助分析者发现有关数据集的有用信息。然而&#xff0c;当轨迹过于密集和复杂时&#xff0c;这种可视化…

Spring Cloud Alibaba - Sentinel源码分析(一)

目录 一、Sentinel核心源码分析 1、Sentinel核心概念 1.1、Node之间的关系 2、Sentinel源码入口 2.0、Sentinel源码启动 2.1、SlotChain解析 2.2、NodeSelectorSlot解析 2.3、ClusterBuilderSlot解析 2.4、StatisticSlot解析 2.5、FlowSlot解析 2.6、DegradeSlot解析…

白鲸优化算法优化VMD参数,最小包络熵为适应度函数,提取最小包络熵对应的IMF分量,采集最佳IMF分量的9种时域指标,提取特征向量。以西储大学数据为例,附MATLAB代码

大家看到这篇文章&#xff0c;肯定会有疑问&#xff0c;难道本篇文章和上一篇文章不是一个意思嘛&#xff0c;这是来凑数的嘛……其实不然&#xff0c;如果各位读者仔细看&#xff0c;就会发现本篇文章和上一篇文章大有不同&#xff0c;这篇文章也是我一直以来想在上一篇文章基…

chatgpt赋能python:Python断言:如何断言等于两个值其中一个?

Python断言&#xff1a;如何断言等于两个值其中一个&#xff1f; Python是一种广泛使用的编程语言&#xff0c;而断言是它的一个重要功能。在编程中&#xff0c;我们可以使用断言来验证代码是否按照预期工作。但是&#xff0c;在某些情况下&#xff0c;我们可能想要断言两个值…

快速上手kettle(四)壶中可以倒出些啥?

快速上手kettle&#xff08;四&#xff09;壶中可以倒出些啥 前言一 、kettle 这壶里能倒出啥&#xff1f;二 、Access输出2.1 Access输出设置2.2 启动转换&#xff0c;查看输出 三 、Excel输出3.1 选择excel扩展名3.2 1 将表中数据分别写入到excel中 四、JSON output4.1 JSON …

常见骨干网络介绍

骨干网络 骨干网络&#xff08;backbone network&#xff09;顾名思义&#xff0c;是深度学习中最核心的网络组成。本文按时间顺序&#xff0c;简要介绍几种影响重大的backbone设计思路&#xff0c;我们或许可以从窥探前人的设计思路中获得启发和灵感。 1.1 AlexNet, 2012 这…

10倍|中科院再传好消息:比英伟达还快,“新技术”实现弯道超车

中科院再传好消息&#xff1a;在光芯片上有了重大突破&#xff0c;李明-祝宁华团队研制出了一款超高集成度光学卷积处理器。 这种方案具有高算力密度、超高的线性扩展性&#xff01; 基于这种技术的&#xff0c;光芯片的性能将再次提升&#xff0c;光芯片是用于AI,如果能变成现…

每日算法(第二十四期)

先来回顾一下上期的问题及答案&#xff1a; 2023年6月15日 「电话号码的字母组合」&#xff08;Letter Combinations of a Phone Number&#xff09;。以下是题目的描述&#xff1a; 给定一个仅包含数字 2-9 的字符串&#xff0c;返回所有它能表示的字母组合。给出数字到字母的…