【问题标题】:Python JSON encoder convert NaNs to null insteadPython JSON 编码器将 NaN 转换为 null
【发布时间】:2015-04-22 18:42:48
【问题描述】:

我正在编写代码来接收能够转换为 JSON 的任意对象(可能是嵌套的)。

Python 内置 JSON 编码器的默认行为是将 NaN 转换为 NaN,例如json.dumps(np.NaN) 导致 NaN。如何将此 NaN 值更改为 null

我尝试subclass JSONEncoder and override the default() method如下:

from json import JSONEncoder, dumps
import numpy as np
    
class NanConverter(JSONEncoder):
    def default(self, obj):
        try:
            _ = iter(obj)
        except TypeError:
            if isinstance(obj, float) and np.isnan(obj):
                return "null"
        return JSONEncoder.default(self, obj)

>>> d = {'a': 1, 'b': 2, 'c': 3, 'e': np.nan, 'f': [1, np.nan, 3]}
>>> dumps(d, cls=NanConverter)
'{"a": 1, "c": 3, "b": 2, "e": NaN, "f": [1, NaN, 3]}'

预期结果:'{"a": 1, "c": 3, "b": 2, "e": null, "f": [1, null, 3]}'

【问题讨论】:

  • 编码前为什么不直接替换字典中的NaNs?
  • 我收到了一个能够转换为 JSON 的任意对象,并且没有直接对其进行编码。但是,我想确保在创建 JSON 字符串表示时将 NaN 转换为 null。
  • 我在想这是否是 JSON 编码器中的错误,因为它会产生无效的 JSON。 {"x": NaN} 在语法上是不正确的 JSON IMO。它应该输出 {"x": "NaN"} 或 {"x": null}。两者在语法上都是有效的 - 其中哪个更有意义是一个不同的问题。

标签: python json numpy nan


【解决方案1】:

使用熊猫

对于那些使用 Pandas 的人,最简单的方法 - 不需要第三方库:df.to_json。这甚至可以转换嵌套结构中的 NaN 和其他 Numpy 类型:

df = pd.DataFrame({
  'words': ['on', 'off'],
  'lists': [
    [[1, 1, 1], [2, 2, 2], [3, 3, 3]],
    [[np.nan], [np.nan], [np.nan]],
  'dicts': [
    {'S': {'val': 'A'}},
    {'S': {'val': np.nan}},
  ]
})

如果将其转换为字典列表,Pandas 会保留原生的 nan 值:

json.dumps(df.to_dict(orient='record'))

> [{
    "words": "on",
    "lists": [[1, 1, 1], [2, 2, 2], [3, 3, 3]],
    "dicts": {"S": {"val": "A"}}
  },
  {
    "words": "off",
    "lists": [[NaN], [NaN], [NaN]],
    "dicts": {"S": {"val": NaN}}
  }]

但如果您让 Pandas 将其直接转换为 JSON 字符串,它会为您解决:

df.to_json(orient='records')

> [{
    "words": "on",
    "lists": [[1,1,1],[2,2,2],[3,3,3]],
    "dicts": {"S":{"val":"A"}}
  },
  {
    "words": "off",
    "lists": [[null],[null],[null]],
    "dicts": {"S":{"val":null}}
  }]

注意orient 的值在to_dict()to_json() 之间略有不同。

使用标准库

如果您只是使用列表、字典和标量值,您可以手动转换 NaN:

import math

def to_none(val):
    if math.isnan(val):
        return None
    return val

【讨论】:

    【解决方案2】:

    这是我用于将NaN 转换为None 的解决方案。嵌套的 list 似乎也处理得很好。 dicts 的递归是自动处理的。

    def null_convert(obj):
        if isinstance(obj, dict):
            for i in obj:
                if isinstance(obj[i], float) and np.isnan(obj[i]):
                    obj[i]= None
                if isinstance(obj[i], list):
                    for j,v in enumerate(obj[i]):
                        if isinstance(v, float) and np.isnan(v):
                            obj[i][j] = None
        return obj
    
    json.loads(json_str, object_hook = null_convert)
    

    【讨论】:

      【解决方案3】:

      您可以尝试将字典序列化为字符串,然后将“NaN”替换为“null”,然后将其编码回来:

          d = json.dumps(d) # json dump string
          d = d.replace("NaN", "null")
          d = json.loads(d) # json load string
      

      但是你必须小心。如果由于某种原因,“NaN”是字典中某个键或值中字符串的一部分,则在替换步骤中需要额外注意。

      【讨论】:

      • 请参阅发布到 Saurabh Chandra Patel 帖子的评论。主要是null不是"null"(不带引号)。
      【解决方案4】:

      我使用以下解决方法:

      json_constant_map = {
          '-Infinity': float('-Infinity'),
          'Infinity': float('Infinity'),
          'NaN': None,
      }
      
      def json_nan_to_none(obj: typing.Any, *, default: typing.Callable = None) -> None:
          # We want to convert NaNs to None and we have to use for now this workaround.
          # We still want an exception for infinity and -infinity.
          # See: https://github.com/python/cpython/pull/13233
          json_string = json.dumps(obj, default=default)
          return json.loads(
              json_string,
              parse_constant=lambda constant: json_constant_map[constant],
          )
      

      【讨论】:

        【解决方案5】:

        有一个 PR 可以在 Python json 标准库中自定义,但它还没有合并。

        【讨论】:

          【解决方案6】:

          你可以使用 simplejson 但如果你只想使用 JSON 模块,那么我的窍门

          json.dumps(d).replace(", NaN," , ', "null",')
          

          【讨论】:

          • 不应该是, null,(未引用)吗?此外,不用说,有许多可能的方法可以找到 NaN(有和没有前导/栏杆逗号,有/没有空格,作为数组的第一个或最后一个元素等。
          【解决方案7】:

          simplejson 将在这里做正确的工作,但还有一个额外的标志值得包括:

          尝试使用 simplejson:

          pip install simplejson
          

          然后在代码中:

          import simplejson
          
          response = df.to_dict('records')
          simplejson.dumps(response, ignore_nan=True,default=datetime.datetime.isoformat)
          

          ignore_nan 标志将正确处理所有 NaN --> null 转换

          默认标志将允许 simplejson 正确解析您的日期时间

          【讨论】:

          • 我可以建议您添加有关default 参数的解释作为对已接受答案的评论吗?似乎比用一个与 OP 的问题无关的参数复制已接受的答案更有意义。
          【解决方案8】:
          1. 正如@Gerrat 指出的那样,很遗憾,你的钩子dumps(d, cls=NanConverter) 不起作用。

          2. @Alexander 的 simplejson.dumps(d, ignore_nan=True) 有效,但引入了额外的依赖项 (simplejson)。

          如果我们引入另一个依赖项(熊猫):

          1. 另一个明显的解决方案是dumps(pd.DataFrame(d).fillna(None)),但Pandas issue 1972 指出d.fillna(None) 会有不可预测的行为:

            注意fillna(None)等价于fillna(),这意味着value参数没有被使用。相反,它使用默认为正向填充的方法参数。

          2. 所以改为使用DataFrame.where:

            df = pd.DataFrame(d)
            dumps(df.where(pd.notnull(df), None)))
            

          【讨论】:

          • d 是一本字典。它没有where 方法。即使不是,您的方法似乎只返回非空值,而我希望 null 在值确实为空的地方。
          • 谢谢,我已经更正了我的解决方案以适应字典。不确定您的其他评论;我得考虑一下。
          • 此代码不适用于 OP 的 d。即使您修复此 sn-p 以使用 d.items() 而不是 d,并从 json 导入 dumps,列表中的 nan 也不会转换。
          【解决方案9】:

          这似乎达到了我的目的:

          import simplejson
          
          
          >>> simplejson.dumps(d, ignore_nan=True)
          Out[3]: '{"a": 1, "c": 3, "b": 2, "e": null, "f": [1, null, 3]}'
          

          【讨论】:

          • 有没有办法在内置的 json 包中获得这种行为?我宁愿不再依赖第三方包。
          • 我找不到。
          【解决方案10】:

          不幸的是,您可能需要使用@Bramar 的建议。您将无法直接使用它。 The documentation 表示 Python 的 JSON 编码器状态:

          如果指定,默认是为无法序列化的对象调用的函数

          你的 NanConverter.default 方法甚至没有被调用,因为 Python 的 JSON 编码器已经知道如何序列化 np.nan。添加一些打印语句 - 你会看到你的方法甚至没有被调用。

          【讨论】:

            猜你喜欢
            • 2018-09-24
            • 2018-06-26
            • 2017-04-23
            • 1970-01-01
            • 1970-01-01
            • 2023-01-20
            • 2020-12-10
            • 1970-01-01
            • 2016-10-15
            相关资源
            最近更新 更多