如何在Python中将具有未引用键的字符串转换为字典

6

我有一个字符串

"{a:'b', c:'d',e:''}"

请注意,字典条目的键未加引号,因此在之前的问题中建议使用简单的eval("{a:'b', c:'d',e:''}")是不起作用的。
将此字符串转换为字典的最便捷方式是什么?
{'a':'b', 'c':'d', 'e':''}

1
这个字符串的来源是什么? - juanpa.arrivillaga
2
@juanpa.arrivillaga:我猜这是一个Javascript对象字面量(不是 JSON,JSON需要属性名加引号)。 - ShadowRanger
目前这里没有一个通用的答案可行。可以考虑使用基于分词器的方法,就像用户2357112在这里所描述的那样。 - wim
5个回答

7
如果这个内容是来自于一个可信来源(不要用于一般用户输入,因为eval 不安全;但是,如果你从潜在的恶意用户那里获取输入,你应该使用 JSON 格式和json 模块),你可以使用一个技巧来使用 eval
source = """{e: '', a: 'b', c: 'd'}"""

class identdict(dict):
    def __missing__(self, key):
        return key

d = eval(source, identdict())
print(d)

打印

{'a': 'b', 'c': 'd', 'e': ''}

这是如何工作的: 我们创建了一个新的字典子类 identdict,它定义了魔法方法__missing__。对于从字典中缺少的键进行查找时,会调用这个方法。在这种情况下,我们只返回键,所以字典将键映射到它们自身。然后使用一个identdict实例作为globals参数来evaluate源代码。eval将查找globals映射中变量的值;由于它是一个identdict,所访问的每个变量的值现在方便地是变量的名称。
适用于更复杂的字符串作为值,以及任何正确的Python文字语法。

不错。虽然我的内容只包含字母数字,但学到了新东西。由于我已经接受了其他答案,所以只能为您点赞。感谢。 - Harrison
@Harrison,你随时可以更改已接受的答案。 - Antti Haapala -- Слава Україні
@AnttiHaapala 很棒的小技巧!但是这个怎么工作的?我不太理解它是如何通过 NameError 的,难道不应该在字典处理缺失值之前就引发了吗?它又是如何返回字符串的呢?我在 __missing__ 方法中添加了 print(type(key)),如果我像 id[1] 这样做,其中 id 是一个 identdict,它会打印出 <class int>,如果我做一些类似 id[x] 的事情,其中 x 没有被定义,我会得到一个 NameError__missing__ 方法将永远不会被调用!很明显,我没有完全理解 eval 内置函数在做什么。 - juanpa.arrivillaga
@juanpa.arrivillaga 不,Python中的全局变量存储在模块级别的字典中;此外,可以将本地变量的映射提供给eval。通常,eval默认使用由调用globals()locals()返回的这些映射;现在我们使用identdict实例代替。 - Antti Haapala -- Слава Україні
这是一个非常被低估的答案。它将是一个非常好的例子,可以快速地映射出调用层次结构。 - Matthew
4
有问题。针对"{e: '', a: 'b', if: 'd'}"无法成功执行。 - wim

2
根据您所解析的内容的复杂程度,这种方法可能有效:
s = "{a:'b', c:'d',e:''}"
d = dict([
    (x.split(':')[0].strip(), x.split(':')[1].strip("' "))
    for x in s.strip("{}").split(',')
])

1

手动解析容易出错且难以普遍适用,而基于eval的方法在键是Python关键字时会失败。目前被接受的答案如果值包含空格、逗号或冒号,则会出现错误,而eval答案无法处理像iffor这样的键。

相反,我们可以将输入作为一系列Python标记进行标记化,并将NAME标记替换为STRING标记,然后取消标记以构建有效的字典文字。从那里开始,我们只需调用ast.literal_eval即可。

import ast
import io
import tokenize

def parse(x):
    tokens = tokenize.generate_tokens(io.StringIO(x).readline)
    modified_tokens = (
        (tokenize.STRING, repr(token.string)) if token.type == tokenize.NAME else token[:2]
        for token in tokens)

    fixed_input = tokenize.untokenize(modified_tokens)

    return ast.literal_eval(fixed_input)

然后parse("{a:'b', c:'d',e:''}") == {'a':'b', 'c':'d', 'e':''},并且关键字作为键或值中的特殊字符不会出现问题:
>>> parse('{a: 2, if: 3}')
{'a': 2, 'if': 3}
>>> parse("{c: ' : , '}")
{'c': ' : , '}

0

警告 如果您的“字典”中间有映射到空字符串的键,则此方法将无法按预期工作。我不会删除此答案,因为我认为这种方法仍然可以挽救。

这可能比威尔的答案更通用,尽管它仍然取决于您正在解析的确切结构。如果您的键值对将由字母数字单词组成,则应该没问题。

In [3]: import re

In [4]: import itertools

In [5]: my_string = "{a:'b', c:'d',e:''}"

In [6]: temp = re.findall(r"\w", my_string)

In [7]: temp = itertools.zip_longest(temp[0::2], temp[1::2], fillvalue = "")

In [8]: dict(temp)
Out[8]: {'a': 'b', 'c': 'd', 'e': ''}

如果你想知道zip函数的运作原理,请参考以下问题:

如何在Python中将列表中的每对元素收集到元组中

我使用了itertools.zip_longest,这样你就可以使用填充值,灵感来自于:

从单个列表中获取成对元素


嗯,仔细想想,如果你在“字典”中间有一个键映射到空字符串,那么这将无法按预期工作。好吧,它可能仍然有用。 - juanpa.arrivillaga
似乎有人给大家投了反对票。我已经为您投回赞成票了。 - Harrison
1
这在很大程度上取决于字符串的确切内容。 - Antti Haapala -- Слава Україні
好的,我已经在第一个评论中承认了这个不足。我应该编辑答案并包含一个警告吗? - juanpa.arrivillaga

-1
import re
str="{a:'b', c:'d',e:''}"
dict([i.replace("'","").split(":") for i in re.findall(r"(\w+\:'.*?')",str)])

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接