【问题标题】:How to convert string dictionary value into dict type in Python如何在 Python 中将字符串字典值转换为 dict 类型
【发布时间】:2021-09-24 05:32:14
【问题描述】:

我正在从文件中读取文本,文本如下所示:

"(id=336346860, name='Western Australia', slug='western-australia', has_public_page=True, lat=-26.0, lng=121.0)"

我想将其转换为字典。我试图将其转换为 dict 类型,但它给出了错误:

fileOutput = "(id=336346860, name='Western Australia', slug='western-australia', has_public_page=True, lat=-26.0, lng=121.0)"
x = dict(fileOutput)

错误:

ValueError: dictionary update sequence element #0 has length 1; 2 is required

有人可以帮忙找出解决办法吗?

【问题讨论】:

标签: python dictionary parsing


【解决方案1】:

更健壮的方法是在字符串前面加上一个标识符,例如 _,使其成为函数调用的有效 Python 语法,然后使用 ast.parse 将字符串解析为 Python 代码,遍历带有ast.walk 的代码树,并查找ast.Call 节点,其中有带有关键字参数列表的keywords 属性,您可以从中提取arg 属性中的名称和@ 中的值987654328@ 属性。由于value 属性本身可以是示例输入中的-26.0 等表达式,由26.0 的常量和- 的一元运算组成,因此您可以使用ast.literal_eval 评估节点以进行转换到它所代表的值:

{
    keyword.arg: ast.literal_eval(keyword.value)
    for node in ast.walk(ast.parse('_' + fileOutput)) if isinstance(node, ast.Call)
    for keyword in node.keywords
}

使用您的示例输入,这将返回:

{'id': 336346860, 'name': 'Western Australia', 'slug': 'western-australia', 'has_public_page': True, 'lat': -26.0, 'lng': 121.0}

【讨论】:

  • 从其中一个 cmets 中可以清楚地看出,OP 尝试解析文件中写为 repr 的 instaloader.PostLocation class(即使他们似乎没有意识到)。他们真的应该找到一种方法来正确处理这些文件。
【解决方案2】:

您可以使用ast.parse 做一些事情。将字符串解析为任何函数的构造函数(不必是dict),然后提取关键字参数。例如,以

开头
>>> mod = ast.parse('dict' + fileOutput)
>>> print(ast.dump(mod, indent=4))
Module(
    body=[
        Expr(
            value=Call(
                func=Name(id='dict', ctx=Load()),
                args=[],
                keywords=[
                    keyword(
                        arg='id',
                        value=Constant(value=336346860)),
                    keyword(
                        arg='name',
                        value=Constant(value='Western Australia')),
                    keyword(
                        arg='slug',
                        value=Constant(value='western-australia')),
                    keyword(
                        arg='has_public_page',
                        value=Constant(value=True)),
                    keyword(
                        arg='lat',
                        value=UnaryOp(
                            op=USub(),
                            operand=Constant(value=26.0))),
                    keyword(
                        arg='lng',
                        value=Constant(value=121.0))]))],
    type_ignores=[])

您现在可以很容易地提取关键字。您甚至可以在参数中期望任意树,因此您必须将ast.literal_eval 独立应用于每个关键字。这并不是特别困难。

首先稍微清理一下输入,以确保它至少看起来是对dict 构造函数(或您添加的任何函数名称)的调用:

if len(mod.body) > 1 or not isinstance(call := mod.body[0].value, ast.Call) or call.func.id != 'dict':
    raise ValueError('Not just one dict')
if call.args:
    raise ValueError('Why are there positional args?')

现在你可以提取关键字了:

>>> {x.arg: ast.literal_eval(x.value) for x in call.keywords}
{'id': 336346860,
 'name': 'Western Australia',
 'slug': 'western-australia',
 'has_public_page': True,
 'lat': -26.0,
 'lng': 121.0}

如果有人试图潜入任意函数调用,ast.literal_eval 将会崩溃。

TL;DR

def parse_line(line):
    mod = ast.parse('dict' + fileOutput)
    if len(mod.body) > 1 or not isinstance(call := mod.body[0].value, ast.Call) or call.func.id != 'dict':
        raise ValueError('Not just one dict')
    if call.args:
        raise ValueError('Why are there positional args?')
    return {x.arg: ast.literal_eval(x.value) for x in call.keywords}

【讨论】:

  • 从其中一个 cmets 中可以清楚地看出,OP 尝试解析文件中写为 repr 的instaloader.PostLocation class(即使他们似乎没有意识到)。他们真的应该找到一种方法来正确处理这些文件。
【解决方案3】:

根据下面列出的几个假设,我构建了一个自定义类来满足您的要求:

  1. 输入始终以括号 () 开头和结尾。
  2. 输入只能包含""(空字符串)或"()"(空括号)或"(id=336346860, name='Western Australia', slug='western-australia', has_public_page=True, lat=-26.0, lng=121.0)"等实际值。
  3. 值将仅在 python 支持的strboolintfloat 中。
  4. 键值对始终由= 分隔。
  5. ,(逗号)不是价值的一部分。 (即,逗号不存在于值中的任何位置

如果上述任何一个假设被打破,该类可能无法按预期工作


代码如下:

from typing import Optional


class MyDict:
    def setRawElements(self):
        """Create a list by splitting the given string"""
        # Assumption #5
        # If there is any comma in the value, then the split may be inconsistent
        self.raw_elements = self.string.split(", ")

    def splitKeyValuePairs(self):
        """Split into key value pairs and create a internal dictionary"""
        for elem in self.raw_elements:
            # Assumption #4
            # If the key and the value is not seperated by '=', then the split may be inconsistent
            key, value = elem.split("=")
            self.dictionary[key] = value

    def setKeyTypes(self):
        """Type conversion"""
        for key, value in self.dictionary.items():
            # Assumption #3
            # Value must be one among (bool, str, float, int)
            if value in ["True", "False"]:
                # check if the value is a boolean [True, False]
                type_ = bool
            elif value and value[0] == value[-1] == "'":
                # check if the value is a str object
                self.dictionary[key] = self.dictionary[key][1:-1]
                # we need not convert a str to str, so we can skip the conversion part
                continue
            elif "." in value:
                # float values will have two parts, integer and fraction seperated by a period
                type_ = float
            else:
                # if above mentioned cases are not matched, ww assume that the type is int
                type_ = int
            # type conversion from str to excpected type
            self.dictionary[key] = type_(self.dictionary[key])

    def parse(self, string):
        self.dictionary = {}
        self.string = string
        if string and string[1:-1]:
            # Assumption #1 and #2
            # If string is not empty and not just empty parenthesis
            self.string = self.string[1:-1]  # remove parenthesis from start and end
            self.setRawElements()
            self.splitKeyValuePairs()
            self.setKeyTypes()
        return self.dictionary

    def __new__(cls, string: str) -> Optional[dict]:
        """Calling a class will return parsed dictionary"""
        return super().__new__(cls).parse(string)

要使用该类,请参考以下代码:

fileOutput = "(id=336346860, name='Western Australia', slug='western-australia', has_public_page=True, lat=-26.0, lng=121.0)"
x = MyDict(fileOutput)
print(x)

下面是输出:

{'id': 336346860, 'name': 'Western Australia', 'slug': 'western-australia', 'has_public_page': True, 'lat': -26.0, 'lng': 121.0}

要检查值的类型,请参考以下代码:

for key, value in x.items():
    print(key, value, type(value), sep=" - ")

输出:

id - 336346860 - <class 'int'>
name - Western Australia - <class 'str'>
slug - western-australia - <class 'str'>
has_public_page - True - <class 'bool'> 
lat - -26.0 - <class 'float'>
lng - 121.0 - <class 'float'>

【讨论】:

  • 大声笑,我花了 50 分钟编写这个程序。请为所花费的宝贵时间投票:)
  • 在一小时内为一门复杂的语言编写自己的解析器的问题在于,肯定会有很多无法解释的陷阱。除了列出的限制(例如支持的类型数量有限以及字符串可能不包含逗号)之外,如果字符串用双引号引起来或者是原始字符串或文档字符串文字,或者浮点数不包含.,如1e99
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-04-08
  • 2021-07-25
  • 1970-01-01
  • 2011-06-10
  • 2012-06-14
相关资源
最近更新 更多