Python如何将冒号分隔的多行错误字符串规范转换为字典
问题描述
我在开发中使用的模块按如下方式定义API错误处理类:
class APIError(BetfairError): """ Exception raised if error is found. """ def __init__( self, response: Optional[dict], method: str = None, params: dict = None, exception: Exception = None, ): super(APIError, self).__init__(response, method, params, exception) self.response = response self.method = method self.params = params self.exception = exception def __str__(self): if self.response: error_data = self.response.get("error") return "%s \nParams: %s \nException: %s \nError: %s \nFull Response: %s" % ( self.method, self.params, self.exception, error_data, self.response, ) else: return "%s \nParams: %s \nException: %s" % ( self.method, self.params, self.exception, )
该错误类输出的异常字符串为固定多行格式,每行均通过冒号分隔键名与值内容,值包含接口路径字符串、None、Python字典/列表字面量,格式示例如下:
betfairlightweight.exceptions.APIError: SportsAPING/v1.0/listMarketBook Params: {'marketIds': ['1.200293211'], 'priceProjection': {'priceData': ['EX_TRADED'], 'exBestOffersOverrides': {}, 'virtualise': True, 'rolloverStakes': False}} Exception: None Error: {'code': -32099, 'message': 'ANGX-0006', 'data': {'APINGException': {'requestUUID': '...........', 'errorCode': 'UNEXPECTED_ERROR', 'errorDetails': ''}, 'exceptionname': 'APINGException'}} Full Response: {'jsonrpc': '2.0', 'error': {'code': -32099, 'message': 'ANGX-0006', 'data': {'APINGException': {'requestUUID': '..........', 'errorCode': 'UNEXPECTED_ERROR', 'errorDetails': ''}, 'exceptionname': 'APINGException'}}, 'id': 1}
为了不受输出位置限制直接按键取值,我目前通过字符串替换拼接加eval的方式将该多行字符串转换为字典,实现代码如下:
# except Exception as e: # e.__str__() e = """betfairlightweight.exceptions.APIError: SportsAPING/v1.0/listMarketBook Params: {'marketIds': ['1.200293211'], 'priceProjection': {'priceData': ['EX_TRADED'], 'exBestOffersOverrides': {}, 'virtualise': True, 'rolloverStakes': False}} Exception: None Error: {'code': -32099, 'message': 'ANGX-0006', 'data': {'APINGException': {'requestUUID': '...........', 'errorCode': 'UNEXPECTED_ERROR', 'errorDetails': ''}, 'exceptionname': 'APINGException'}} Full Response: {'jsonrpc': '2.0', 'error': {'code': -32099, 'message': 'ANGX-0006', 'data': {'APINGException': {'requestUUID': '..........', 'errorCode': 'UNEXPECTED_ERROR', 'errorDetails': ''}, 'exceptionname': 'APINGException'}}, 'id': 1}""" fs = [] nfs = [] for row in e.split('\n'): fs.append(row[:row.find(':')] + ':') nfs.append('\'' + row[:row.find(':')] + '\'' + ':') for x, y in zip(fs, nfs): e = e.replace(x, y) for z in e.split('\n'): if (': {' not in z) and (': None' not in z): e = e.replace(z, z.replace(': ', ': \'').strip() + '\'') for_dict = eval('{' + e.replace('\n', ',') + '}') print(for_dict) # Get SportsAPING/v1.0/listMarketBook value print(for_dict['betfairlightweight.exceptions.APIError']) # Get ABC-0025 value print(for_dict['Error']['message']) # Get 2.0 value print(for_dict['Full Response']['jsonrpc']) # Get ['EX_TRADED'] value print(for_dict['Params']['priceProjection']['priceData'])
请问是否存在更符合Python编码规范的实现方式,或是现成的工具模块,可以更简洁、专业、安全地完成这类多行冒号分隔字符串到字典的转换?
实现方案
最优方案:直接访问异常对象属性,无需解析字符串
你当前的实现完全走了弯路:这个APIError类本身已经把所有需要的字段都作为公开实例属性存储,__str__方法只是为了方便日志打印做的格式化输出,根本不是给程序解析用的数据源。捕获异常后直接读取属性即可,零解析成本、零安全风险、性能最高:
try: # 你的接口调用逻辑 client.call_api(...) except APIError as e: # 直接读取属性,不需要处理任何字符串 api_path = e.method request_params = e.params raw_exception = e.exception error_info = e.response.get("error") if e.response else None full_response = e.response # 直接按需取值即可 print(api_path) # 输出 SportsAPING/v1.0/listMarketBook print(error_info["message"]) # 输出 ANGX-0006 print(full_response["jsonrpc"]) # 输出 2.0 print(request_params["priceProjection"]["priceData"]) # 输出 ['EX_TRADED']
备选方案:仅能拿到异常字符串时,用安全解析替代eval
如果是从日志等场景拿到的只有格式化后的异常字符串、无法获取原始异常对象,绝对不要用eval——eval会执行字符串内的任意代码,只要异常内容被注入恶意代码就会直接触发安全问题。可以用Python标准库自带的ast.literal_eval做安全的字面量解析,这个方法只会解析字典、列表、字符串、数字、None、布尔值这类原生字面量,不会执行任意代码,实现逻辑也更简洁鲁棒:
import ast from typing import Dict, Any def parse_api_error(err_str: str) -> Dict[str, Any]: result = {} # 按行拆分,过滤空行 lines = [line.strip() for line in err_str.splitlines() if line.strip()] for line in lines: # 只按第一个冒号拆分,避免值内部的冒号(比如字典里的字符串带冒号)导致拆分错误 key, value_part = line.split(":", 1) key = key.strip() value_part = value_part.strip() if value_part == "None": value = None else: try: # 尝试解析为Python字面量(字典、列表、数字等) value = ast.literal_eval(value_part) except (SyntaxError, ValueError): # 解析失败说明是普通字符串,直接赋值 value = value_part result[key] = value return result
这个实现比你当前的字符串替换逻辑鲁棒性更强,不会因为值内容里出现和键名重复的文本就替换出错,也没有任意代码执行的安全风险。
注意事项
- 任何场景下都优先选第一种方案,解析格式化字符串是不得已的下策
- 禁止用
eval处理任何来源不可控的字符串,这是Python开发中明确的安全反模式
内容的提问来源于stack exchange,提问作者Digital Farmer
相关产品推荐
相关产品推荐

