java表达式解析引擎_Go 实现的数学表达式解析计算引擎

2023-10-27

前言,一下内容引用作者博客。

math-engine-demo

导读

这篇文章将从头开始,使用 Go 语言来实现一个完整的数学表达式计算引擎。本文采用的是抽象语法树(Abstract Syntax Tree,AST)实现方式。
虽然本文的实现代码为 Go,但不用纠结于此,语言只是实现方式的一种选择,作为开发工程师,相信你读起来它并不会感到费力。你完全可以在读完文章后,用 C/C++,Rust 或者任意你喜欢的语言来实现一遍,甚至,这是检验你学习成果的最佳方式。
文章的所有代码和编译后的二进制可执行文件都会放在 Github, 文章末尾附有链接。
那么,就让我们开始吧。

概述

我们来先做一道题数学题,计算下面的式子:92 + 5 + 5 * 27 - (92 - 12) / 4 + 26

什么?没了??就这么简单???( 是的,就这么简单

凭借个人实力,我觉得大家完全可以口算给出这道题的答案。但是,你并不会这么做,因为你觉得你是一名工程师,你要用你所擅长的代码来解决这个问题,以至于一发不可收,最后解决了这类问题。(我没有、我不想、别瞎说.jpg)

如果不往下看,停顿思考,你的实现思路是什么样的?每个人都会有一个自己的实现思路,事实上,解决这个问题的方法很多,没有必要拘泥于某一种算法。本文为了保证代码实现质量的同时,还能够阐述基础的语言处理结构,这里使用的是抽象语法树(Abstract Syntax Tree,AST,下称:AST)实现方式。使用抽象语法树还有一个目的,就是窥一斑而知全豹,让大家能了解脚本语言(典型的如PHP、Javascript)是如何动态解析执行的。

所以,看完本章,你会了解到:

  • 词法分析,如何手写一个词法分析器
  • 语法分析,如何手写一个语法分析器
  • AST,如何构建一个AST
  • 运算优先级算法的实现
  • 执行一个 AST

题目分析思路

首先,我们先来分析一下这个题目。92 + 5 + 5 * 27 - (92 - 12) / 4 + 26
根据小学学过的理论知识,我们知道:先乘除后加减,有括号最优先。所以在我们口算这道题的时候,我们的计算顺序应该是:X = ( 92 - 12 ) = 80Y = 5 * 27 = 135Z = X / 4 = 80 / 4 = 20
原题 = 92 + 5 + Y - Z + 26 = 92 + 5 + 135 - 20 + 26 = 238

为了能够用代码实现这个算法,我们需要提炼出口算时的计算模式,或者说,计算的规律。有了规律,转换成代码实现就会很容易。

不知道你有没有注意到,我们口算解这道题的时候,并没有说一下子把题目解出来,而是对题目先做了分解,比如 (92-12)是第一步分解出来的,我们可以先计算这个小的子题,得出80这个结果后,再代入到原式中。这个思路正是上面说的”先乘除后加减,有括号最优先”的具体实践,也就是说,运算符是有优先级的,需要根据算式中优先级的高低,先分解优先级高的计算出来,然后计算优先级低的,而不是简单的从题目的起始一直计算到结尾。

很明显,在这道题中,()、的优先级最高,*、/的优先级次之,+、-的优先级最低。同一优先级可以从左到右顺序计算。
确立了优先级,我们就能根据上面的口算步骤,模拟一个算法流程:

1、找出较高优先级的子算式 92-12,得出结果 80,代替92-12的位置;
2、找出较高优先级的子算式 5*7,得出结果 35,代替5*7的位置;
3、找出较高优先级的子算式 80/4,得出结果 20,代替80/4的位置;
4、发现优先级都是一样的,可以顺序计算,依次计算得出结果238

图解如下:

图解

模拟的算法流程有了,但是,emmm… 好像还是不知道该从哪里开始码代码,难道要一遍遍的遍历查找计算吗?不。

那就进一步分析这个流程,可以发现,在每一步的最后一个操作都是做一个简单的运算,然后得出结果,这个简单运算遵循 number opeator number,可以很明确的知道,这是一个二元运算,我们记作(lhs op rhs)。

lhs, left hand side 的缩写,意为左侧。rhs 同理。

所以,回过头,再来规划一下上面的题目和流程,可以总结出:计算一个数学表达式,本质上就是把表达式按照运算优先级分出二元运算、运算合并的过程。
有了这个认知后,我们就可以把这个问题跟我们的数据结构结合在一起,用某种或者多种数据结构来描述这个题目。在计算机中,该如何描述呢?我们可以先改进一下lhs op rhs, 把op提到最前面, 转化成op lhs rhs, 因为它们都是一个个二元运算,而每个二元运算都可以看做是一个Root为op,LeftNode为lhs,RightNode为rhs的二叉树(Root=op LeftNode=lhs RightNode=rhs),那么按照算法流程来构建一个二叉树就可以很好的表示这个题目。

最终,我们可以得出这张图:

二叉树的表示

整个待计算的表达式可以用二叉树来完美的表示。这是一个令人兴奋的事情,因为我们把一道数学问题成功的转化成了一道数据结构的算法问题,也就是说,我们只需要一种算法对这个二叉树求解,就能得出原算式的答案。事实上,这就是解决这道题的核心所在(不仅仅是这道数学运算题,把范围扩展到整个计算机领域都会适用),一旦我们能够通过数据结构和算法描述现实问题,那么我们就可以利用计算机完成问题的映射,并且通过计算得出答案。

解决问题的思路有了,那我们就可以开始准备 Coding,实现我们说的数据结构。

词法分析

词法分析:将字符序列转换为单词(Token)序列的过程。

为了能让计算机理解我们输入的字符串,我们需要对这个字符串做解析,那第一个阶段就是词法分析。
不要惊慌!即使你从来没接触过这类知识,这个小节也并不会出现让你感到晦涩难懂的地方。相信我!(其实,看完你会觉得很简单)

92 + 5 + 5 * 27 - (92 - 12) / 4 + 26 这个算式,对我们而言,有意义的字符/词是什么?我们来逐个拆解一下:

  • 92 有意义,因为它是数字,参与运算;
  • + 有意义,因为它是操作符,参与运算;
  • 5, +, 5, *, 27, - , 92, -, 12, /, 4, +, 26都有意义,原因同上;
  • ( 有意义,它表示一个优先级开始;
  • ) 有意思,它表示一个优先级结束;

当我们把所有有意义的词拆解出来,那剩下的自然就是无意义的了。在这个式子中就是空格。

上面的这个拆解过程,我们就称之为词法分析。通俗的来讲,词法分析是将输入的字符串逐个分析,通过某种方式(一般是有限状态机)来识别有意义的字符,转化成相应的标记Token,无意义的字符直接忽略丢弃,最终得到一个全部是有意义的Token序列的过程。
上面我们提到了两种类型,一个是数字,一个是操作符(()我们也当做是操作符),在代码中可以定义成常量,供接下来使用:

const (  // 字面量,e.g. 50    Literal = iota  // 操作符, e.g. + - * /    Operator)

定义好类型,我们接下来可以定义 Token:

type Token struct {  // 原始字符    Tok  string  // 类型,有 Literal、Operator 两种    Type int}

只要能够通过词法分析得到的 Token,都是有意义的,对于本文来讲,Token只有Literal, Operator两种类型,所以,接下来的扫描过程只需要处理这两种类型就行了。
词法分析的具体实现代码:

// 定义一个结构体,描述一个词法分析器type Parser struct {  // 输入的字符串    Source string  // 扫描器当前所在的字符    ch     byte  // 扫描器当前所在的位置    offset int  // 扫描过程出现的错误收集    err error}// 逐个字符扫描,得到一串 Token 序列func (p *Parser) parse() []*Token {    toks := make([]*Token, 0)  // 一直获取下一个 Token    for {        tok := p.nextTok()        if tok == nil {      // 已经到达末尾或者出现错误时,停止            break        }    // 收集 Token        toks = append(toks, tok)    }    return toks}// 获取下一个 Tokenfunc (p *Parser) nextTok() *Token {  // 已经到达末尾或者出现错误    if p.offset >= len(p.Source) || p.err != nil {        return nil    }    var err error  // 跳过所有无意义的空白符    for p.isWhitespace(p.ch) && err == nil {        err = p.nextCh()    }    start := p.offset    var tok *Token    switch p.ch {  // 操作符    case        '(',        ')',        '+',        '-',        '*',        '/',        '^',        '%':        tok = &Token{            Tok:  string(p.ch),            Type: Operator,        }        tok.Offset = start    // 前进到下一个字符        err = p.nextCh()  // 字面量(数字)    case        '0',        '1',        '2',        '3',        '4',        '5',        '6',        '7',        '8',        '9':        for p.isDigitNum(p.ch) && p.nextCh() == nil {        }        tok = &Token{            Tok:  strings.ReplaceAll(p.Source[start:p.offset], "_", ""),            Type: Literal,        }        tok.Offset = start  // 捕获错误    default:        if p.ch != ' ' {            s := fmt.Sprintf("symbol error: unkown '%v', pos [%v:]%s",                string(p.ch),                start,                ErrPos(p.Source, start))            p.err = errors.New(s)        }    }    return tok}// 前进到下一个字符func (p *Parser) nextCh() error {    p.offset++    if p.offset < len(p.Source) {        p.ch = p.Source[p.offset]        return nil    }  // 到达字符串末尾    return errors.New("EOF")}// 空白符func (p *Parser) isWhitespace(c byte) bool {    return c == ' ' ||        c == '' ||        c == '' ||        c == 'v' ||        c == 'f' ||        c == ''}// 数字func (p *Parser) isDigitNum(c byte) bool {    return '0' <= c && c <= '9' || c == '.' || c == '_' || c == 'e'}// 对错误包装,进行可视化展示func ErrPos(s string, pos int) string {    r := strings.Repeat("-", len(s)) + ""    s += ""    for i := 0; i < pos; i++ {        s += " "    }    s += "^"    return r + s + r}

为了方便调用,我们封装一下这个Parser结构体,通过一个函数来把它变成一个黑箱,简化流程:

// 封装词法分析过程,直接调用该函数即可解析字符串为[]Tokenfunc Parse(s string) ([]*Token, error) {  // 初始化 Parser    p := &Parser{        Source: s,        err:    nil,        ch:     s[0],    }  // 调用 parse 方法    toks := p.parse()    if p.err != nil {        return nil, p.err    }    return toks, nil}

现在我们只需要调用 Parse函数,就能够对任意字符串进行分析,得到一串 Token序列。

至此,我们所有的词法分析的代码全部就完成了。这短短不到100行代码,我们就得到了一个可以工作的很好的词法分析器,它能够处理目前我们需要计算数学表达式的输入字符串。(放大一下视角,在编译器领域,对于源代码的第一阶段的词法分析,和我们现在写的代码并没什么两样,只不过是它们的类型更多,逻辑更复杂而已。这种通过有限状态机的方式来写,虽然会比较繁琐、代码量多,但是贵在逻辑很清楚,大多数语言会用这种算法来实现)

有了 Token 序列是没有什么卵用的,我们的目的是求解。接下来我们要用它来进行下一阶段:语法分析和构建一个 AST。

语法分析、构建 AST

语法分析:在词法分析的基础上将单词Token序列组合成各类语法短语, 判断源代码在结构上是否正确。

拿到一串 Token 序列,把它当做一个输入,由一个函数去执行,然后得到一个检查结果,这个就是语法分析阶段要做的事情,这个函数就是语法分析器。语法分析器得出的结果是对我们输入的整个字符串在语法上的检查结果,它能够告诉我们,这个输入的字符串内容是否能够被识别格式化为下一个阶段目标(这里的目标是构建一个 AST ),举个例子:

1+2+3+4+5 >>>>词法分析>>>>语法分析>>>>无错误>>>>AST

1+2+(3-4 >>>>词法分析>>>>语法分析>>>>出错,4之后应该要有一个操作符,可能是)或者+//*等等

可以看出,语法分析阶段是我们能够确定是否可以继续进行求解很重要的一步。有了它,原始输入的算式字符串才有转成计算机所能理解的数据结构的可能。一般来讲,语法分析不仅仅孤立存在于本身的语法语义检查,他还会产生一个数据结构,这个数据结构通常是一个语法树。要注意的是,并不是只有完全的语法分析通过才会产生这个数据结构,部分语法通过也可以有这个数据结构,具体要看你的语法分析器的宽容度,也就是纠错能力。说的比较抽象,我们来看个例子:

输入 1+2+3 0 5 06 9*4,词法分析通过,然后到语法分析,很明显整个算式是无法通过语法检查的,但是语法分析器可以只解析到正确的部分 1+2+3,然后返回一个1+2+3的语法树结构,这样即使语法没通过,但是也能得到一个语法树结构。

就目前的问题而言,我们可以在语法分析阶段生成一个 AST, 用这个 AST 描述数学表达式,就像做思路分析里的这张图一样:

二叉树的表示

根据这张图,先定义用到的两个 AST 节点:

// 基础表达式节点接口type ExprAST interface {    toStr() string}// 数字表达式节点type NumberExprAST struct {    // 具体的值    Val float64}// 操作表达式节点type BinaryExprAST struct {    // 操作符    Op string    // 左右节点,可能是 数字表达式节点/操作表达式节点/nil    Lhs,    Rhs ExprAST}// 实现接口func (n NumberExprAST) toStr() string {    return fmt.Sprintf(        "NumberExprAST:%s",        strconv.FormatFloat(n.Val, 'f', 0, 64),    )}// 实现接口func (b BinaryExprAST) toStr() string {    return fmt.Sprintf(        "BinaryExprAST: (%s %s %s)",        b.Op,        b.Lhs.toStr(),        b.Rhs.toStr(),    )}

解析数学表达式这类问题,只需要 NumberExprAST 和 BinaryExprAST 这两个节点就可以了。原因词法分析阶段已经说过的,只有 Literal、Operator 两种类型。

定义基础的 AST 结构:

// AST 生成器结构体type AST struct {    // 词法分析的结果    Tokens []*Token    // 源字符串    source    string    // 当前分析器分析的 Token    currTok   *Token    // 当前分析器的位置    currIndex int    // 错误收集    Err error}

下面开始实现语法分析和构建 AST 的核心代码:

// 定义操作符优先级,value 越高,优先级越高var precedence = map[string]int{"+": 20, "-": 20, "*": 40, "/": 40, "%": 40, "^": 60}// 语法分析器入口func (a *AST) ParseExpression() ExprAST {    lhs := a.parsePrimary()    return a.parseBinOpRHS(0, lhs)}// 获取下一个 Tokenfunc (a *AST) getNextToken() *Token {    a.currIndex++    if a.currIndex < len(a.Tokens) {        a.currTok = a.Tokens[a.currIndex]        return a.currTok    }    return nil}// 获取操作优先级func (a *AST) getTokPrecedence() int {    if p, ok := precedence[a.currTok.Tok]; ok {        return p    }    return -1}// 解析数字,并生成一个 NumberExprAST 节点func (a *AST) parseNumber() NumberExprAST {    f64, err := strconv.ParseFloat(a.currTok.Tok, 64)    if err != nil {        a.Err = errors.New(            fmt.Sprintf("%vwant '(' or '0-9' but get '%s'%s",                err.Error(),                a.currTok.Tok,                ErrPos(a.source, a.currTok.Offset)))        return NumberExprAST{}    }    n := NumberExprAST{        Val: f64,    }    a.getNextToken()    return n}// 获取一个节点,返回 ExprAST// 这里会处理所有可能出现的类型,并对相应类型做解析func (a *AST) parsePrimary() ExprAST {    switch a.currTok.Type {    case Literal:        return a.parseNumber()    case Operator:        // 对 () 语法处理        if a.currTok.Tok == "(" {            a.getNextToken()            e := a.ParseExpression()            if e == nil {                return nil            }            if a.currTok.Tok != ")" {                a.Err = errors.New(                    fmt.Sprintf("want ')' but get %s%s",                        a.currTok.Tok,                        ErrPos(a.source, a.currTok.Offset)))                return nil            }            a.getNextToken()            return e        } else {            return a.parseNumber()        }    default:        return nil    }}// 循环获取操作符的优先级,将高优先级的递归成较深的节点// 这是生成正确的 AST 结构最重要的一个算法,一定要仔细阅读、理解func (a *AST) parseBinOpRHS(execPrec int, lhs ExprAST) ExprAST {    for {        tokPrec := a.getTokPrecedence()        if tokPrec < execPrec {            return lhs        }        binOp := a.currTok.Tok        if a.getNextToken() == nil {            return lhs        }        rhs := a.parsePrimary()        if rhs == nil {            return nil        }        nextPrec := a.getTokPrecedence()        if tokPrec < nextPrec {            // 递归,将当前优先级+1            rhs = a.parseBinOpRHS(tokPrec+1, rhs)            if rhs == nil {                return nil            }        }        lhs = BinaryExprAST{            Op:  binOp,            Lhs: lhs,            Rhs: rhs,        }    }}

parseBinOpRHS是一个非常重要的算法,他根据定义的操作符优先级进行高优先级递归,从而使高优先级的节点具有更深的 AST 节点,这样,我们遍历 AST 的时候就会天然的进行高优先级的计算。(这是一个成熟的算法,更多的细节可以阅读这篇 Wiki Operator-precedence_parser)。

同样,为了便于调用,我们封装一下生成 AST 的方法:

// 生成一个 AST 结构指针func NewAST(toks []*Token, s string) *AST {    a := &AST{        Tokens: toks,        source: s,    }    if a.Tokens == nil || len(a.Tokens) == 0 {        a.Err = errors.New("empty token")    } else {        a.currIndex = 0        a.currTok = a.Tokens[0]    }    return a}

OK!所有的语法分析和 AST 构建的代码都已经完成了。你现在可以通过下面的代码来尝试查看生成的 AST 结构:

func main() {    exp := "92 + 5 + 5 * 27 - (92 - 12) / 4 + 26"    // input text -> []token    toks, err := Parse(exp)    if err != nil {        fmt.Println("ERROR: " + err.Error())        return    }    // []token -> AST Tree    ast := NewAST(toks, exp)    if ast.Err != nil {        fmt.Println("ERROR: " + ast.Err.Error())        return    }    // AST builder    ar := ast.ParseExpression()    if ast.Err != nil {        fmt.Println("ERROR: " + ast.Err.Error())        return    }    fmt.Printf("ExprAST: %+v", ar)}

运行这段代码,应该可以看到如下输出:

ExprAST: {Op:+ Lhs:{Op:- Lhs:{Op:+ Lhs:{Op:+ Lhs:{Val:92} Rhs:{Val:5}} Rhs:{Op:* Lhs:{Val:5} Rhs:{Val:27}}} Rhs:{Op:/ Lhs:{Op:- Lhs:{Val:92} Rhs:{Val:12}} Rhs:{Val:4}}} Rhs:{Val:26}}

成功的将一段源字符串转化成了一个 ExprAST 结构!

AST 求解

现在离我们成功只差一步之遥,我们只需要对这个 ExprAST 进行某种算法运算,就可以得出答案。事实上,我们得到的这个 ExprAST 是一个二叉树结构,根据经验,写一个后序遍历算法(LRD)进行求解即可。

代码如下:

// 一个典型的后序遍历求解算法func ExprASTResult(expr ExprAST) float64 {    // 左右值    var l, r float64    switch expr.(type) {    // 传入的根节点是 BinaryExprAST    case BinaryExprAST:        ast := expr.(BinaryExprAST)        // 递归左节点        l = ExprASTResult(ast.Lhs)        // 递归右节点        r = ExprASTResult(ast.Rhs)        // 现在 l,r 都有具体的值了,可以根据运算符运算        switch ast.Op {        case "+":            return l + r        case "-":            return l - r        case "*":            return l * r        case "/":            if r == 0 {                panic(errors.New(                    fmt.Sprintf("violation of arithmetic specification: a division by zero in ExprASTResult: [%g/%g]",                        l,                        r)))            }            return l / r        case "%":            return float64(int(l) % int(r))        default:        }    // 传入的根节点是 NumberExprAST,无需做任何事情,直接返回 Val 值    case NumberExprAST:        return expr.(NumberExprAST).Val    return 0.0}

改造一下之前的 main()方法:

func main() {    exp := "92 + 5 + 5 * 27 - (92 - 12) / 4 + 26"    // input text -> []token    toks, err := Parse(exp)    if err != nil {        fmt.Println("ERROR: " + err.Error())        return    }    // []token -> AST Tree    ast := NewAST(toks, exp)    if ast.Err != nil {        fmt.Println("ERROR: " + ast.Err.Error())        return    }    // AST builder    ar := ast.ParseExpression()    if ast.Err != nil {        fmt.Println("ERROR: " + ast.Err.Error())        return    }    fmt.Printf("ExprAST: %+v", ar)    // 加入下面的代码    // AST traversal -> result    r := ExprASTResult(ar)    fmt.Println("progressing ...", r)    fmt.Printf("%s = %v", exp, r)}

再次运行,可以看到如下输出:

ExprAST: {Op:+ Lhs:{Op:- Lhs:{Op:+ Lhs:{Op:+ Lhs:{Val:92} Rhs:{Val:5}} Rhs:{Op:* Lhs:{Val:5} Rhs:{Val:27}}} Rhs:{Op:/ Lhs:{Op:- Lhs:{Val:92} Rhs:{Val:12}} Rhs:{Val:4}}} Rhs:{Val:26}}progressing ...  23892 + 5 + 5 * 27 - (92 - 12) / 4 + 26 = 238

看到了吗,92 + 5 + 5 * 27 - (92 - 12) / 4 + 26 = 238,我们成功的计算出来了这个算式的结果,是不是跟你一开始口算的结果是一样的呢?!(不要怀疑,这个运算的结果是百分百正确的 )

所有的代码到这里就全部写完了,你现在可以尝试对这个算法输入任意的数学表达式算式,他都能帮你算出结果!很快!!!

结尾

我们回顾一下刚刚实现的引擎:它读取一段表达式源字符串,经过词法分析、语法分析、生成 AST ,最后遍历执行 AST 得到结果。这些步骤,会不会让你觉得它跟脚本语言的解释引擎很像?

是的,典型的如 PHP,如果你有看过Zend引擎的底层实现,你应该就不会对此感到陌生。我们写的这个引擎当然没办法跟 Zend 这种解释引擎去比较,但是完全可以窥一斑而知全豹,通过短短一两百行的代码来模拟实现一个小型的引擎,理解底层系统是如何对输入的源代码进行解析执行的,这对你加深底层编译器的了解有着非常重要的帮助。所以,本文并不是仅仅作为一篇代码讲解或者算法实现的教科文,而是给你一种思路和方式去了解离我们业务较远但是却非常重要的底层系统原理。如果有兴趣,你甚至可以借助 LLVM 来实现一个你自己的编程语言(老实讲,只要你愿意,这并不是一件很难的事情)。

本文并没有提及一些专业的编译器术语,这是故意而为之。一来是可能看官们并没有这方面的知识储备,会让大家觉得晦涩难懂,二来是我本身对这些概念的理解也不是特别的深刻,怕提及解释不到位误导各位。如果你有兴趣,可以在下面的 Github issue 区跟我探讨交流,期待你的高见!

https://github.com/dengsgo/math-engine

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

java表达式解析引擎_Go 实现的数学表达式解析计算引擎 的相关文章

  • 在一定范围内的 Hash 爆破尝试

    爆破 SHA1 import hashlib cipher 6E32D0943418C2C33385BC35A1470250DD8923A9 lower for i in range 49 58 for j in range 48 58 f
  • CSS中使用flex弹性布局实现上下左右垂直居中排列并设置子元素之间的间距

    场景 Flex是Flexible Box的缩写 意为 弹性布局 怎样使用弹性布局实现页面上下两个元素上下左右垂直居中排列 实现如下类似布局 最外层是是一个div div里面是上面一个照片 下面一个表单 这两个元素居中排列 注 博客 霸道流氓
  • Unity3D镜头眩光组件Lens Flare

    在自然界中 由于亮度差异过大的时候 容易产生镜头眩光 为了增加场景的真实性 某些情况下我们也需要用到该组件 右键点击创建一个新的flare 这是个结构比较简单的组件 只需要拖入眩光的图片即可 然后在产生眩光的灯光上添加lens Flare组
  • 刀剑乱舞网页服务器闪退,刀剑乱舞无法打开怎么办 刀剑乱舞闪退解决方法

    刀剑乱舞无法打开怎么办 刀剑乱舞闪退解决方法 有些网友在玩游戏时 刀剑乱舞游戏无法打开 这是怎么回事呢 下面我们就一起来看看刀剑乱舞闪退解决方法吧 刀剑乱舞手游卡了怎么办 卡机闪退解决方法有哪些 刀剑乱舞手游今天开服 不少玩家遇到了卡机和闪
  • shell通过fio工具进行磁盘性能测试

    一 环境准备 已安装fio工具可忽略此步骤 准备一台centos服务器 要求能够访问互联网 安装fio工具 yum y install fio fio version 二 脚本内容 直接复制脚本到自己的主机中 我这里命名为fio sh bi
  • css玻璃雨滴效果,CSS实现雨滴动画效果的实例代码

    玻璃窗 今天我们要实现的是雨滴效果 不过实现雨滴前 我们先把毛玻璃的效果弄出来 没有玻璃窗 雨都进屋了 还有啥好敲打的 window position absolute width 100vw height 100vh background
  • 【C++】右值引用(极详细版)

    在讲右值引用之前 我们要了解什么是右值 那提到右值 就会想到左值 那左值又是什么呢 我们接下来一起学习 目录 1 左值引用和右值引用 1 左值和右值的概念 2 左值引用和右值引用的概念 2 左值引用和右值引用引出 3 右值引用的价值 1 补
  • Javascript 正则表达式使用手册 .

    一 正则表达式匹配常用语法 字符 规定表达式字符出现一次或多次 字符 规定表达式字符出现零次或多次 字符 规定表达式字符出现零次或一次 匹配的是字符的开头 匹配的是一行的开头 匹配的是字符的结尾 匹配的是一行的结尾 b 匹配的是一个词语的边
  • 通过FXmlFile构建xml时,注意xml规范

    直接说问题 构建xml时 用 FXmlFile dependencyXMLFile new FXmlFile dependencyXML EConstructMethod ConstructFromBuffer 失败 原因时 构建depen
  • Web功能实现(1.展示全部2.模糊查询3.修改用户4.删除用户)

    需求 1 展示全部 2 模糊查询 3 修改用户 4 删除用户 首先写数据库脚本 CREATE DATABASE 32312 IF NOT EXISTS test 40100 DEFAULT CHARACTER SET utf8 USE te
  • Wifi简介

    一 WIFI的基本架构 1 wifi用户空间的程序和库 external wpa supplicant 生成库libwpaclient so和守护进程wpa supplicant 2 hardware libhardware legary
  • 关于Mybatis逆向工程的一些查询操作

    查询所有数据不带参数的可以使用 selectByExampleWithBLOBs example 查询的数据需要按字段的排序的 example setOrderByClause 字段名 ASC 升序排列 desc为降序排列 去除重复的数据
  • GEE学习笔记 五十四:QGIS展示3D的高程数据

    写了一个多月的GEE中文教程文档 想到GEE头就疼 今天就写一篇不是GEE的文章 QGIS作为一个开源的非常好用的本地GIS工具 这里不在赘述 这里说它的一个比较有意思的内容 通过DEM数据展示3D地形 下载DEM 高程数据 比如从官网下载
  • 在Ubuntu 14.04 64bit上安装思维导图工具XMind

    这是一款对个人免费的工具 提供了一些基本功能 如果你需要更多功能 可以付费购买Pro版本 从下面的官网地址下载64bit的deb包 http www xmind net download linux 下面完成后 Ubuntu软件管理中心会自
  • Vue项目引入引入ElementUI

    目录 一 安装ElementUI 二 完整引入elementUI 1 在main js中引入elementUI 2 测试 三 按需引入elementUI 1 安装babel plugin component 2 修改 babelrc 文件
  • 权力的游戏第一季/全集Game of Thrones迅雷下载

    权力的游戏 是一部中世纪史诗奇幻题材的电视连续剧 该剧以美国作家乔治 R R 马丁的奇幻巨作 冰与火之歌 七部曲为基础改编创作 故事背景中虚构的世界 分为两片大陆 位于西面的 日落国度 维斯特洛 位于东面的类似亚欧大陆 维斯特洛大陆边境处发
  • mybatis-plus分页

    ApiOperation value 条件过滤分页查询列表 PostMapping list conditions public ResponseDTO
  • 配置wifi热点_WiFi就像“空气”要覆盖在生活的每个角落

    以高速发展的现代社会来说 热点其实含盖了两种意思 一个是被称作WiFi热点 另一个被称作新闻热点 那么今天所围绕的主题就是WiFi热点 WiFi热点就是将手机接收到的GPRS 3G或4G信号转化为wifi信号发出去的技术 手机必须有无线AP
  • springboot整合mybatis:查询语句,返回null

    springboot整合mybatis时 查询数据库数据时 返回结果为null 刚开始以为是数据库没连接上 结果增 改 删的其他语句则执行成功 但唯有查询语句始终返回null 一条数据一个null 该情况一般情况下是 mapper xml文

随机推荐

  • 深入理解Android之Gradle

    深入理解Android之Gradle 格式更加精美的PDF版请到 https pan baidu com s 1GfN6F8sOaKFAdz5y1bn3VQ下载 weibo分享失效 请各位到百度云盘下载 Gradle是当前非常 劲爆 得构建
  • GitHub上SSH keys和Deploy keys的区别

    平时安装一个git然后去GitHub进行SSH keys 配置最后就开始使用 然后换一台电脑再使用 ssh keygen t rsa C your email 生成一个ssh key 将其添加到自己到github中去 然而发现添加后这台电脑
  • conda安装PaddlePaddle

    最近在学深度学习 但是我打开c盘看见多了 keras之流的东西 又要安飞浆时突然想到conda的默认安装路径 Anaconda Prompt里执行 conda info env 查看已经安装的环境以及位置 进入百度飞浆官网 找到安装教程 W
  • 华为OD机试 - 最佳植树距离(Java & JS & Python)

    题目描述 按照环保公司要求 小明需要在沙化严重的地区进行植树防沙工作 初步目标是种植一条直线的树带 由于有些区域目前不适合种植树木 所以只能在一些可以种植的点来种植树木 在树苗有限的情况下 要达到最佳效果 就要尽量散开种植 不同树苗之间的最
  • 带你了解软件版本号的命名规则

    1 常见软件的版本号命名 软件 升级过程 说明 Linux Kernel 0 0 1 1 0 0 2 6 32 3 0 18 若用 X Y Z 表示 则偶数 Y 表示稳定版本 奇数 Y 表示开发版本 Windows Windows 98 W
  • 是创新还是天真?BlockCity推出BC众创引争议

    三个简陋的主页面 两种推广返佣奖励模式 七个用户身份等级设置 只能围绕BlockCity进行推广 就这样一个用于营销传播的返佣平台 或者说加强版的自营淘宝客平台 居然被自吹自擂地冠以 创业元宇宙 的名义 这就是BlockCity 区块城市
  • MicroBlaze系列教程(9):xilisf串行Flash驱动库的使用

    文章目录 1 xilisf库简介 2 xilisf库函数 3 xilisf配置 4 xilisf应用示例 工程下载 本文是Xilinx MicroBlaze系列教程的第9篇文章 1 xilisf库简介 xilisf库 Xilinx In s
  • 32位下printf的坑(输出错误的值)

    记一次使用printf的坑 printf输出错误 32位编译 include
  • 360校招编程题:内存管理

    题目描述 物联网技术的蓬勃发展 各种传感器纷纷出现 小B所在的项目组正在开放一个物联网项目 她们在研究设计一种新的传感器 这种传感器有自己的基本处理单元 具有一定的自主性 能够进行简单的数据收集 处理 存储和传输 为降低系统功耗并保证系统可
  • 【网络编程】网络基础知识

    前言 小亭子正在努力的学习编程 接下来将开启javaEE的学习 分享的文章都是学习的笔记和感悟 如有不妥之处希望大佬们批评指正 同时如果本文对你有帮助的话 烦请点赞关注支持一波 感激不尽 目录 网络编程 什么是网络编程 网络通信基本模式 网
  • 关于利用JavaScript中的sort方法实现自定义排序

    众所周知 javascript中的sort方法可以实现排序 但是如果只是使用默认的方法 很难拿到理想的结果 默认用法 const arr 1 3 2 12 5 9 1 arr sort console log 排列的信息 arr 1 1 1
  • Keras中文官方文档(离线版)

    点此查看
  • chatgpt赋能python:Python交互编程入门指南

    Python交互编程入门指南 Python是一种高级编程语言 适合初学者和专业人士使用 Python的互动式编程方式为开发人员提供了快速反馈的环境 从而实现更便捷和高效的开发过程 在本文中 我们将介绍Python的交互编程 为您提供Pyth
  • 计算机磁盘是如何实现存储的?

    存储原理简述 硬盘是在硬质盘片 一般是铝合金 以前 IBM 也尝试过使用玻璃 上涂敷薄薄的一层铁磁性材料 硬盘储存数据的原理和盒式磁带类似 只不过盒式磁带上存储是模拟格式的音乐 而硬盘上存储的是数字格式的数据 写入时 磁头线圈上加电 在周围
  • 极智开发

    1 logo修改 BasicLayout jsx import logo from assets example jpg 替换成自己的logo即可 1 2 文字标题 defaultSettings js const proSettings
  • 同行评审的五个方法都是怎样实现的有什么区别

    审查 小组评审 走查 同级桌查 临时评审 审查 非作者等专家在内的针对特定对象进行检查以发现缺陷的过程 最正式 小组评审 一种 轻型审查 可采用审查的指导方针和流程 走查 是产品的作者向一组同事说明该产品 希望获得他们的意见以满足自己的需要
  • 在Java中操作串口实现短信收发 收藏

    1 配置comm jar Comm jar是Sub实现底层串口操作的API 调用了本地的DLL文件 因为Java本身不具备直接访问硬件设置的能力 都是通过调用本地方法来实现的 可以Java的官方网站下载 下载之后把其中Comm jar包导入
  • CPU 矩阵的 LU分解 without pivot

    hello lu without pivot cpu simple cpp 此文件包含 main 函数 程序执行将在此处开始并结束 include
  • 备选列表排列算法的 Python 实现

    备选列表排列算法的 Python 实现 备选列表排列是一种常见的问题 它涉及对给定列表中的元素进行排列 以生成所有可能的组合 在这个问题中 我们将探讨一个用 Python 实现备选列表排列的算法 算法的思路是通过递归方式生成所有可能的排列
  • java表达式解析引擎_Go 实现的数学表达式解析计算引擎

    前言 一下内容引用作者博客 math engine demo 导读 这篇文章将从头开始 使用 Go 语言来实现一个完整的数学表达式计算引擎 本文采用的是抽象语法树 Abstract Syntax Tree AST 实现方式 虽然本文的实现代