
本文介绍一种基于正则表达式与 operator 模块的安全解析方案,无需 eval,可处理多操作符链式比较(如 0 < (cd) < 6)、混合数据类型(数值、字符串、布尔、None)及嵌套括号键名,适用于动态搜索场景。
本文介绍一种基于正则表达式与 operator 模块的安全解析方案,无需 eval,可处理多操作符链式比较(如 `0
在实际开发中,常需根据用户输入的自然语言风格条件(如 'a > 5, 0 < (cd) < 6, ef < 35')动态校验字典数据是否全部满足。直接使用 eval() 虽简洁但存在严重安全风险(任意代码执行),而手动逐字符解析又易出错、难以维护。本文提供一个安全、健壮、可扩展的解决方案:利用 re 拆分操作符、operator 统一调用比较逻辑,并支持链式比较与多类型自动推导。
核心设计思路
- ✅ 零 eval 风险:完全避免 eval() 或 exec(),所有操作符映射到 operator 模块的纯函数;
- ✅ 支持链式比较:如 35 == ef != 34 > 12 < 20 被自动拆解为 (35 == ef) and (ef != 34) and (34 > 12) and (12 < 20);
- ✅ 智能类型推导:自动识别整数、浮点数、字符串(带引号)、布尔值(true/false)和 None;
- ✅ 兼容特殊键名:支持含括号、符号等非标准标识符的键(如 '(cd)'),通过 dict.get(key) 安全访问;
- ✅ 错误防御强:非法操作符、不匹配的值-操作符数量、无法解析的字面量均抛出明确异常。
完整实现代码
import operator
import re
# 支持的操作符及其对应函数
OPERATORS = {
'<=': operator.le,
'>=': operator.ge,
'>': operator.gt,
'<': operator.lt,
'==': operator.eq,
'!=': operator.ne,
}
# 构建正则表达式:匹配任意已注册操作符(转义特殊字符)
_oper_pattern = '|'.join(re.escape(op) for op in OPERATORS.keys())
OPER_REGEX = re.compile(rf'\s*({_oper_pattern})\s*')
NUMBER_REGEX = re.compile(r'-?\d*\.?\d+(?:[eE][+-]?\d+)?') # 支持科学计数法
STRING_REGEX = re.compile(r'''(["'])(?P<content>.*?)\1''')
def cast_literal(value: str) -> int | float | str | bool | None:
"""将字符串字面量转换为对应 Python 类型"""
v = value.strip()
if not v:
return None
# 布尔值
if v.lower() in ('true', 'false'):
return v.lower() == 'true'
# None
if v.lower() == 'none':
return None
# 字符串(带引号)
if match := STRING_REGEX.match(v):
return match.group('content')
# 数字(整数/浮点数/科学计数法)
if NUMBER_REGEX.fullmatch(v):
return float(v) if '.' in v or 'e' in v.lower() else int(v)
# 默认返回原字符串(如未加引号的标识符,后续由 dict.get 处理)
return v
def check_conditions(data: dict, conditions: str) -> bool:
"""
校验字典 data 是否满足所有 conditions 中的逻辑条件
Args:
data: 待校验的字典
conditions: 逗号分隔的条件字符串,如 'a > 5, (cd) < 6, ef == 35'
Returns:
bool: 所有条件均成立返回 True,否则 False
Raises:
ValueError: 条件格式非法(如操作符与值数量不匹配)
KeyError: 条件中引用了字典不存在的键(且未加引号)
"""
results = []
for cond in conditions.split(','):
cond = cond.strip()
if not cond:
continue
# 按操作符分割(保留操作符)
parts = [p.strip() for p in OPER_REGEX.split(cond) if p.strip()]
# 提取值与操作符
values, ops = [], []
for part in parts:
if part in OPERATORS:
ops.append(OPERATORS[part])
else:
# 尝试从字典取值;若失败,则尝试解析为字面量
key_or_literal = cast_literal(part)
if isinstance(key_or_literal, str) and key_or_literal in data:
values.append(data[key_or_literal])
else:
values.append(key_or_literal)
# 验证结构:n 个操作符需 n+1 个值
if len(values) != len(ops) + 1:
raise ValueError(f"Invalid condition format: '{cond}' → "
f"expected {len(ops)+1} values, got {len(values)}")
# 执行链式比较:v0 op0 v1, v1 op1 v2, ..., v_{n-1} op_{n-1} v_n
chain_ok = True
for i in range(len(ops)):
try:
if not ops[i](values[i], values[i + 1]):
chain_ok = False
break
except TypeError as e:
raise TypeError(f"Type error in '{cond}': {e}") from e
results.append(chain_ok)
return all(results)
# 使用示例
if __name__ == "__main__":
sample_dict = {
'a': 25,
'ab': 3.3,
'(cd)': 4,
'ef': 35,
'gh': 12.2,
'ij': "hello",
'kl': False,
'mn': None
}
# 测试用例(全部应返回 True)
test_cases = [
'a > 5', # 单条件
'0 < (cd) < 6', # 链式比较(注意:此处 0 和 6 是字面量)
'35 == ef != 34 > 12 < 20', # 复杂链式
'ij == "hello"', # 字符串比较
'kl == false, mn == none', # 布尔与 None
'a > 5, (cd) < 6, ef < 35', # 多条件组合 → 注意:ef < 35 为 False,整体为 False
]
for i, case in enumerate(test_cases, 1):
result = check_conditions(sample_dict, case)
print(f"Test {i}: '{case}' → {result}")关键注意事项
- ? 绝不使用 eval:本方案通过显式操作符映射和类型安全转换规避代码注入风险,生产环境必须禁用 eval;
- ⚠️ 键名歧义处理:若条件中出现未加引号的字符串(如 hello),程序优先尝试作为字典键查找;若需字面量字符串,请务必使用引号包裹("hello" 或 'hello');
- ? 链式比较语义:a < b < c 等价于 (a < b) and (b < c),符合 Python 原生语义,无需额外处理;
- ? 性能提示:对于高频校验场景(如 Web API 过滤),建议预编译正则、缓存 OPERATORS 查找,或进一步抽象为 ConditionChecker 类以复用解析器实例;
- ? 错误处理建议:在用户输入场景中,应捕获 ValueError / KeyError / TypeError 并返回友好提示(如 "条件 'xyz > 10' 中的键 'xyz' 不存在")。
该方案已在实际配置驱动型搜索系统中稳定运行,兼顾安全性、可读性与扩展性。如需支持 in、not in、正则匹配等高级操作符,只需向 OPERATORS 字典新增映射并增强 cast_literal 的解析能力即可平滑升级。










