有一个日志文件,其中文本以空格分隔的key=value
对形式出现,并且每一行最初都是从Python字典中的数据序列化的,例如:
' '.join([f'{k}={v!r}' for k,v in d.items()])
键始终只是字符串。值可以是ast.literal_eval
可以成功解析的任何值,至少也可以。
如何处理此日志文件并将行转换回Python字典?示例:
>>> to_dict("key='hello world'")
{'key': 'hello world'}
>>> to_dict("k1='v1' k2='v2'")
{'k1': 'v1', 'k2': 'v2'}
>>> to_dict("s='1234' n=1234")
{'s': '1234', 'n': 1234}
>>> to_dict("""k4='k5="hello"' k5={'k6': ['potato']}""")
{'k4': 'k5="hello"', 'k5': {'k6': ['potato']}}
以下是有关数据的一些额外信息:
eval
,exec
,yaml.load
)编辑: :根据注释的要求,这是MCVE和无法正常运行的示例代码
>>> def to_dict(s):
... s = s.replace(' ', ', ')
... return eval(f"dict({s})")
...
...
>>> to_dict("k1='v1' k2='v2'")
{'k1': 'v1', 'k2': 'v2'} # OK
>>> to_dict("s='1234' n=1234")
{'s': '1234', 'n': 1234} # OK
>>> to_dict("key='hello world'")
{'key': 'hello, world'} # Incorrect, the value was corrupted
答案 0 :(得分:5)
ast.literal_eval
之类的内容无法方便地解析您的输入,但是它可以作为tokenized作为一系列Python令牌而被解析。这使事情比以前容易一些。
=
令牌可以在您的输入中出现的唯一位置是作为键值分隔符;至少到目前为止,ast.literal_eval
不接受带有=
令牌的任何东西。我们可以使用=
令牌来确定键值对在何处开始和结束,而其余的大部分工作都可以由ast.literal_eval
处理。使用tokenize
模块还可以避免=
的问题或字符串文字中的反斜杠转义。
import ast
import io
import tokenize
def todict(logstring):
# tokenize.tokenize wants an argument that acts like the readline method of a binary
# file-like object, so we have to do some work to give it that.
input_as_file = io.BytesIO(logstring.encode('utf8'))
tokens = list(tokenize.tokenize(input_as_file.readline))
eqsign_locations = [i for i, token in enumerate(tokens) if token[1] == '=']
names = [tokens[i-1][1] for i in eqsign_locations]
# Values are harder than keys.
val_starts = [i+1 for i in eqsign_locations]
val_ends = [i-1 for i in eqsign_locations[1:]] + [len(tokens)]
# tokenize.untokenize likes to add extra whitespace that ast.literal_eval
# doesn't like. Removing the row/column information from the token records
# seems to prevent extra leading whitespace, but the documentation doesn't
# make enough promises for me to be comfortable with that, so we call
# strip() as well.
val_strings = [tokenize.untokenize(tok[:2] for tok in tokens[start:end]).strip()
for start, end in zip(val_starts, val_ends)]
vals = [ast.literal_eval(val_string) for val_string in val_strings]
return dict(zip(names, vals))
这在您的示例输入以及带有反斜杠的示例中均正确运行:
>>> todict("key='hello world'")
{'key': 'hello world'}
>>> todict("k1='v1' k2='v2'")
{'k1': 'v1', 'k2': 'v2'}
>>> todict("s='1234' n=1234")
{'s': '1234', 'n': 1234}
>>> todict("""k4='k5="hello"' k5={'k6': ['potato']}""")
{'k4': 'k5="hello"', 'k5': {'k6': ['potato']}}
>>> s=input()
a='=' b='"\'' c=3
>>> todict(s)
{'a': '=', 'b': '"\'', 'c': 3}
顺便说一句,我们可能会寻找令牌类型NAME而不是=
令牌,但是如果它们向set()
添加literal_eval
支持,那将会中断。寻找=
将来也可能会失败,但似乎不像寻找NAME
令牌那样容易。
答案 1 :(得分:3)
正则表达式替换功能进行救援
我不是为您重写一个类似ast的解析器,但是效果很好的一个技巧是使用正则表达式替换加引号的字符串,并将其替换为“变量”(我我们选择了__token(number)__
),有点像您正在放弃一些代码。
记下您要替换的字符串(应注意空格),用逗号替换空格(在:
等字符通过最后一次测试之前先防止出现符号),然后再次替换为字符串。
import re,itertools
def to_dict(s):
rep_dict = {}
cnt = itertools.count()
def rep_func(m):
rval = "__token{}__".format(next(cnt))
rep_dict[rval] = m.group(0)
return rval
# replaces single/double quoted strings by token variable-like idents
# going on a limb to support escaped quotes in the string and double escapes at the end of the string
s = re.sub(r"(['\"]).*?([^\\]|\\\\)\1",rep_func,s)
# replaces spaces that follow a letter/digit/underscore by comma
s = re.sub("(\w)\s+",r"\1,",s)
#print("debug",s) # uncomment to see temp string
# put back the original strings
s = re.sub("__token\d+__",lambda m : rep_dict[m.group(0)],s)
return eval("dict({s})".format(s=s))
print(to_dict("k1='v1' k2='v2'"))
print(to_dict("s='1234' n=1234"))
print(to_dict(r"key='hello world'"))
print(to_dict('key="hello world"'))
print(to_dict("""k4='k5="hello"' k5={'k6': ['potato']}"""))
# extreme string test
print(to_dict(r"key='hello \'world\\'"))
打印:
{'k2': 'v2', 'k1': 'v1'}
{'n': 1234, 's': '1234'}
{'key': 'hello world'}
{'key': 'hello world'}
{'k5': {'k6': ['potato']}, 'k4': 'k5="hello"'}
{'key': "hello 'world\\"}
关键是使用非贪婪的正则表达式提取字符串(带引号/双引号)并将其替换为表达式中的非字符串(例如,它们是字符串变量而不是文字)。正则表达式已经过调整,因此可以接受转义引号和字符串末尾的双转义(自定义解决方案)
替换功能是一个内部功能,因此它可以利用非本地词典和计数器来跟踪替换的文本,因此,一旦照顾到空格,就可以将其恢复。
用逗号替换空格时,必须注意不要在冒号(最后测试)或字母数字/下划线后考虑的所有事项之后进行(因此,逗号替换正则表达式中的\w
保护) )
如果我们在将原始字符串放回打印之前取消注释调试打印代码:
debug k1=__token0__,k2=__token1__
debug s=__token0__,n=1234
debug key=__token0__
debug k4=__token0__,k5={__token1__: [__token2__]}
debug key=__token0__
已对字符串进行了修剪,并且空格替换已正常进行。付出更多的努力,应该可能会引用引号并将k1=
替换为"k1":
,以便可以使用ast.literal_eval
代替eval
(风险更大,并且不是必需的)这里)
我确定某些超复杂表达式会破坏我的代码(我什至听说很少有json解析器能够解析100%的有效json文件),但是对于您提交的测试,它会起作用的(当然,如果某个有趣的家伙尝试将__tokenxx__
标识放在原始字符串中,那将会失败,也许可以用其他一些无效的变量占位符代替)。一段时间之前,我已经使用此技术构建了一个Ada lexer,以便能够避免字符串中的空格,并且效果很好。
答案 2 :(得分:2)
您可以找到所有出现的=
个字符,然后找到给出有效ast.literal_eval
结果的最大字符数。然后可以解析这些字符的值,并将其与最后一次成功解析和当前=
的索引之间的字符串切片所找到的键相关联:
import ast, typing
def is_valid(_str:str) -> bool:
try:
_ = ast.literal_eval(_str)
except:
return False
else:
return True
def parse_line(_d:str) -> typing.Generator[typing.Tuple, None, None]:
_eq, last = [i for i, a in enumerate(_d) if a == '='], 0
for _loc in _eq:
if _loc >= last:
_key = _d[last:_loc]
_inner, seen, _running, _worked = _loc+1, '', _loc+2, []
while True:
try:
val = ast.literal_eval(_d[_inner:_running])
except:
_running += 1
else:
_max = max([i for i in range(len(_d[_inner:])) if is_valid(_d[_inner:_running+i])])
yield (_key, ast.literal_eval(_d[_inner:_running+_max]))
last = _running+_max
break
def to_dict(_d:str) -> dict:
return dict(parse_line(_d))
print([to_dict("key='hello world'"),
to_dict("k1='v1' k2='v2'"),
to_dict("s='1234' n=1234"),
to_dict("""k4='k5="hello"' k5={'k6': ['potato']}"""),
to_dict("val=['100', 100, 300]"),
to_dict("val=[{'t':{32:45}, 'stuff':100, 'extra':[]}, 100, 300]")
]
)
输出:
{'key': 'hello world'}
{'k1': 'v1', 'k2': 'v2'}
{'s': '1234', 'n': 1234}
{'k4': 'k5="hello"', 'k5': {'k6': ['potato']}}
{'val': ['100', 100, 300]}
{'val': [{'t': {32: 45}, 'stuff': 100, 'extra': []}, 100, 300]}
免责声明:
此解决方案不如@Jean-FrançoisFabre的解决方案那么优雅,并且我不确定它是否可以解析传递给to_dict
的内容的100%,但它可能会为您提供自己的版本灵感。
答案 3 :(得分:1)
popstr
:从看起来像字符串的字符串开头拆分内容
如果以单引号或双引号开头,我将寻找下一个并在该点拆分。
def popstr(s):
i = s[1:].find(s[0]) + 2
return s[:i], s[i:]
poptrt
:从带括号('[]','()','{}')的字符串开始处拆分内容。
如果以方括号开头,我将开始为每个起始字符实例递增,并为其补码每个实例递减。当我达到零时,我分裂了。
def poptrt(s): d = {'{':'}','[':']','(':')'} b = s [0] c = lambda x:{b:1,d [b]:-1} .get(x,0) 零件= [] t,i = 1,1 当t> 0和s时: 如果i> len-1: 打破 '\'“'中的elif s [i]: s,s ,s = s [:i],* map(str.strip,popstr(s [i:])) parts.extend([ s,s ]) 我= 0 其他: t + = c(s [i]) 我+ = 1 如果t == 0: 返回''.join(parts + [s [:i]]),s [i:] 其他: 引发ValueError('您的字符串带有不平衡的括号。')
def to_dict(log):
d = {}
while log:
k, log = map(str.strip, log.split('=', 1))
if log.startswith(('"', "'")):
v, log = map(str.strip, popstr(log))
elif log.startswith((*'{[(',)):
v, log = map(str.strip, poptrt(log))
else:
v, *log = map(str.strip, log.split(None, 1))
log = ' '.join(log)
d[k] = ast.literal_eval(v)
return d
assert to_dict("key='hello world'") == {'key': 'hello world'}
assert to_dict("k1='v1' k2='v2'") == {'k1': 'v1', 'k2': 'v2'}
assert to_dict("s='1234' n=1234") == {'s': '1234', 'n': 1234}
assert to_dict("""k4='k5="hello"' k5={'k6': ['potato']}""") == {'k4': 'k5="hello"', 'k5': {'k6': ['potato']}}
import ast
def popstr(s):
i = s[1:].find(s[0]) + 2
return s[:i], s[i:]
def poptrt(s):
d = {'{': '}', '[': ']', '(': ')'}
b = s[0]
c = lambda x: {b: 1, d[b]: -1}.get(x, 0)
parts = []
t, i = 1, 1
while t > 0 and s:
if i > len(s) - 1:
break
elif s[i] in '\'"':
_s, s_, s = s[:i], *map(str.strip, popstr(s[i:]))
parts.extend([_s, s_])
i = 0
else:
t += c(s[i])
i += 1
if t == 0:
return ''.join(parts + [s[:i]]), s[i:]
else:
raise ValueError('Your string has unbalanced brackets.')
def to_dict(log):
d = {}
while log:
k, log = map(str.strip, log.split('=', 1))
if log.startswith(('"', "'")):
v, log = map(str.strip, popstr(log))
elif log.startswith((*'{[(',)):
v, log = map(str.strip, poptrt(log))
else:
v, *log = map(str.strip, log.split(None, 1))
log = ' '.join(log)
d[k] = ast.literal_eval(v)
return d