代码之家  ›  专栏  ›  技术社区  ›  wim

将key=value对转换回Python dict

  •  4
  • wim  · 技术社区  · 7 年前

    有一个以空格分隔的形式显示文本的日志文件 key=value 对,并且每一行最初都是从Python dict中的数据序列化的,类似于:

    ' '.join([f'{k}={v!r}' for k,v in d.items()])
    

    钥匙总是串在一起的。这些值可以是 ast.literal_eval 能成功解析,不多不少。

    例子:

    >>> 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']}}
    

    以下是有关数据的一些额外上下文:

    • 钥匙是 valid names
    • 输入行格式良好(例如,没有悬挂的支架)
    • 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
    
    3 回复  |  直到 7 年前
        1
  •  7
  •   user2357112    7 年前

    您的输入不能方便地用类似 ast.literal_eval ,但它 tokenized 作为一系列Python标记。这让事情变得比以前容易一些。

    唯一的地方 = 评估 不接受任何带有 里面有记号。我们可以使用 = 评估 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}
    

    顺便说一句,我们可能会查找标记类型名称,而不是 = 代币,但如果他们再加上 set() literal_eval . 寻找 = 也可能在未来破裂,但似乎不像寻找的那样破裂 NAME

        2
  •  4
  •   Jean-François Fabre    7 年前

    我是 为您重写一个类似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\\"}
    

    关键是使用非贪婪正则表达式提取字符串(引号/双引号),并用非字符串替换它们(就像那些是字符串一样)

    替换函数是一个内部函数,因此它可以利用非局部字典&计数器和跟踪替换的文本,以便在处理完空格后可以恢复。

    当用逗号替换空格时,必须小心不要在冒号(上一个测试)或alphanum/下划线(因此 \w 替换regex中的逗号保护)

    如果在放回打印的原始字符串之前取消对调试打印代码的注释:

    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,它能够避免字符串中的空格,效果非常好。

        3
  •  3
  •   Ajax1234    7 年前

    你可以找到 = 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]}
    

    免责声明:

    to_dict ,但它可能会给你自己版本的灵感。

        4
  •  1
  •   piRSquared    6 年前

    提供两个助手函数。

    • popstr :从看起来像字符串的字符串开始拆分对象

      def popstr(s):
          i = s[1:].find(s[0]) + 2
          return s[:i], s[i:]
      
    • poptrt :从被方括号(“[]”、“()”、“{}”)包围的字符串开始拆分。

      定义权限: b=s[0] c=lambda x:{b:1,d[b]:-1}.get(x,0) t、 i=1,1 而t>0和s: 打破 s、 s码 零件.延伸([ s、 s码 ]) i=0 其他: t+=c(s[i]) i+=1 如果t==0: 返回“”。join(parts+[s[:i]]),s[i:] 其他:


    细嚼细绳直到没有细绳可嚼

    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