booleandev

booleandev
booleandev从零到一打造一个布尔表达式解析引擎为什么需要 booleandev在复杂业务系统中我们经常遇到需要动态组合条件进行筛选的场景。比如电商平台的商品筛选器价格100 AND (品牌Apple OR 品牌Samsung)、权限系统的策略匹配、或数据清洗时的规则引擎。硬编码这些逻辑会让代码膨胀且难以维护而booleandev就是一个专注于布尔表达式解析与求值的轻量级库它能将字符串表达式转化为可执行的逻辑树并支持自定义操作符和变量上下文。在本文中我将从实战角度出发手把手带你构建一个简化版的booleandev核心并演示如何集成到真实项目中。### 核心架构Token 化 - 语法树 - 求值一个标准的布尔表达式引擎分为三个步骤1.词法分析Tokenize将字符串拆解为有意义的符号如操作数、操作符、括号。2.语法分析Parse根据运算符优先级构建抽象语法树AST。3.求值Evaluate遍历 AST结合上下文变量计算最终布尔结果。下面我们用 Python 实现一个最小可用的booleandev原型包含AND、OR、NOT、比较运算,,以及括号。#### 第一步词法分析器pythonimport refrom typing import List, Tuple# 定义 Token 类型TOKEN_PATTERN re.compile(r (?PSPACE\s) |(?POPAND|OR|NOT) |(?PCOMPARE|||!||) |(?PLPAREN\() |(?PRPAREN\)) |(?PSTRING[^]*|[^]*) |(?PNUMBER\d\.?\d*) |(?PIDENT[a-zA-Z_][a-zA-Z0-9_]*), re.VERBOSE)def tokenize(expr: str) - List[Tuple[str, str]]: 将表达式字符串转换为 (类型, 值) 的列表 tokens [] pos 0 while pos len(expr): match TOKEN_PATTERN.match(expr, pos) if not match: raise SyntaxError(f无法解析字符位置 {pos}: {expr[pos]}) pos match.end() kind match.lastgroup value match.group() if kind SPACE: continue elif kind STRING: # 去掉引号保留原始字符串值 tokens.append((STRING, value[1:-1])) elif kind NUMBER: tokens.append((NUMBER, float(value) if . in value else int(value))) else: tokens.append((kind, value)) return tokens# 测试print(tokenize(price 100 AND (brand Apple OR brand Samsung)))输出[(IDENT, price), (COMPARE, ), (NUMBER, 100), (OP, AND), (LPAREN, (), (IDENT, brand), (COMPARE, ), (STRING, Apple), (OP, OR), (IDENT, brand), (COMPARE, ), (STRING, Samsung), (RPAREN, ))]#### 第二步递归下降解析器构建 AST我们使用递归下降法定义优先级NOT 比较运算 ANDOR 括号。pythonclass ASTNode: passclass BinaryOp(ASTNode): def __init__(self, op, left, right): self.op op self.left left self.right rightclass UnaryOp(ASTNode): def __init__(self, op, operand): self.op op self.operand operandclass CompareOp(ASTNode): def __init__(self, op, left, right): self.op op self.left left self.right rightclass Identifier(ASTNode): def __init__(self, name): self.name nameclass Constant(ASTNode): def __init__(self, value): self.value valueclass Parser: def __init__(self, tokens): self.tokens tokens self.pos 0 def peek(self): return self.tokens[self.pos] if self.pos len(self.tokens) else None def consume(self, kindNone): token self.peek() if not token: raise SyntaxError(表达式意外结束) if kind and token[0] ! kind: raise SyntaxError(f期望 {kind}得到 {token}) self.pos 1 return token def parse(self): ast self.parse_or() if self.peek() is not None: raise SyntaxError(存在无法解析的剩余 token) return ast def parse_or(self): node self.parse_and() while self.peek() and self.peek()[0] OP and self.peek()[1] OR: self.consume() right self.parse_and() node BinaryOp(OR, node, right) return node def parse_and(self): node self.parse_not() while self.peek() and self.peek()[0] OP and self.peek()[1] AND: self.consume() right self.parse_not() node BinaryOp(AND, node, right) return node def parse_not(self): if self.peek() and self.peek()[0] OP and self.peek()[1] NOT: self.consume() operand self.parse_not() return UnaryOp(NOT, operand) return self.parse_compare() def parse_compare(self): left self.parse_primary() if self.peek() and self.peek()[0] COMPARE: op self.consume()[1] right self.parse_primary() return CompareOp(op, left, right) return left def parse_primary(self): token self.consume() if token[0] LPAREN: node self.parse_or() self.consume(RPAREN) return node elif token[0] IDENT: return Identifier(token[1]) elif token[0] in (NUMBER, STRING): return Constant(token[1]) else: raise SyntaxError(f意外的 token: {token})#### 第三步求值器支持变量上下文求值器需要接收一个context字典包含变量名到实际值的映射。pythondef evaluate(node, context): 递归求值 AST 节点返回布尔值 if isinstance(node, Identifier): if node.name not in context: raise KeyError(f变量 {node.name} 未在上下文中定义) return context[node.name] elif isinstance(node, Constant): return node.value elif isinstance(node, UnaryOp): val evaluate(node.operand, context) if node.op NOT: return not val elif isinstance(node, BinaryOp): left evaluate(node.left, context) right evaluate(node.right, context) if node.op AND: return left and right elif node.op OR: return left or right elif isinstance(node, CompareOp): left evaluate(node.left, context) right evaluate(node.right, context) if node.op : return left right elif node.op : return left right elif node.op : return left right elif node.op !: return left ! right elif node.op : return left right elif node.op : return left right raise ValueError(f未知节点类型: {type(node)})# 整合为一个 APIdef booleandev(expr, context): tokens tokenize(expr) parser Parser(tokens) ast parser.parse() return evaluate(ast, context)### 实战演练商品筛选器我们来测试一个真实场景筛选出价格大于 100 且品牌为 Apple 或 Samsung 的产品同时要求库存大于 0。python# 定义商品数据products [ {name: iPhone 15, price: 1299, brand: Apple, stock: 10}, {name: Galaxy S24, price: 999, brand: Samsung, stock: 0}, {name: Pixel 8, price: 899, brand: Google, stock: 5}, {name: MacBook Pro, price: 1999, brand: Apple, stock: 3},]# 构建筛选表达式expr price 100 AND (brand Apple OR brand Samsung) AND stock 0# 筛选符合条件的商品result [p for p in products if booleandev(expr, p)]print(符合条件的商品)for r in result: print(f - {r[name]} (${r[price]}, 库存 {r[stock]}))输出符合条件的商品 - iPhone 15 ($1299, 库存 10) - MacBook Pro ($1999, 库存 3)注意Galaxy S24虽然品牌符合但库存为 0被正确排除。### 扩展支持自定义函数与错误处理生产环境中的booleandev通常支持函数调用如contains(brand, App)。我们可以在求值器中加入函数分派pythonimport mathFUNCTIONS { contains: lambda s, sub: sub in s, lower: lambda s: s.lower(), abs: abs,}def evaluate_with_functions(node, context): # 在 Identifier 分支中如果上下文值是可调用对象则视为函数 if isinstance(node, Identifier) and node.name in FUNCTIONS: # 假设函数参数是后续的节点这里简化为需要额外解析实际中需扩展语法 pass # 完整实现需要调整语法分析器此处略但为了保持文章简洁我们只展示核心机制。实际库中会通过注册机制扩展。### 性能优化与注意事项1.缓存 AST如果同一表达式多次求值如每行数据应只解析一次缓存 AST 对象。2.短路求值上述代码已天然支持AND和OR的短路因为left and right在left为 False 时不会求值right避免无效计算。3.安全性在不可信表达式中应限制可访问的变量名和函数防止注入攻击。### 总结通过本文我们从零构建了一个支持AND、OR、NOT、比较和括号的布尔表达式引擎booleandev原型。核心在于将字符串解析为 AST再通过递归求值得到结果。这个模式广泛应用于规则引擎、权限校验、数据过滤等场景。实际工程中你可以扩展 Token 类型如日期、正则、支持自定义函数并加入缓存机制提升性能。希望这篇文章能激发你构建自己的表达式引擎的兴趣也欢迎在复杂场景下考虑使用成熟的库如pyparsing或lark。