本周推荐 | 表达式引擎的组合子实现方案

news2024/11/26 4:28:04

9bf895b2ee49531cccfec081ab62608b.gif

推荐语:本文清晰而详细地介绍了如何使用 Parser 组合子方案,结合 Monad 通过合理的分层、抽象和组合,在性能达标的情况下实现消息场景中函数式的表达式解析。非常具有实践意义,推荐阅读学习!

——大淘宝技术终端开发工程师 闲行

c29b952f71fa52f1c3bc21231b372fe1.png

什么是表达式引擎

从定义上讲,表达式(Expression)是由代表量的符号和代表运算的符号组成的符合规则的组合。简单地说:
一个良好定义的表达式执行完成会产生一个值。
比如执行表达式 1+2*3 得到结果 7${version}>10 根据version返回 Bool 值。

代码里的表达式会在编译时编程2进制代码,在运行期得到结果,在某些情况下,我们需要计算字符串形式的表达式,此时解析并执行表达式的程序称为表达式引擎。

418ff821ff4ea89961295bd096a0e4fc.png

为什么需要一个表达式引擎

主要是解决多维度Key导致配置数量爆炸的问题。

一个大型程序往往有一部分配置的数据,可以在程序不变的情况下改变行为。一般情况下,配置方案体现为 Key -> Value的形式,稍微复杂一点,考虑到需要对不同业务进行隔离和管控,会对 Key 进行分组,再复杂一点,需要支持同一个 Key 针对不同的策略有不同的值,比如不同版本的配置不同。

但是消息场景的逻辑更复杂,多维度导致配置数量爆炸,举个例子,消息气泡是否支持转发功能,目前由 消息状态、业务类型、模板类型、模板实例类型四个维度决定,消息状态有5种,业务类型目前有80+,模板类型300+,模板实例类型2000+,Key的值理论上和这些维度是乘积关系,有超过 2亿 种情况,这对于一个配置来说当然是不可接受的。

之前的方案是加入优先级和默认逻辑,即先考虑最细粒度的模板实例类型,如果没有再考虑模板类型,以此按优先级决定是否支持转发,对于不支持转发的,默认为空,默认逻辑本质上是代码也表示了一部分配置。优先级逻辑将 Key 值与维度的关系从乘法变成了加法,配合默认值,我们将转发的配置数量降低到了360多条。

除了转发,考虑到其他长按菜单、页面配置、输入能力等场景,总的配置条数超过了600条,这些配置能不能由代码自动维护呢,答案是不行,因为这些配置都是和产品逻辑相关联的,比如某些消息从支持转发变为不支持转发,类似的产品需求和变更是需要人来执行的,这就带来了很多维护的问题:

  1. 内容冗余,配置中存在大量的 Key 具有同样的 Value,导致大量的冗余,存在进一步优化的空间;

  2. 规则碎片化,很难理解规则的总和,这是由于这些规则不是独立的,比如想知道转发的完整规则是什么,就必须汇总360多条记录才能得出结果,这就会导致实际规则难以梳理、与产品预期不一致的问题;

  3. 变更影响范围大,当规则变化、或者新增 Value 时,需要修改大量已有的配置数据,比如千牛上的商家群、粉丝群等5种需要去掉红包能力,由于输入能力和群成员角色有关,群成员有4个角色,总共需要修改20条配置,大幅增加了出错的概率。如果新增维度涉及到优先级的变化,此时需要修改代码,影响面就变成了所有的配置项,带来大量的测试验证成本和风险;

  4. 逻辑实现复杂,对于多维度优先级的逻辑和默认逻辑的支持,以及像自己发送的消息才支持撤回这样的具体逻辑,导致配置与代码边界不清晰,实现逻辑比较复杂,这就会带来可读性、可维护性和稳定性的问题。

通过仔细梳理了这些配置,发现这些这些问题的出现是因为以 Key 的规则为出发点去思考的,而 Key 的逻辑各不相同又不断变化,从而导致了各种问题。很自然的联想到,能不能从 Value 的角度出发,同一个 Value 的配置只有一条,这样就能去掉大量的冗余,另外 Value 是直接表示业务逻辑,Value归一也便于对业务迭代的支持。

这时,Value对应的就不是一个 Key 了,而是多个 Key 的并,这就需要表达式来表示这个并。

dd0350cce54e22a7b53a8e2bd178cfdb.png

表达式的能力

先来看一个简单的表达式:

${bizType} >= 11000 && ${bizType} < 12000

这时用来表示BC场景的表达式,当处于BC聊天页面时,表达式的结果为 true,否则为 false,可以看到,表达式引擎的输入是字符串和环境变量,输出Bool值,因此这是一个解释器的工作,具体来说分为两步:

  1. Parse,将表达式解析成一个表达式树;

  2. Eval,使用环境变量对表达式树求值。

根据业务场景梳理出表达式需要支持的文法:

expr := expr andor term | term
 andor := && | ||
 term := factor compare factor | factor
 compare := == | >= | > | <= | < | != 
 factor := identifier | num | str | (expr) 
 identifier := "${"{a-zA-z0-9_.}"}"
 str := "\""string"\""
 num := ["-"]{0-9}

811911407aea48039358f1fee51dd2c2.png

表达式1.0的问题

第一个版本的表达式我们使用了苹果的谓词表达式 NSPredicate,成功将规则总条数降低了87%。

使用NSPredicate 遇到了几个问题,比如为了保证规则的双端一致性,采用了业界较为常用的 ${var} 的格式,在 iOS 端需要预处理成 $var 的形式,以及如果变量在调用 evaluateWithObject:substitutionVariables: 时不存在会直接crash,导致老版本功能不正常,我们也进行了发布策略上的兼容。但是在上线使用后,还是遇到了一个无法解决的问题:

NSPredicate不支持嵌套取值,有些业务场景需要使用消息中嵌套较深的数据,如 ${message.ext.templateDynamicData.subscribeInfo.showType} == 6,每次遇到这样的需求都需要编码把数据取出来,再加到环境变量中去,在多次遇到这个问题之后,一方面产生了很多和业务的沟通协作成本,另一方面也让我们参数文档名存实亡;

随着业务多次提出相关的问题,以及在 NSPredicate 上扩展的尝试失败之后,我们决定自己写一个表达式引擎。

a30623b8b949cac2b858e2ceabeece53.png

新表达式技术方案

  Parser 生成器 vs Parser 组合子

对于 Parser 来说,他的任务就是将字符串解析成AST,这在编译原理中一般分为词法分析和语法分析,业界有两种主流的方案,一种是以 Yacc 为代表的 Paser 生成器,一种是以 Parsec 为代表的 Parser 组合子。这两种方案各有优缺点,这里我选择了 Parser 组合子,主要有三个原因,第一是我们解析的是比较简单的上下文无关文法,这个用 Parser 组合子实现起来非常简单,代码完全可控;第二利用 Monad 的抽象让我们免于处理树或栈这些数据结构的状态细节;第三 Parser 组合子能够很方便的扩展解析能力。

  提取左因子,消除左递归

上述文法存在两处递归

1: expr := expr andor term | term
2: factor := identifier | num | string | (expr)

第一处为直接左递归,第二处为间接递归,间接递归的问题不需要修改文法,直接左递归可以通过寻找左公共因子解决:

expr := term expr'
expr' := andor expr | nil

然后就可以使用递归下降来解析。

  整体方案

8c6f1932d6e44fe23bc29cc865dfa916.png

方案整体分为4层,最下面的基础是一个 Parser Monad 的抽象,定义了所有 Parser 的行为,接着第二层基于这个抽象定义了三个基本 Parser 来处理解析成功、失败和继续解析下一个字符的情况,然后第三层定义了用于表示 条件循环 等解析结构的组合子,这些都是通用的解析能力。

在通用解析能力上,第四层定义了表达式树的叶子结点和计算节点,然后使用表达式树对Parser Monad 进行类型化,根据文法逐层定义对应的 Parser,最终完成了 expr Parser 对整个表达式的解析。

d3c1f0c6512545fa7caf9d4836da3a5a.png

关键表现

  Parser 解析的函数抽象

Parser 的抽象分为解析的抽象和组合的抽象。

一步解析本质上是一个函数,一个函数是由输入和输出定义的,一般来说 Parser 输入字符串,输出某种类型的树结构,输入是确定的,对于输出来说,综合考虑一共有三种情况:

1、成功解析了一部分,还有剩余的字符串
2、解析完成
3、解析失败

因此可以利用元组和可空类型,将函数定义为:

public typealias ParserF<A> = (String) -> (A, String)?

当返回具体的元组且String非空时为第一种情况,String空为第二种情况,返回nil为第三种情况。

  多个 Parser 的组合

除了函数抽象外,由于大部分Parser只负责解析一部分输入字符串,我们需要一种能够组合两个Parser,并处理表达式树和剩余字符串中间状态的能力,这个能力除了实际操作树结构之外,我们还可以利用Monad实现。

在大多数语言中,Monad体现为一个叫做flatMap的函数,这里抽象完成的Parser定义如下:

// parser定义
public class Parser<A> {
    public let f : ParserF<A>
    init(_ f: @escaping ParserF<A>) {
        self.f = f
    }


    // 对上下文相关的计算进行符合结合律的结合
    public func flatMap<B>(_ f : @escaping ((A) -> Parser<B>)) -> Parser<B> {
        Parser<B> { string in
            self.f(string).flatMap { (a, string2) in
                f(a).f(string2)
            }
        }
    }


    // 执行计算
    public func runParser(_ string : String) -> (A, String)? {
        f(string)
    }
}

这里 flatMap 是一个高阶函数,接受一个函数f,返回一个新的 Parser

理解它的关键在于区分这里涉及到的三个函数,第一步是使用自己的函数,对输入字符串 string 做计算,计算出结果 (a,string2),第二步是将 a 传给 f,获得一个新的 Parser,然后用这个 Parser 的函数对 string2 进行计算。这样做的优势是可以将解析计算进行顺序组合,并将组合和中间状态的维护抽象在这个函数的内部,我们在使用时只关心解析的结果即可,实现了计算本身和计算结果的隔离。

调用 flatMap 只对计算进行了结合,生成了新的计算,计算的执行需要通过 runParser 方法的调用来实现。

  三个基本 Parser

完成了 Parser 的抽象后,我们需要实现三个基本的 Parser,然后以此为基础进行组合:

  1. 节点 Parser,表示解析成功,使用一个值构造一个节点;

  2. 失败 Parser,表示解析失败;

  3. 前进 Parser,表示取下一个字符进行解析

// 基本parser
class Node<V> : Parser<V> {
    init(_ v : V) {
        super.init { string in
            (v, string)
        }
    }
}


class Fail<A>: Parser<A> {
    init() {
        super.init { _ in
            (A, String)?.none
        }
    }
}


class NextChar : Parser<Character> {
    init() {
        super.init { string in
            string.firstIndex { c in
                c != " "
            }.flatMap { index in
                (string[index], String(string.suffix(from: string.index(after: index))))
            }
        }
    }
}

可以看到 Parser 的本质就是函数。

  定义常用的组合子

在解析的过程中,我们需要用到顺序、条件、循环等结构化编程的能力,但这里我们将通过组合的方式来支持这些能力。

校验组合子,使用一个函数生成一个Parser,用于校验一个字符是否符合定义的条件,相当于解析过程中的if:

public func check(p : @escaping ((Character) -> Bool)) -> Parser<Character> {
    NextChar().flatMap { (c) -> Parser<Character> in
        p(c) ? Node<Character>(c) : Fail<Character>()
    }
}

比如判断是否是某个具体的字符 c,可以通过组合生成 isChar Parser :

public func isChar(_ c : Character) -> Parser<Character> {
    check { c2 in
        c == c2
    }
}

分支组合子,将两个 Parser 组合成一个新的 Parser

其行为是若第一个解析成功就使用第一个,否则使用第二个进行解析,相当于解析过程中的 if..else...

public func or<A>(_ p1 : Parser<A>, _ p2 : Parser<A>) -> Parser<A> {
    Parser<A> { string in
        p1.runParser(string) ?? p2.runParser(string)
    }
}

文法中还有一些多分支的情况,比如比较大小需要处理 == | >= | > | < | <= | != 6种分支情况,通过 or 组合子会出现多层嵌套,可以对 or 组合子进行扩展支持多个 Parser 的分支组合:

public func first<A>(_ ps : Parser<A>...) -> Parser<A> {
    Parser<A> { string in
        for p in ps {
            if let r = p.runParser(string) {
                return r
            }
        }
        return nil
    }
}

重复组合子,用于一个 Parser 多次解析的情况
比如表达式中很多地方允许任意的空格,这对应了解析过程中类似 While 的循环结构,当然,在 FP 中我们会更多地使用递归版本,当第一个字符解析成功后,再递归调用自己,如果失败,则返回空数组表示递归结束,最后回收递归栈构造解析结果。

public func many<A>(_ p : Parser<A>) -> Parser<[A]> {
    or(
    p.flatMap { a in
        many(p).flatMap { suffix in
            Node([a] + suffix)
        }
    },
    Node([]) //递归终止条件
    )
}

很多时候我们需要至少出现一次的组合子,比如一个自然数,是[0-9]连续出现至少一次,我们可以通过普通 Parser 和 重复组合子的再组合来实现。

public func many1<A>(_ p : Parser<A>) -> Parser<[A]> {
    p.flatMap { a in
        many(p).flatMap { suffix in
            Node([a] + suffix)
        }
    }
}

分支和重复都有了,顺序组合子怎么实现呢

顺序组合其实就是 flatMap, 它实现了对两个 Parser 的顺序组合。以 (expr) 来说,需要先处理 (,再处理expr,再处理 ),这里可以将括号匹配稍微泛化一下:

public func bracket<O, A, C>(open : Parser<O>, p : Parser<A>, close : Parser<C>) -> Parser<A> {
    open.flatMap { _ in
        p.flatMap { a in
            close.flatMap { _ in
                Node(a)
            }
        }
    }
}

  定义表达式树

完成通用的解析能力定义,在实现上述文法相关的解析之前,需要对文法解析结果建模。一个合法的表达式解析完成后会产生一个表达式树,在Swift中,树结构可以很方便的用递归枚举来定义 :

public enum Expr2 {
    indirect case AND(Expr2, Expr2)
    indirect case OR(Expr2, Expr2)
    indirect case EQ(Expr2, Expr2)
    indirect case NOTEQ(Expr2, Expr2)
    indirect case BIGTHAN(Expr2, Expr2)
    indirect case BIGEQTHAN(Expr2, Expr2)
    indirect case SMALLEQTHAN(Expr2, Expr2)
    indirect case SMALLTHAN(Expr2, Expr2)
    indirect case BRACKET(Expr2)
    case IDENTIFIER(String)
    case NUM(Int)
    case STRING(String)
}

  实现叶子节点的 Parser

叶子结点目前一共有三种 ——identifier\num\string,叶子节点自己就是一个值。有了前面通用的基本Parser和组合子 ,我们就可以来逐个定义它们对应的Parser了,比如identifier := ${string}就可以定义为 :

// 标识符合法值 .46,_95,num48-57,大写65-90,小写97-122
public func isIdentifierChar() -> Parser<Character> {
    check { c in
        let value = c.asciiValue ?? 0
        return value == 46 || value == 95
        || (value >= 48 && value <= 57)
        || (value >= 65 && value <= 90) || (value >= 97 && value <= 122)
    }
}


// 标识符 ${xxx}
public func identifier() -> Parser<Expr2> {
    isChar("$").flatMap { _ in
        isChar("{").flatMap { _ in
            many1(isIdentifierChar()).flatMap { cs in
                isChar("}").flatMap { _ in
                    Node(.IDENTIFIER(String(cs)))
                }
            }
        }
    }
}

作为测试,通过 let treeNode = identifier().runParser("${abc}") 可以成功解析出 Identifier节点。

同理可以定义出 numstr的实现。

可以看到,identifier()的解析过程只涉及解析的结果 cs,而不涉及各种中间的状态和错误的处理,甚至连输入字符串都没出现,这就是 Monad 的优势。

  实现计算节点的 Parser

除了叶子节点之外全部是计算节点,计算节点需要根据自己的两个孩子节点计算出值,因此计算节点的值实际上是一个函数,比如 == Parser :

// 计算节点中值的类型
public typealias MakeExpr = (Expr2, Expr2) -> Expr2


// == 对应的函数
func EQ(_ e1 : Expr2, _ e2 : Expr2) -> Expr2 {
    .EQ(e1, e2)
}


// 解析 == 
public func isEq() -> Parser<MakeExpr> {
    isChar("=").flatMap { _ in
        isChar("=").flatMap { _ in
            Node(EQ)
        }
    }
}

同理可以给出 &&||==>=><=<!= 等计算节点对应的 Parser。这里只是解析了字符串,生成了计算的表达式树,但是并没有计算。

有了这些基本的计算节点之后,我们就可以与firstor组合出 factor\compare\term\expr'\expr 的定义了:

public func compare() -> Parser<MakeExpr> {
    first(isEq(), notEq(), bigeqthan(), bigthan(), smalleqthan(), smallthan())
}


public func factor() -> Parser<Expr2> {
    first(identifier(), num(), string(), bracketExpr())
}


public func term() -> Parser<Expr2> {
    or(factor().flatMap({ factor1 in
        compare().flatMap { makeExpr in
            factor().flatMap { factor2 in
                Node(makeExpr(factor1, factor2))
            }
        }
    }), factor())
}


public func _expr() -> Parser<PartExpr> {
    or(andor().flatMap({ makeExpr in
        expr().flatMap { expr2 in
            Node { expr1 in
                makeExpr(expr1, expr2)
            }
        }
    }),
       Node{ expr in
        expr
    })
}


public func expr() -> Parser<Expr2> {
    term().flatMap { expr in
        _expr().flatMap { partExpr in
            Node(partExpr(expr))
        }
    }
}

  解决 expr\factor\(expr) 的死循环问题

在“实现计算节点的 Parser “节中我们已经完成了所有 Parser 的定义,但是实际运行起来后,发现程序陷入了死循环。

在“提取左因子,消除左递归“节,我们通过提取左因子消除了表达式文法中的左递归,实际上文法中还有一处递归,即 expr 间接调用 factor,  factor 的定义中包含 (expr) ,而(expr)的实现是这样的:

public func bracketExpr2() -> Parser<Expr2> {
    bracket( open: isChar("("), p: expr(), close:isChar(")"))
}

这导致了实际运行过程中的死循环,原因是Swift对函数的参数是严格求值的,在 bracket 调用时就要获得 expr() 的返回值,这里我们通过定义惰性求值版本的 bracket 函数来解决:

public func bracketLazy<O, A, C>(open : Parser<O>, p : @escaping () -> Parser<A>, close : Parser<C>) -> Parser<A> {
    open.flatMap { _ in
        p().flatMap { a in
            close.flatMap { _ in
                Node(a)
            }
        }
    }.setName("()")
}


public func bracketExpr() -> Parser<Expr2> {
    bracketLazy( open: isChar("("), p: { expr() }, close:isChar(")") )
}

最后给 Parser 加一个外部调用的方法,返回解析后的表达式树:

public func parse(_ string : String) -> A? {
    runParser(string).flatMap{$0.0}
}

现在可以调用 parser 函数获取解析成功的表达式树:

let string = "(3<3||5<=5) && ${key.text} == \"456\" && ${type} == 123";
let expr = expr().parse(string)

  求值

拿到表达式树之后,可以结合环境变量进行求值,这一步需要对表达式树定义 eval 方法,具体通过模式匹配根据不同的节点分别给出对应的实现即可:

func eval(_ context : [String : Any]?) -> Any? {
    switch self {
    case .AND(let expr1, let expr2):
        return expr1.evalBool(context) && expr2.evalBool(context)
    case .OR(let expr1, let expr2):
        return expr1.evalBool(context) || expr2.evalBool(context)
    case .IDENTIFIER(let str):
        return MPKVC.mpValue(forKeyPath: str, kvcObject: context)
    ......
}

而前面 NSPredicate 的痛点问题也可以在求值环节解决,对于系统 NSPredicate 不支持嵌套取值的问题,我们可以在 case .IDENTIFIER(let str): 使用 KVC 支持,考虑到Swift是一个静态强类型的语言,这里需要借助OC的能力;对于老版本没有${var}对应数据的问题,也可以在求值阶段予以忽略,保证原有逻辑不变。

至此,就完成了整个解析和求值工作。

cecd7bdbb3a7f3cf4b78737d28f4cee5.png

性能表现

表达式的解析和求值作为一项纯CPU工作,其运行的性能主要看解析和求值时间,使用release包在iPhone7上实测数据(20次平均值)如下:

6758a710747cf3e590fde8a998463533.png

可以得出两个结论:

  1. 解析耗时远大于求值耗时,对重复求值的表达式进行缓存可以大大提升性能表现;

  2. 目前消息单项业务涉及的表达式,大部分都很简单,最复杂的是长按菜单,一共有8项,首次全部解析+求值在低端机耗时约5ms,后续求值耗时< 0.5ms,性能符合要求。

解析过程的性能,目前来说仍然存在少量的回溯情况,这部分可以通过提取左因子来优化。

0e81144bc14410551f1380a51768afaa.png

再谈 Monad

本文针对表达式引擎的需求,利用 Monad 强大的抽象和组合能力,通过定义 Parser 抽象、基本 Parser、基本组合子、表达式树和表达式 Parser,实现了函数式的表达式解析。

MonadFP 中的一个非常重要的抽象,这个概念由 Maclane 于60年代在范畴论中提出,80年代末由 Wadler 等人引入编程领域,随后深刻的影响了 FP 的理念与设计,近些年随着函数式的趋势兴起。在绝大多数场合,这个概念的中文翻译是“单子”,但是和 Convolution 翻译成“卷积”一样很难从名称上理解。

Monad 可以理解为一种容器,里面装了 value 或者 function,通过 flatMapbind 或者 >>= 这样的函数对两个 Monad 进行复合,复合之后的结果仍然是一个 Monad,因此可以一直复合下去,flatMap 是一个高阶函数,接受一个函数,这个函数以第一个 Monad 的结果为参数,返回第二个 Monad,从而实现了上下文相关的计算。

Monad 对计算结构进行了抽象,比如说每个 Parser实际上都对应了一个对应字符内容的集合,flatMap 实现了顺序计算的抽象,or 实现了对条件计算的抽象,many 实现了对迭代计算的抽象,这些计算结构是在匹配数字、关键字、字符串中反复出现的。另外整个过程中 Optional Monad 默默地处理掉了错误。

对于编程实践来说,计算结构的抽象将计算中和结果无关的部分隐藏起来,使得在使用时可以只关注结果,实现关注点的分离,使代码的结构更清晰。实际上,Parser Monad 中状态维护和错误处理还可以进一步抽象出 State MonadOptional Monad,这两个 Monad 的组合也可以进一步通过 Monad Transformer 进行抽象,这个过程完全不需要改变上层代码,有兴趣的读者可以试一试。

FP 的实践过程中,大部分函数都是很短的,单个看是很简单的,但是复合起来能实现很复杂的功能。我觉得纯函数是很好理解的,高阶函数、柯里化、和类型、函数的组合也不是很难,难的是怎么在计算过程都是纯函数的前提下实现了对状态的维护、错误的处理和对IO副作用的处理,这是 FP 在之前遇到的难题,Monad 给出了一种答案。

References:
https://hackage.haskell.org/package/parsec
https://www.cs.nott.ac.uk/~pszgmh/monparsing.pdf
https://rwh.readthedocs.io/en/latest/

7446cae3b6ba6ccf98916463a7c08183.png

团队介绍

我们是来自大淘宝技术全域触达&用户互动客户端团队,负责包含Push、POP弹层和消息沟通三大触达场景。全域触达&用户互动客户端团队追求极致的性能、流畅的交互体验和稳定的触达效率,用智能化的调控策略为用户带来更好的使用体验。

¤ 拓展阅读 ¤

3DXR技术 | 终端技术 | 音视频技术

服务端技术 | 技术质量 | 数据算法

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

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

相关文章

java+mysql基于ssm的网上订餐外卖管理系统

随着人们生活节奏的加快,网上订餐和外卖是当前很多人的一个选择,如何能够让更多的人吃上美味健康的饭菜是本系统研究的一个重要内容,同时本系统还拥有外卖的功能,让食客可以第一时间享受的想要吃到的美食 根据条件需要,系统结构主要由三大用户组成。一是买家组成的买家用户,二…

虹科技术 | 终端入侵防御 | 在重大攻击中发现新的Babuk勒索软件

11月期间&#xff0c;Morphisec在调查一个客户的防范事件时发现了Babuk勒索软件的一个全新变种。Babuk在2021年初首次被发现&#xff0c;当时它开始针对企业进行双重勒索攻击&#xff0c;以窃取和加密数据。这一年晚些时候&#xff0c;一个威胁者在一个讲俄语的黑客论坛上泄露了…

【案例教程】气象数据相关分析:如何使用格点数据分析中国霜冻灾害变化技术

【查看原文】气象数据相关分析及使用系列&#xff1a;如何使用格点数据分析中国霜冻灾害变化技术 霜冻是一种短历时的农业气象灾害&#xff0c;它是由于日最低气温下降&#xff0c;使植物茎、叶处温度下降到0℃以下&#xff0c;导致正在生长的植物受到冻伤的现象。霜冻出现的早…

深度学习——数据增广(笔记)+代码

1.为什么要进行数据增广&#xff1f; ①大型数据集是深度网络的前提条件 ②图像增广对训练数据进行一系列的随机变化&#xff0c;生成相似但不同的训练样本&#xff0c;从而扩大训练集的规模 ③数据增广&#xff1a;可以处理图片和文本和语音。 ④对于图片的处理方式包括&a…

FITC-PEG-DBCO,Fluorescein-PEG-DBCO,荧光素-PEG-DBCO

【产品描述】 DBCO试剂在水性缓冲液中具有快速动力学和稳定性&#xff0c;可用于标记叠氮化物修饰的生物分子&#xff0c;具有高特异性和反应性。FITC具有高吸收率的荧光量子产率和良好的水溶性等特点&#xff0c;是生物学中应用广泛的一种绿色荧光素衍生物&#xff0c;除了用作…

【TypeScript系列】【一篇就够】TypeScript知识点总结(一)

00 TypeScript简介 TypeScript是JavaScript的超集。它对JS进行了扩展&#xff0c;向JS中引入了类型的概念&#xff0c;并添加了许多新的特性。TS代码需要通过编译器编译为JS&#xff0c;然后再交由JS解析器执行。TS完全兼容JS&#xff0c;换言之&#xff0c;任何的JS代码都可以…

神经网络科研绘图可视化工具

本文介绍10种科研绘图可视化工具。 目录1.神经网络框架自带的可视化工具库2.NN-SVG3.PlotNeuralNet4.netron5.ZETANE6.Tensorspace.js7.GRAPHCORE8.nn_vis9.PowerPoint1.神经网络框架自带的可视化工具库 pytorch&#xff1a;pytorchviz库&#xff1b; keras&#xff1a;keras.…

Map和Set

目录 1.搜索 1.1 概念 1.2 模型 2.Map的使用 2.1 Map说明 2.2 Map.Entry说明,v> 2.3 Map的常见方法 2.3.1 V put(K key, V value) 2.3.2 V get(Object key) 2.3.3 V getOrDefault(Object key, V defaultValue) 2.3.4 Set keySet() Collection values() 2.3.5 S…

JavaWeb语法二:Thread类的基本使用

目录 1.创建线程 1.2&#xff1a;run()和start()方法 1.3&#xff1a;Thread的几个常见属性 2.线程的有关操作 2.1&#xff1a;启动一个线程&#xff1a;start() 2.2&#xff1a;中断一个线程 2.2.1&#xff1a;使用自定义的变量来作为标志位 2.2.2&#xff1a;使用Thr…

高校校园网络

开发工具(eclipse/idea/vscode等)&#xff1a; 数据库(sqlite/mysql/sqlserver等)&#xff1a; 功能模块(请用文字描述&#xff0c;至少200字)&#xff1a;开发工具IDEA-2020.1,数据库mysql55.527,SSM框架&#xff0c;jdk版本"1.8.0_74"&#xff0c;java语言。 管理员…

小白年薪26万,为什么Python岗位的薪资越来越高?问题解析

人工智能和大数据概念的兴起&#xff0c;带动了Python的快速增长——Python语言逻辑简洁、入门简单、生态丰富&#xff0c;几乎成为几个新兴领域的不二选择。而除了这两个领域&#xff0c;Python还有更多的适用领域&#xff1a;爬虫、web、自动化运维等领域都非常适合Python发挥…

大二学生HTML期末作业、个人主页网页制作作业

&#x1f389;精彩专栏推荐&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业…

代码随想录第八天

专题&#xff1a;字符串 题目&#xff1a; 编写一个函数&#xff0c;其作用是将输入的字符串反转过来。输入字符串以字符数组 char[] 的形式给出。不要给另外的数组分配额外的空间&#xff0c;你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。你可以假设数组中的所…

CleanMyMacX4.15好用吗?CleanMyMac X2023安全吗?

CleanMyMac x4.15已经正式发布&#xff0c;作为最新的系统清理和应用管理软件&#xff0c;可以让用户一键智能化清理Mac电脑&#xff0c;自动扫描Mac上的所有文件&#xff0c;包括&#xff1a;Mac系统清理、邮件应用程序清理、大和旧型文件清理、itunes文件清理、废纸篓清理、应…

【总结】少信那些个包发论文的营销文案,7场直播讲解如何锻炼基础的学术研究能力...

对于广大研究生甚至本科生来说&#xff0c;发表满足学校或者实验室要求的公开学术论文是毕业要求&#xff0c;多少都会具有一定的压力&#xff0c;尤其是一些学校实验室要求比较高。作为过来人&#xff0c;其中焦虑有三也是懂的&#xff0c;研究生期间为此焦虑过一两年。正应如…

安科瑞ARB5弧光保护装置非电量保护装置故障告警可选配GOOSE

安科瑞 王晶淼/刘芳 一、ARB5系列弧光保护装置概述 ARB5系列弧光保护装置外观时尚、大屏幕液晶显示&#xff0c;图形化多语言菜单&#xff0c;操作快捷方便&#xff1b;具有强大的功能集成&#xff1a;集保护、测量、控制、监测、通讯、故障录波、事件记录等多种功能于一体,准…

时间序列-预测(Forcasting):时间序列预测算法总结

一、背景介绍 绝大部分行业场景,尤其是互联网、量化行业,每天都会产生大量的数据。金融领域股票价格随时间的走势;电商行业每日的销售额;旅游行业随着节假日周期变化的机票酒店价格等; 我们称这种不同时间收到的,描述一个或多种特征随着时间发生变化的数据,为时间序列…

如何完全的卸载ArcGIS?

打开控制面板&#xff0c;找到“卸载程序” 选择卸载程序&#xff0c;在新界面中找到与ArcGIS有关的程序。 右键单击要卸载的内容&#xff0c;并选择“卸载/更改”。按照此方法&#xff0c;把要卸载的软件一个个的卸载掉。 通过以上步骤&#xff0c;我们就把ArcGIS软件从电脑中…

酷早报:12月14日全球Web3加密行业重大资讯大汇总

2022年12月14日 星期三 【数据指标】 加密货币总市值&#xff1a;$0.88万亿 BTC市值占比&#xff1a;39.15% 恐慌贪婪指数&#xff1a;30 恐慌【今日快讯】 1、【政讯】 1.1、拜登&#xff1a;通胀正在下降&#xff0c;一切都在朝着正确的方向发展 1.2.1、贝莱德&#xff1a;11…

ChatGPT:探索RLHF与GPT的完美结合

前言 ChatGPT已经发布一周了热度依旧不减&#xff0c;ChatGPT也各种大显神通&#xff0c;为各大网友“出谋划策”&#xff0c;有写周报的&#xff0c;有写绩效的甚至还有写论文的&#xff0c;作为一个NLP从业者&#xff0c;除了好好体验下其中的乐趣&#xff0c;其背后的原理当…