如何安全访问Python嵌套字典并返回数据对应来源路径
问题描述
我们持有多个嵌套字典结构,用户可自由选取不同字典中的字段拼装生成YAML文件,例如可从字典data1中选取某一数据,从字典data2中选取另一数据。
但最终生成的YAML文件中,无法快速定位每个数据的来源,因此需要实现一种简便的嵌套字典数据访问方案,在获取数据值的同时自动附加该数据的完整访问路径,预期效果参考如下示例:
my_data_dict = { "data_source_1": {"foo": {"bar": 'data1'}}, "data_source_2": {"foo": {"bar": 'data2'}}, "data_source_3": {"foo": {"bar": 'data3'}}, } >>> my_data_dict["data_source_1"]["foo"]["bar"] expected : {'data': 'data1', '__source': '["data_source_1"]["foo"]["bar"]'}
原有实现基于*eval()*完成取值,存在严重的任意代码执行安全风险,不适合生产环境使用,原有代码如下:
def get_info(_my_data_dict, keys): """Return data in a dictionary with 2 keys : data and __source.""" result = dict() result["data"] = eval(_my_data_dict + keys) result["__source"] = str(keys.strip()) return result my_data_dict = { "data_source_1": {"foo": {"bar": 'data1'}}, "data_source_2": {"foo": {"bar": 'data2'}}, "data_source_3": {"foo": {"bar": 'data3'}}, } >>> print(get_info("my_data_dict", '["data_source_1"]["foo"]["bar"]')) {'data': 'data_foobar', '__source': '["data_source_1"]["foo"]["bar"]'}
安全实现方案
核心思路是完全抛弃eval(),通过逐层访问字典的方式取值,从根源上避免代码注入风险。提供两种可直接使用的实现:
方案1:键列表传参(最安全,无解析开销)
直接传入按访问顺序排列的键列表,不需要做字符串解析,性能最优,完全没有注入风险,是最推荐的实现方式:
from functools import reduce import operator def get_info(data_dict: dict, key_path: list[str]): """ 访问嵌套字典,返回取值结果与完整访问路径 :param data_dict: 待访问的根字典对象 :param key_path: 按层级顺序排列的键列表,例:["data_source_1", "foo", "bar"] """ # 逐层完成字典取值 data = reduce(operator.getitem, key_path, data_dict) # 拼接标准格式的来源路径 source_path = "".join(f'["{k}"]' for k in key_path) return {"data": data, "__source": source_path} # 调用示例 my_data_dict = { "data_source_1": {"foo": {"bar": 'data1'}}, "data_source_2": {"foo": {"bar": 'data2'}}, "data_source_3": {"foo": {"bar": 'data3'}}, } print(get_info(my_data_dict, ["data_source_1", "foo", "bar"])) # 输出:{'data': 'data1', '__source': '["data_source_1"]["foo"]["bar"]'}
方案2:兼容原有字符串路径传参
如果需要兼容原有字符串格式的路径输入,使用正则提取路径中的键名,全程不执行任何动态代码,同样可以保证安全:
import re from functools import reduce import operator def get_info(data_dict: dict, key_path_str: str): """ 兼容字符串格式路径的安全实现 :param data_dict: 待访问的根字典对象 :param key_path_str: 字符串格式访问路径,例:'["data_source_1"]["foo"]["bar"]' """ # 正则提取所有方括号内的键名,无代码执行逻辑 key_path = re.findall(r'\["([^"]+)"\]', key_path_str.strip()) if not key_path: raise ValueError("键路径格式无效") # 逐层取值,路径不存在时抛出原生KeyError,和原生字典访问行为一致 data = reduce(operator.getitem, key_path, data_dict) return {"data": data, "__source": key_path_str.strip()} # 调用示例 my_data_dict = { "data_source_1": {"foo": {"bar": 'data1'}}, "data_source_2": {"foo": {"bar": 'data2'}}, "data_source_3": {"foo": {"bar": 'data3'}}, } print(get_info(my_data_dict, '["data_source_1"]["foo"]["bar"]')) # 输出:{'data': 'data1', '__source': '["data_source_1"]["foo"]["bar"]'}
补充说明
- 两种实现都完全规避了
eval()带来的代码注入风险,取值逻辑和原生字典访问行为一致 - 如果嵌套结构中包含列表,只需要调整键解析逻辑,将数字格式的键转为整数类型即可支持索引访问
- 优先使用方案1的键列表传参方式,从根源上避免字符串解析带来的格式兼容问题
内容的提问来源于stack exchange,提问作者John
相关产品推荐
相关产品推荐

