
本文介绍一种基于正则解析与操作符映射的安全、可扩展方案,用于批量校验字典中键值对是否同时满足用户输入的类 Python 表达式条件(如 'a > 5, 0 < (cd) < 6, ef < 35'),避免 eval() 风险,支持多种比较运算符、嵌套不等式及字符串/数值/布尔/None 类型自动推断。
本文介绍一种基于正则解析与操作符映射的安全、可扩展方案,用于批量校验字典中键值对是否同时满足用户输入的类 python 表达式条件(如 `'a > 5, 0
在实际数据处理场景中,常需根据用户动态输入的条件字符串(如 'a > 5, 0 < (cd) < 6, ef < 35')判断一个字典是否整体满足所有约束。虽然可手动拆分、提取键名与操作符并逐条执行判断,但这种方式易出错、难以维护,且无法优雅支持链式比较(如 0 < x < 6)或混合类型(字符串、浮点数、布尔值、None)。更关键的是,直接使用 eval() 执行用户输入存在严重安全风险,绝不推荐。
以下提供一个轻量、健壮、无外部依赖(仅标准库)的解决方案,核心思想是:将条件字符串结构化解析 → 映射为安全的操作函数 → 按顺序组合求值。
✅ 核心实现逻辑
我们使用 re 进行词法切分,operator 模块提供标准比较函数,并通过 cast() 自动识别并转换字面量类型(整数、浮点、布尔、带引号字符串、None)。关键设计包括:
- 操作符预注册:支持 ==, !=, <, <=, >, >= 六种常见比较;
- 智能键值获取:若切分出的 token 是字典中存在的 key(如 'a'),则取 data['a'];否则尝试解析为字面量(如 '3.14' → 3.14, '"hello"' → 'hello');
- 链式比较支持:通过 v[i:i+2] 将 0 < (cd) < 6 拆解为 (0 < data['(cd)']) and (data['(cd)'] < 6),天然兼容任意长度的连续不等式;
- 类型安全转换:cast() 函数严格匹配数字、布尔字面量('true'/'false')、单/双引号字符串及 None,拒绝非法输入。
? 完整可运行代码
import operator
import re
# 1. 定义支持的操作符及其对应函数
operators = {
'<=': operator.le,
'>=': operator.ge,
'>': operator.gt,
'<': operator.lt,
'==': operator.eq,
'!=': operator.ne
}
# 2. 构建正则:匹配任意已注册操作符(转义防特殊字符干扰)
_oper = '|'.join(map(re.escape, operators.keys()))
oper_re = re.compile(fr'\s*({_oper})\s*').split
# 3. 数值与字符串匹配正则
number_re = re.compile(r'-?\d*(\.\d+)?').fullmatch
string_re = re.compile(r'("|\')(?P<str>.*)\1').fullmatch
def cast(value: str) -> float | int | bool | str | None:
"""安全解析字面量字符串为对应 Python 值"""
v = value.strip()
if not v:
return None
lower_v = v.lower()
if lower_v in ('true', 'false'):
return lower_v == 'true'
if lower_v == 'none':
return None
if m := string_re(v):
return m.group('str')
if m := number_re(v):
return float(v) if '.' in v else int(v)
raise ValueError(f"无法解析字面量: {value}")
def check_conditions(data: dict, conditions: str) -> bool:
"""
判断字典 data 是否满足所有 conditions 中的条件
Args:
data: 待校验的字典
conditions: 逗号分隔的条件字符串,如 'a > 5, 0 < (cd) < 6, ef == 35'
Returns:
bool: 所有条件均成立返回 True,否则 False
Raises:
ValueError: 条件语法错误(如操作符数量不匹配)或字面量无法解析
"""
facts = [] # 存储每个原子比较的结果(True/False)
for cond in conditions.split(','):
cond = cond.strip()
if not cond:
continue
values, ops = [], []
# 按操作符切分,交替提取值和操作符
tokens = oper_re(cond)
for token in tokens:
token = token.strip()
if not token:
continue
if token in operators:
ops.append(operators[token])
else:
# 尝试从字典取值,失败则解析为字面量
val = data.get(token)
if val is None:
val = cast(token)
values.append(val)
# 验证结构:n 个操作符必须对应 n+1 个值(如 a<b<c → 2 op, 3 val)
if len(values) != len(ops) + 1:
raise ValueError(f"条件格式错误: '{cond}' —— 值与操作符数量不匹配")
# 执行所有二元比较:v0 op0 v1, v1 op1 v2, ...
for i in range(len(ops)):
try:
result = ops[i](values[i], values[i + 1])
except TypeError as e:
raise TypeError(f"类型不匹配无法比较 '{values[i]}' {list(operators.keys())[list(operators.values()).index(ops[i])]} '{values[i+1]}'") from e
facts.append(result)
return all(facts)
# ✅ 使用示例
if __name__ == "__main__":
test_data = {
'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, ef == 35',
'mn is None, ij == "hello", kl == False',
'ab < 5, gh > 10',
]
for case in test_cases:
try:
result = check_conditions(test_data, case)
print(f"✅ '{case}' → {result}")
except Exception as e:
print(f"❌ '{case}' → 错误: {e}")⚠️ 注意事项与最佳实践
- 安全性优先:本方案完全规避 eval() 和 exec(),所有用户输入均经显式解析与类型校验,杜绝远程代码执行(RCE)风险。
- 键名限制:字典键若含空格、括号、点号等(如 '(cd)'),需确保用户输入的条件中完全一致(包括括号),否则会当作字面量解析失败。
- 链式比较语义:0 < x < 6 被解释为 (0 < x) and (x < 6),符合直觉;但 a != b != c 不等价于 (a != b) and (b != c)(数学上可能不成立),请按需调整逻辑。
- 扩展性提示:如需支持 in、not in、is、is not 等操作符,只需在 operators 字典中添加对应 operator 函数(如 operator.contains)并增强 cast() 对容器字面量的支持。
- 性能考量:对于高频调用场景,可将 oper_re、number_re 等正则编译为模块级常量,避免重复编译;复杂条件建议预编译为检查函数缓存。
该方案在简洁性、安全性与表达力之间取得良好平衡,适用于配置驱动型校验、低代码规则引擎、数据质量检查等生产环境,是替代 eval() 的专业级实践范本。









