学习cel-go了解一下通用表达语言评估是什么

文章目录

    • 1. 前言
    • 2. cel-go
      • 2.1 cel-go关键概念
        • Applications(应用)
        • Compilation(编译)
        • Expressions(表达式)
        • Environment环境
        • 解析表达式的三个阶段
    • 3. cel-go的使用
    • 4. cel-go使用
    • 5. 说明
    • 6. 小结
    • 7. 参考

1. 前言

最近因为在项目里面实现的一个使用+和||来组合获取字段值的功能有点儿弱了,就想着没有什么库可以改进这一些东西,增强数据获取的解析能力,让获取不同字段拼接值的数据变得更加简单。

于是去github看了一下,发现一个cel-go的库,这个库支持对表达式进行取值和判断,可以满足我的要求,于是就学习了一下这个库相关的使用。

2. cel-go

cel这个名字的全称是Common Expression Language,简单翻译就是通用表达语言。官方给的解释如下:

The Common Expression Language (CEL) is a non-Turing complete language designed for simplicity, speed, safety, and portability. CEL’s C-like syntax looks nearly identical to equivalent expressions in C++, Go, Java, and TypeScript.
通用表达语言(CEL)是一种非图灵完备性语言,旨在简单、快速、安全和可移植。CEL的类C语法看起来与C++、Go、Java和TypeScript中的等效表达式几乎相同。

CEL程序是单个表达式,cel-go推荐在使用此库的时候去Codelab学习下,然后评估是否应该选择此库。

这个CodeLab库主要是介绍关于CEL库的一些概念、如何判断CEL是否可用于你的项目、cel-go运行的相关概念以及给出了一些练习的case,帮助你快速了解CEL是什么,以及cel-go如何使用。

这里单独拎出来cel-go关键概念来学习一下。

2.1 cel-go关键概念

**注:**下面内容从2. Key concepts章节翻译摘抄过来。

Applications(应用)

CEL是通用的,可以用于各种应用,从路由RPC到定义安全策略。CEL可扩展、与应用程序无关、并且针对一次编译多次评估的工作流程进行了优化。

许多服务和应用程序都会评估声明性配置。例如,基于角色的控制访问(RABC)是一种声明性配置,可以在给定角色和一组用户的情况下生成访问决策,如果声明性配置占80%的用例,当用户需要更强的表达能力时,CEL是一个非常有用的工具,可以补充剩余的20%。

K8s中就利用cel-go做了一些关于配置数据的验证以及一些状态的验证逻辑。具体可以参考:Common Expression Language in Kubernetes

Compilation(编译)

表达式是针对环境编译的。编译步骤会生成protobuf形式的抽象语法树(AST)。编译后的表达式通常会被存储以供将来使用,以尽可能快地进行计算。可以使用许多不同的输入来计算单个编译表达式。(这里有个实践应用的优化tips,在项目中,一般都会将编译后生成的AST用本地缓存存储起来,后续使用的时候直接取出,而不是每次使用都编译。)

Expressions(表达式)

用户定义表达式,服务和应用程序定义其运行的环境。函数签名声明输入,并写在CEL表达式之外。CEL可用的函数库是自动导入的(这里从代码里面看的话,有个标准函数库,在使用cel.NewEnv构建env的时候会自动导入)。

下面示例中,表达式采用请求对象,并且该请求包含声明令牌,表达式返回一个布尔值,指示声明令牌是否仍然有效。

// Check whether a JSON Web Token has expired by inspecting the 'exp' claim.
//
// Args:
//   claims - authentication claims.
//   now    - timestamp indicating the current system time.
// Returns: true if the token has expired.
//
timestamp(claims["exp"]) < now
Environment环境

环境由服务定义。嵌入CEL的服务和应用程序声明表达式环境。环境是可以在表达式中使用的变量和函数的集合。

CEL类型检查器使用基于原型的声明来确保表达式中所有标识符和函数引用均已正确声明和使用。

解析表达式的三个阶段

处理表达式分为三个阶段:解析、检查和评估。CEL最常见的模式是控制平面(Control Plane)在配置时解析和检查表达式,并存储AST。

在运行时,数据平面重复检索并评估AST。CEL针对运行时效率进行了优化,但解析和检查不应在延迟关键代码路径中进行。(这里就对应里面的Store AST,意思是解析和检查需要保存下来,而且处理的路径应该是旁路,而不是关键代码路径中)

使用ANTLR词法分析器/解析器语法将CEL从人类可读的表达式解析为抽象语法树。解析阶段发出一个基于原型的抽象语法树,其中AST中的每个Expr节点都包含一个整数id,用于索引解析和检查期间生成的元数据。解析过程中生成的syntax.proto忠实地表示了以表达式的字符串形式键入的内容的抽象表达。

一旦表达式被解析,就可以根据环境进行检查,以确保表达式中的所有变量和函数标识符都已声明并正确使用。类型检查器生成一个checked.proto,其中包含类型、变量和函数解析元数据,可以极大地提高评估效率。

CEL评估员(用户)需要做的三件事:

  • 任何自定义扩展的函数绑定
  • 变量绑定
  • 要评估的AST

函数和变量绑定应与编译AST所使用的绑定相匹配。这些输入中的任何一个都可以在多个评估中重复使用,例如跨多组变量评估的AST,或者针对许多AST使用相同的变量,或者在进程的声明周期中使用的函数绑定(常见情况)。

3. cel-go的使用

关于cel-go的练习例子,可以直接通过4. Hello, World!经典的Hello,World章节进行练习。这些case主要的学习知识如下:

  • exercise1
    • 主要是学习表达式的编译以及评估
  • exercise2
    • 主要是学习在定义环境的时候引入新的变量,以及如何引入变量的数据类型
  • exercise3
    • 主要是学习如何使用逻辑与或表达式的使用
  • exercise4
    • 主要是学习如何自定义函数逻辑,虽然cel-go的std.lib提供了许多有用的类型转换、时间计算等相关的函数,但涉及到一些自定义的业务逻辑,就需要一些自定义函数来支撑了。
  • exercise5
    • 主要是学习如何构建出一个json数据,主要是为了表达CEL并非只能输出布尔类型结果,同样可以输出其他数据类型的结果
  • exercise6
    • 主要是学习构建protos,这里主要是将具体的数据编译到我们的proto定义的message结构中去。这里会学习cel.Container(name string) 的概念,主要是为具体的proto的message准备的。

通过上面6个case的学习基本可以知道cel-go的使用。

4. cel-go使用

在使用之前我们可以结合上面的6个case,给出一个具体的需求,然后在此需求之上利用cel-go进行不同表达式的实现。

需求:需要实现从一个json string的字符串中读取出内部json字段的数据,与”hello”字符串通过“-”进行拼接,同时对于json string中含有的时间字段与当前时间进行时间对比,如果比当前时间小则拼接“before”,否则拼接“after”。

package main

import (
	"encoding/json"
	"fmt"
	"github.com/google/cel-go/cel"
	"github.com/google/cel-go/common/types"
	"github.com/google/cel-go/common/types/ref"
	"github.com/google/cel-go/common/types/traits"
	"google.golang.org/protobuf/encoding/prototext"
	"google.golang.org/protobuf/proto"
	"log"
	"reflect"
	"sort"
	"strings"
	"time"

	rpcpb "google.golang.org/genproto/googleapis/rpc/context/attribute_context"
	structpb "google.golang.org/protobuf/types/known/structpb"
	tpb "google.golang.org/protobuf/types/known/timestamppb"
)

func main() {
	exercise7()
}

func exercise7() {
	fmt.Println("==== Exercise 7: self-coding ====")
	// json数据字符串
	dataStr := `{
			"payment":{"payment_id":111,"ctime":1717834001},
			"transaction":{"transaction_id":333}
		}
`
	// 新构建一个运行时环境
	env, err := cel.NewEnv(
		// 定义变量名以及数据类型,表示data的数据类型为map,key为string,值是动态的不确定
		cel.Variable("data", cel.MapType(cel.StringType, cel.DynType)),
		// 定义变量名为now, 类型为时间戳类型
		cel.Variable("now", cel.TimestampType),
		// define a single args function
		// 定义函数,函数名称为json_int_to_str,接收一个double类型的参数,返回一个string参数
		cel.Function("json_int_to_str",
			cel.Overload("double_to_str", []*cel.Type{cel.DoubleType}, cel.StringType,
				cel.UnaryBinding(func(value ref.Val) ref.Val {
					return value.ConvertToType(cel.StringType)
				}))),
		// 定义函数compare_time比较时间,接受一个double类型的参数和一个时间类型的参数,返回布尔值
		cel.Function("compare_time",
			cel.Overload("compare_time", []*cel.Type{cel.DoubleType, cel.TimestampType}, cel.BoolType,
				cel.BinaryBinding(func(lhs ref.Val, rhs ref.Val) ref.Val {
					v := rhs.Value().(time.Time).Unix()-lhs.ConvertToType(cel.IntType).Value().(int64) >= 0
					return types.Bool(v)
				}))),
	)
	if err != nil {
		log.Fatalf("loading env error: %v", err)
	}
	// 将表达式编译成ast树(这里的compile会执行编译&检查)
	ast, issues := env.Compile(`json_int_to_str(data.payment.payment_id) + "-" + "hello you! " + (compare_time(data.payment.ctime, now - duration('500s')) ? "before":"after")`)
	if issues.Err() != nil {
		log.Fatal(issues.Err())
	}
	// 利用当前的运行环境构建出一个Ast的评估实例
	program, err := env.Program(ast)
	if err != nil {
		log.Fatalf("program error: %v", err)
	}
	// 对数据进行pb结构的反序列化
	structData := &structpb.Struct{}
	err = json.Unmarshal([]byte(dataStr), structData)
	if err != nil {
		log.Fatalf("unmarshal dataStr error: %v", err)
	}
	// 最后数据的评估阶段
	val, details, err := program.Eval(map[string]interface{}{"data": structData, "now": &tpb.Timestamp{Seconds: time.Now().Unix()}})
	fmt.Println(val, details, err)
}

解释:

  1. 运行的环境,如果在表达式中需要使用一些变量作为表达式的数据获取,则此变量一定需要再构建env的时候声明,否则会报错undeclared reference to 'data' (in container '') 表示data字段未声明。
  2. 自定义函数,如果我们想要将int转成str,实际std.lib是有对应的函数支持的,但如果我们非要自己实现,就需要实现函数的声明与逻辑的实现,否则会报错 undeclared reference to 'json_int_to_str' (in container '') 表示这个函数未声明
  3. 编译阶段和构建Ast实例这里,是解析表达式的必经之路,这里主要是利用词法分析器将我们的表达式解析为抽象语法树,即从人能够理解的表达式转变为解析器能够理解的语法树,从而为后续的表达式评估做准备。
  4. 数据的反序列化,数据是序列化的json字符串,我们就可以利用pb中的Struct结构进行反序列化,后在评估的时候将数据显式通过之前在构建env时候声明过的变量传入即可。

最后输出的结果为

==== Exercise 7: self-coding ====
111-hello you! before <nil> <nil>

上面的自定义int转为string的函数,在std.lib中有声明,所以可以直接使用string(data.payment.payment_id)也是可以实现数字转字符串的效果。

// String conversion functions.
const (
	StringToString    = "string_to_string"
	BoolToString      = "bool_to_string"
	IntToString       = "int64_to_string"
	UintToString      = "uint64_to_string"
	DoubleToString    = "double_to_string"
	BytesToString     = "bytes_to_string"
	TimestampToString = "timestamp_to_string"
	DurationToString  = "duration_to_string"
)

stdFunctions = []*decls.FunctionDecl{
// String conversions
	function(overloads.TypeConvertString,
		decls.Overload(overloads.StringToString, argTypes(types.StringType), types.StringType,
			decls.UnaryBinding(identity)),
		decls.Overload(overloads.BoolToString, argTypes(types.BoolType), types.StringType,
			decls.UnaryBinding(convertToType(types.StringType))),
		decls.Overload(overloads.BytesToString, argTypes(types.BytesType), types.StringType,
			decls.UnaryBinding(convertToType(types.StringType))),
		decls.Overload(overloads.DoubleToString, argTypes(types.DoubleType), types.StringType,
			decls.UnaryBinding(convertToType(types.StringType))),
		decls.Overload(overloads.DurationToString, argTypes(types.DurationType), types.StringType,
			decls.UnaryBinding(convertToType(types.StringType))),
		decls.Overload(overloads.IntToString, argTypes(types.IntType), types.StringType,
			decls.UnaryBinding(convertToType(types.StringType))),
		decls.Overload(overloads.TimestampToString, argTypes(types.TimestampType), types.StringType,
			decls.UnaryBinding(convertToType(types.StringType))),
		decls.Overload(overloads.UintToString, argTypes(types.UintType), types.StringType,
			decls.UnaryBinding(convertToType(types.StringType)))),
}

func convertToType(t ref.Type) functions.UnaryOp {
	return func(val ref.Val) ref.Val {
		return val.ConvertToType(t)
	}
}

// Type conversion methods and overloads
const (
	TypeConvertInt       = "int"
	TypeConvertUint      = "uint"
	TypeConvertDouble    = "double"
	TypeConvertBool      = "bool"
	TypeConvertString    = "string"
	TypeConvertBytes     = "bytes"
	TypeConvertTimestamp = "timestamp"
	TypeConvertDuration  = "duration"
	TypeConvertType      = "type"
	TypeConvertDyn       = "dyn"
)

5. 说明

除了我们上面的使用例子中说的cel-go的使用用法,cel-go还支持自定义env的构建:

// NewCustomEnv creates a custom program environment which is not automatically configured with the
// standard library of functions and macros documented in the CEL spec.
//
// The purpose for using a custom environment might be for subsetting the standard library produced
// by the cel.StdLib() function. Subsetting CEL is a core aspect of its design that allows users to
// limit the compute and memory impact of a CEL program by controlling the functions and macros
// that may appear in a given expression.
//
// See the EnvOption helper functions for the options that can be used to configure the
// environment.
func NewCustomEnv(opts ...EnvOption) (*Env, error) {
	// ...
}

支持类型的自定义

func init() {
	// 这是初始化时候定义的list和map类型
	paramA := types.NewTypeParamType("A")
	paramB := types.NewTypeParamType("B")
	listOfA := types.NewListType(paramA)
	mapOfAB := types.NewMapType(paramA, paramB)
}

同样,我们也可以使用自定义的数据进行函数的定义与特定逻辑的处理

env, err := cel.NewEnv(
		// 省略...
		cel.Function("in_concat",
			cel.Overload("in_concat", []*cel.Type{cel.DynType, cel.ListType(types.NewTypeParamType("C"))}, cel.BoolType,
				cel.BinaryBinding(func(lhs ref.Val, rhs ref.Val) ref.Val {
					list := rhs.ConvertToType(types.ListType)
					strArray := make([]string, 0)
					for _, l := range list.Value().([]ref.Val) {
						if v, ok := l.Value().(int64); ok {
							strArray = append(strArray, strconv.FormatInt(v, 10))
						}
					}
					sType := lhs.ConvertToType(types.StringType)
					return types.Bool(sType.Value().(string) == strings.Join(strArray, ""))
				}))),
	)
ast, issues := env.Compile(`in_concat(data.payment.payment_id, [1,11])`)

我们通过自定义出ListType,从而实现判断payment_id是否在数组拼接的结果中,从而实现特定的逻辑。注意由于在标准库的init()函数中已经声明了ListType,如果我们没有使用自定义env的方法,而使用NewEnv的方法的同时,有自定义ListType,这样会导致type在注册校验的时候报错:

	paramTypeC := types.NewTypeParamType("C")
	customListType := types.NewListType(paramTypeC)

	env, err := cel.NewEnv(
		cel.Types(customListType)
	)
	
	// 报错:loading env error: type registration conflict. found: list(), input: list(C)

另外关于优化的部分,这里有一些在评估表达式时候的优化,对于一些常规的表达式,我们可以看到它的判断都是在Optimize这里的逻辑进行判断和评估的,主要是一些逻辑与或或者元素的in相关的逻辑,可以看下:

// Optimize applies a sequence of optimizations to an Ast within a given environment.
//
// If issues are encountered, the Issues.Err() return value will be non-nil.
func (opt *StaticOptimizer) Optimize(env *Env, a *Ast) (*Ast, *Issues) {
	// 省略....
}

func maybePruneBranches(ctx *OptimizerContext, expr ast.NavigableExpr) bool {
	call := expr.AsCall()
	args := call.Args()
	switch call.FunctionName() {
	case operators.LogicalAnd, operators.LogicalOr:
		return maybeShortcircuitLogic(ctx, call.FunctionName(), args, expr)
	case operators.Conditional:
		cond := args[0]
		truthy := args[1]
		falsy := args[2]
		if cond.Kind() != ast.LiteralKind {
			return false
		}
		if cond.AsLiteral() == types.True {
			ctx.UpdateExpr(expr, truthy)
		} else {
			ctx.UpdateExpr(expr, falsy)
		}
		return true
	case operators.In:
		haystack := args[1]
		if haystack.Kind() == ast.ListKind && haystack.AsList().Size() == 0 {
			ctx.UpdateExpr(expr, ctx.NewLiteral(types.False))
			return true
		}
		needle := args[0]
		if needle.Kind() == ast.LiteralKind && haystack.Kind() == ast.ListKind {
			needleValue := needle.AsLiteral()
			list := haystack.AsList()
			for _, e := range list.Elements() {
				if e.Kind() == ast.LiteralKind && e.AsLiteral().Equal(needleValue) == types.True {
					ctx.UpdateExpr(expr, ctx.NewLiteral(types.True))
					return true
				}
			}
		}
	}
	return false
}

6. 小结

因为想要增强词法解析能力,花时间去学习了一下cel-go,发现cel-go是非常强大的,虽然自己只是学习了一些使用的基本功能,要想把cel-go学好,还是需要去看更多的cel-go的源码。

这里主要是针对cel-go的基本功能进行了学习,让自己可以通过cel-go构建出声明了变量、函数、自定义函数等的环境,然后将自己需要实现功能的逻辑变成表达式,利用cel-go的编译和程序功能生成AST抽象语法树,最后将定义变量的数据送入到环境评估器中,得到最终的表达式结果。

7. 参考

  • cel-go
  • CEL-Go Codelab:
    Fast, safe, embedded expressions

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

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

相关文章

一键解锁创意无界:高效AI生成古典肖像图片,轻松打造艺术化身

在数字化时代&#xff0c;创意与艺术的结合正逐渐改变我们的生活。你是否曾梦想过拥有一幅专属于自己的古典肖像画&#xff0c;却又苦于找不到合适的画师或高昂的费用而望而却步&#xff1f;现在&#xff0c;这一切都将成为现实&#xff01; 进入首助编辑高手的AI魔法智绘图板块…

Java—装饰器模式

介绍 装饰器模式 装饰器模式&#xff08;Decorator Pattern&#xff09;是一种结构型设计模式&#xff0c;它允许你动态地将行为添加到现有的对象中&#xff0c;而无需修改其代码。装饰器模式提供了比继承更灵活的功能扩展方式。 主要角色 Component&#xff1a;定义一个对…

【编程技巧】降低程序复杂度:控制逻辑与业务逻辑分离

为什么要降低代码复杂度 好的项目都是迭代出来的&#xff0c;所以代码肯定是会被人维护的 降低代码复杂度就是为了降低下一个维护人的维护成本&#xff0c;更简单地理解跟修改代码 代码组成 代码逻辑 控制逻辑 业务逻辑 控制逻辑 控制业务逻辑的代码 例如&#xff1a;加缓存…

Redis-sentinel(哨兵模式)的搭建步骤及相关知识

1、什么是redis-sentinel&#xff0c;和redis主从复制相比&#xff0c;它具有什么优势 1.1、redis主从复制 Redis主从复制是一种用于数据冗余和可伸缩性的机制&#xff0c;它将一台Redis服务器的数据复制到其他Redis服务器。在这种模式下&#xff0c;数据会实时地从一个主节点…

PS通过GTX实现SFP网络通信1

将 PS ENET1 的 GMII 接口和 MDIO 接口 通过 EMIO 方 式引出。在 PL 端将引出的 GMII 接口和 MDIO 接口与 IP 核 1G/2.5G Ethernet PCS/PMA or SGMII 连接&#xff0c; 1G/2.5G Ethernet PCS/PMA or SGMII 通过高速串行收发器 GTX 与 MIZ7035/7100 开发…

Pytest 读取excel文件参数化应用

本文是基于Pytest框架&#xff0c;读取excel中的文件&#xff0c;传入页面表单中&#xff0c;并做相应的断言实现。 1、编辑媒体需求 首先明确一下需求&#xff0c;我们需要对媒体的表单数据进行编辑&#xff0c;步骤如下&#xff1a; 具体表单如下图所示 1、登录 2、点击我…

配置中心理论学习

配置中心是一种用于集中管理应用程序配置信息的系统或服务。在微服务架构中&#xff0c;由于服务数量众多且可能分布在不同的环境中&#xff0c;配置中心的作用尤为突出。它允许开发者将配置信息从应用程序代码中分离出来&#xff0c;集中存储和管理&#xff0c;从而提高配置的…

训练营第三十八天 | 309.最佳买卖股票时机含冷冻期动态规划系列七总结714.买卖股票的最佳时机含手续费股票问题总结篇!

309.最佳买卖股票时机含冷冻期 力扣题目链接(opens new window) 给定一个整数数组&#xff0c;其中第 i 个元素代表了第 i 天的股票价格 。 设计一个算法计算出最大利润。在满足以下约束条件下&#xff0c;你可以尽可能地完成更多的交易&#xff08;多次买卖一支股票&#x…

Linux文本处理三剑客+正则表达式

Linux文本处理常用的3个命令&#xff0c;脚本或者文本处理任务中会用到。这里做个整理。 三者的功能都是处理文本&#xff0c;但侧重点各不相同&#xff0c;grep更适合单纯的查找或匹配文本&#xff0c;sed更适合编辑匹配到的文本&#xff0c;awk更适合格式化文本&#xff0c;对…

硬件SPI读写W25Q64

硬件SPI读写W25Q64 接线图&#xff08;和软件SPI一样&#xff09; 使用SPI1&#xff0c;SCK&#xff0c;接PA5&#xff1b;MISO&#xff0c;接PA6&#xff1b;MOSI&#xff0c;接PA7&#xff1b;NSS&#xff0c;可接PA4。 接线图对应&#xff1a;PA5接CLK引脚&#xff0c;PA6…

基于深度学习的红外船舶检测识别分类完整实现数据集8000+张

随着遥感技术的快速发展&#xff0c;包括无人机、卫星等&#xff0c;红外图像在船舶检测识别中的作用日益凸显。相对于可见光图像&#xff0c;红外图像具有在夜晚和恶劣天气条件下高效检测识别船舶的天然优势。近年来&#xff0c;深度学习作为一种强大的图像处理技术&#xff0…

微服务链路追踪ELK

微服务链路追踪&ELK 链路追踪概述链路追踪sluthzipkinelk日志管理平台 一 链路追踪 1 概述 1.1 为什么需要链路追踪 ​ 微服务架构是一个分布式架构&#xff0c;它按业务划分服务单元&#xff0c;一个分布式系统往往有很多个服务单元。由于服务单元数量众多&#xff0…

sqoop的安装配置

1. 上传并解压安装包 tar -zxvf sqoop-1.4.7.bin__hadoop-2.6.0.tar.gz -C ../server/ 重命名&#xff1a;mv sqoop-1.4.7.bin__hadoop-2.6.0 sqoop 2. 配置环境变量 sudo vim /etc/profile # 配置sqoop的环境变量 export SQOOP_HOME/export/server/sqoop export PATH$PATH…

Camtasia2024破解永久激活码注册码分享最新

随着数字时代的到来&#xff0c;视频制作已成为许多人日常生活和工作中不可或缺的一部分。而在众多视频编辑软件中&#xff0c;Camtasia凭借其强大的功能和易用性&#xff0c;赢得了广泛的用户喜爱。近期&#xff0c;Camtasia 2024的破解版本在网络上引起了广泛关注。本文旨在为…

FreeRTOS移植:STM32L476 nucleo-L476RG 开发板《03》

系列文章 FreeRTOS移植&#xff1a;STM32L476 nucleo-L476RG 开发板《01》 FreeRTOS移植&#xff1a;STM32L476 nucleo-L476RG 开发板《02》 说明 在上篇 FreeRTOS移植&#xff1a;STM32L476 nucleo-L476RG 开发板《02》 开始移植适配 FreeRTOS&#xff0c;FreeRTOS 移植适配…

大数据技术学习回顾01-大数据的特点、技术体系(三驾马车)

参考来源&#xff1a; 极客时间专栏&#xff1a;从0开始学大数据&#xff0c;作者&#xff1a;李智慧 大数据技术整体概览 大数据技术体系 大数据处理的主要应用场景包括数据分析、数据挖掘与机器学习。数据分析主要使用 Hive、Spark SQL 等 SQL 引擎完成&#xff1b;数据挖…

LaTeX 学习 第2节 数学结构

----用教授的方式学习 目录 2.1 上标与下标 2.2 上下画线与花括号 2.3 分式 2.4 根式 2.5 矩阵 ​​​​​​​LaTex安装包&#xff1a;https://download.csdn.net/download/weixin_38135241/89416392 LaTex- windows安装包&#xff1a;https://download.csdn.net/down…

unity38——MemoryProfiler性能分析器,截帧分析当前性能占用率的具体文件

定义&#xff1a;性能分析器 (Unity Profiler) 是一种可以用来获取应用程序性能信息的工具。可以将性能分析器连接到网络中的设备或连接到已连接到计算机的设备&#xff0c;从而测试应用程序在目标发布平台上的运行情况。还可以在 Editor 中运行性能分析器&#xff0c;从而在开…

ESP32 BLE学习(0) — 基础架构

前言 &#xff08;1&#xff09;学习本文之前&#xff0c;需要先了解一下蓝牙的基本概念&#xff1a;BLE学习笔记&#xff08;0.0&#xff09; —— 基础概念&#xff08;0&#xff09; &#xff08;2&#xff09; 学习一款芯片的蓝牙肯定需要先简单了解一下该芯片的体系结构&a…

shell编程基础(第18篇:更多的文件操作命令介绍)

前言 对于文件来说&#xff0c;除了它的文件内容之外&#xff0c;就是对其文件本身的操作&#xff0c;比如我们想要重命名文件、移动文件、复制文件、已经获取文件所在目录&#xff0c;文件名等操作&#xff0c;今天一起学习更多的文件操作相关的命令 basename 用于获取文件名…