规则引擎 | 青训营笔记

147 阅读6分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 6 天

一、本堂课重点内容

  • 认识规则引擎
  • 编译原理基本概念
  • 设计一个规则引擎
  • 规则引擎的实现

二、详细知识点介绍

认识规则引擎

规则引擎定义

规则引擎是一种嵌入在应用程序中的组件,实现了将业务决策从应用程序代码中分离出来,并使用预定义的语句模块编写业务决策。其接受数据输入,解释业务规则,并根据业务规则做出业务决策。

在使用规则引擎之前,业务处理的流程如下:

image.png

而使用规则引擎之后如下,开发人员只需要维护规则引擎就可以了

image.png

使用规则引擎有如下优点

  • 解决了开发人员重复编码的问题
  • 其使业务决策与服务本身解耦,提高服务的可维护性
  • 缩短开发路径,提高效率

组成部分

规则引擎由三部分组成

  • 数据输入

    支持接受使用预定义的语义编写的规则作为策略集。比如“price > 500”,接受业务数据作为执行过程中的参数,比如价格、标签等。

  • 规则理解

    能够按照预定义的词法、语法、优先级、运算符等正确理解业务规则所表达的语义。

  • 规则执行

    根据执行时输入的参数对策略集中的规则进行正确的解释和执行。同时对规则执行过程中的数据类型进行检查,确保执行结果的正确。

应用场景

  1. 风控对抗

    与黑灰产的对抗过程中,策略研发和产品需要能够根据黑灰产特征进行快速识别和对抗。规则引擎作为风控系统的核心,使产研人员能够不断的调整和优化对抗策略,以实现最好的风控识别效果。

  2. 活动策略运营

    业务活动的运营需要及时根据用户效果反馈进行运营策略的优化和调整。引入规则引擎后。可以将服务代码与业务运营逻辑解耦,提高运营策略的迭代效率。方便新玩法的探索和效果验证。

  3. 数据分析和清洗

    在数据分析系统中使用规则引擎可以便捷的实现对数据进行整理、清洗和转换。数据分析师可以根据不同的需求来自定义数据处理的规则,方便快捷的产出所需要的数据。

编译原理基本概念

规则引擎通过词法分析、语法分析生成抽象语法树后进行执行,其中整个过程有着一定的输入和输出。

词法分析

词法分析就是把源代码字符串转换为词法单元(Token)的过程。

举个栗子

price > 500 && (isNewUser || userLevel > 5)

经过词法分析后就变成了

price>500&&(isNewUser||userLevel>5)

那么如何识别Token呢?

答案是使用有限状态自动机,它具有有限个状态,在任何一个状态,基于输入的字符都能做一个确定的状态转换。

graph LR
start((start)) --p--> s1((s1))
start --5--> s2((s2))
start --">、&、|"--> s3((s3))
s1--"r、i、c、e"-->s1
s1-->参数((参数))
s2--"0、0"-->s2
s2 --> 数字((数字))
s3--"&、|"-->s3
s3-->符号((符号))

语法分析

语法分析就是在词法分析的基础上,识别表达式的语法结构的过程。

仍然以上例来叙述,在得到Token后,通过语法分析,将会得到类似于下图所示的树形结构

graph TD
1((>)) --> price(price)
1 --> 500(500)

这种结构就叫做抽象语法树,其中每一个节点(子树)都是一个语法单元,这个语法单元的构成规则就叫“语法”。每个节点还可以有下级节点。上例得到的抽象语法树如下

image.png

抽象语法树

上下文无关语法

语言句子无需考虑上下文,就可以判断正确性,其可以用巴科斯范式(BNF)来表示。其描述了语句形成的过程。

exp : add;
add : add '+' mul | mul;			//加法表达式
mul : mul '*' pri | pri;			//乘法表达式
pri : string | bool | number | identifier;	//基础表达式

产生式:一个表达式可以由另外已知类型的表达式或者符号推导产生。

内置符号:字面量(string、bool、number)标识符、运算符

  • 一个基础表达式可以由常量(string、bool、number)或标识符(identifier)
  • 一个乘法表达式可以由基础表达式或者乘法表达式 * 基础表达式组成

递归下降算法

就是自顶向下构造语法树,不断对Token进行语法展开(下降),展开过程中可能会遇到递归的情况。

类型检查

类型综合

根据子表达式的类型构造出父表达式的类型。例如,表达式A+B的类型是根据A和B的类型定义的。

graph TD
1((+)) --> 10((10))
1 --> 20((20))

2((+)) --> str1((''str1''))
2 --> str2((''str2''))

编译时检查 & 运行时检查

类型检查可以发生在表达式的编译阶段,即构造语法树的阶段;也可以发生在执行的阶段。

  • 编译时:需要提前声明参数类型,即在构建语法树过程中进行类型检查

    int1 : int; str1 : string
    
  • 执行时:可以根据执行时的参数输入的值的类型,在执行过程中进行类型检查

    int1 : 108; str1 : "300"
    

设计一个规则引擎

设计目标

设计一个规则引擎,支持特定的词法、运算符、数据类型和优先级。并且支持基于以上预定义语法的规则表达式的编译和执行。

词法(合法Token)

  • 参数

    由字母数字下划线组成。如:_ab2user_name

  • 布尔值

    truefalse

  • 字符串

    "abcd"'abcd'`abcd`

  • 十进制int

    1234

  • 十进制float

    123.5

  • 预定义运算符

    +-

运算符

  • 一元运算符

    +-

  • 二元运算符

    +-*/%><>=<===!=

  • 逻辑操作符

    &&||!

  • 括号

    ()

数据类型

  • 字符串
  • 布尔值
  • 十进制int
  • 十进制float

优先级

优先级运算符
0||
1&&
2!-+
3><>=<===!=
4+-
5*/
6()

词法分析

有限状态自动机如下所示

graph LR
0((0))--字母或下划线-->1((1))
1--"数字、字母或下划线"-->1
1--其他-->关键字{关键字}
关键字--bool关键字-->2((终点2))
2---2n(常量bool值)
关键字--非关键字-->3((终点3))
3---3n(参数标识符)
0--数字或者小数点-->4((4))
4--数字或小数点-->4
4--其他--->5((终点5))
5---5n(常量float或int)

0--单字符运算符---->6((终点6))
6---6n(加减乘除百分号括号)

0--"单双引号、反引号"-->7((7))
7--其他字符-->7
7--"单双引号、反引号"--->8((终点8))
8---8n(常量string)

0--"=、&、|"-->9((9))
9--"=、&、|"--->10((终点10))
10---10n(==和&&和逻辑或)

0--">、<、!"-->11((11))
11--">、<、!"--->12((终点12))
12---12n(>=或<=或!=)

语法分析

expr: logOr EOF;
logOr: logOr '||' logAnd | logAnd;
logAnd: logAnd '&&' logNot | logNot;
logNot: '!' logNot | cmp;
cmp: cmp '>' add | cmp '>=' add | cmp '<' add | cmp '<=' add | cmp '==' add | cmp '!=' add | add;
add: add '+' mul | add '-' mul | mul;
mul: mul '*' pri | mul '/' pri | mul '%' pri | pri;
pri: BooleanLiteral | IntegerLiteral | FloatLiteral | StringLiteral | Identifier | '('expr')';

优先级的表达

type precedence struct {
    validSymbols    []Symbol        // 当前优先级支持的运算符类型
    nextPrecedence  *precedence     // 更高优先级的
    planner         planner         // 当前优先级的处理函数
}

image.png

语法树结构

  • 一元运算符 左子树为空,右子树为右操作数
  • 二元运算符 左子树为左操作数,右子树为右操作数
  • 括号 左子树为空,右子树为内部表达式的AST

image.png

语法树执行与类型检查

语法树执行

  1. 预先定义好每种操作符的执行逻辑
  2. 对抽象语法树进行后续遍历执行,即:
    • 先执行左子树,得到左节点的值;
    • 再执行右子树,得到右节点的值;
    • 最后根据根节点的操作符执行得到根节点的值。

类型检查

  • 检查时机 执行时检查
  • 检查方法 在一个节点的左右子树执行完成后,分别校验左右子节点的类型是否符合对应操作符的类型检查预设规则
    • '>'符号要求左右子节点的值都存在且为 intfloat
    • '!'符号要求左节点为空且右节点的值为bool

三、实践练习例子

规则引擎的实现,根据之前的规则引擎的设计,将该规则引擎实现。

原项目地址点击此处

四、课后个人总结

本次课程学习了规则引擎相关的知识,了解到规则引擎在业务流程中的重要作用,以及实现规则引擎的基本流程,也了解了一些编译原理相关的知识。