有没有一种方法可以覆盖 Python 的 JSON 处理程序?

7
我在json中编码无穷大的时候遇到了一些问题。
`json.dumps` 将其转换为 `“Infinity”`,但我希望它能将其转换为 `null` 或其他自定义值。
不幸的是,设置 `default` 参数似乎只在 `dumps` 不理解对象时起作用,否则默认处理程序似乎会被绕过。
是否有方法可以预先编码对象,更改类型/类的默认编码方式或在正常编码之前将某些类型/类转换为不同的对象?
4个回答

6
看这里的源码:http://hg.python.org/cpython/file/7ec9255d4189/Lib/json/encoder.py 如果你想要子类化JSONEncoder,你可以仅覆盖iterencode(self, o, _one_shot=False)方法,该方法有显式特例用于Infinity(在内部函数中)。
为了使其可重用,您还需要更改__init__以接受一些新选项,并将它们存储在类中。
或者,您可以从pypi选择一个json库,该库具有所需的可扩展性:https://pypi.python.org/pypi?%3Aaction=search&term=json&submit=search 以下是一个示例:
import json

class FloatEncoder(json.JSONEncoder):

    def __init__(self, nan_str = "null", **kwargs):
        super(FloatEncoder,self).__init__(**kwargs)
    self.nan_str = nan_str

    # uses code from official python json.encoder module.
    # Same licence applies.
    def iterencode(self, o, _one_shot=False):
        """Encode the given object and yield each string
        representation as available.

        For example::

            for chunk in JSONEncoder().iterencode(bigobject):
                mysocket.write(chunk)
        """
        if self.check_circular:
            markers = {}
        else:
            markers = None
        if self.ensure_ascii:
            _encoder = json.encoder.encode_basestring_ascii
        else:
            _encoder = json.encoder.encode_basestring
        if self.encoding != 'utf-8':
            def _encoder(o, _orig_encoder=_encoder,
                         _encoding=self.encoding):
                if isinstance(o, str):
                    o = o.decode(_encoding)
                return _orig_encoder(o)

        def floatstr(o, allow_nan=self.allow_nan,
                     _repr=json.encoder.FLOAT_REPR,
                     _inf=json.encoder.INFINITY,
                     _neginf=-json.encoder.INFINITY,
                     nan_str = self.nan_str):
            # Check for specials. Note that this type of test is 
            # processor and/or platform-specific, so do tests which
            # don't depend on the internals.

            if o != o:
                text = nan_str
            elif o == _inf:
                text = 'Infinity'
            elif o == _neginf:
                text = '-Infinity'
            else:
                return _repr(o)

            if not allow_nan:
                raise ValueError(
                    "Out of range float values are not JSON compliant: " +
                    repr(o))

            return text

        _iterencode = json.encoder._make_iterencode(
                markers, self.default, _encoder, self.indent, floatstr,
                self.key_separator, self.item_separator, self.sort_keys,
                self.skipkeys, _one_shot)
        return _iterencode(o, 0)


example_obj = {
    'name': 'example',
    'body': [
        1.1,
        {"3.3": 5, "1.1": float('Nan')},
        [float('inf'), 2.2]
    ]}

print json.dumps(example_obj, cls=FloatEncoder)

ideone: http://ideone.com/dFWaNj


给定的代码片段在Python3.6中无法工作。会出现“AttributeError: 'FloatEncoder' object has no attribute 'encoding'”错误。 - AbdealiJK
@AbdealiJK 这段代码编写后已经过去了3.5年,Python 3.6版本已经发布。https://www.python.org/downloads/release/python-360/ - Marcin
啊,没注意日期 ^_^ 看来我得想出其他办法,因为我的应用需要保持对3.3+的支持。 - AbdealiJK

5
不,没有简单的方法可以实现这个。事实上,根据标准,NaNInfinity浮点值根本不应该被JSON序列化。Python使用的是标准的扩展。您可以通过向dumps传递allow_nan=False参数使python编码符合标准,但这会对无穷大/无穷小数值引发一个ValueError错误,即使您提供了一个default函数也是如此。
你有两种方法可以做你想要的:
1. 子类化JSONEncoder并更改这些值的编码方式。请注意,您必须考虑到可能包含无穷大值等的序列情况。据我所知,没有API可以重新定义如何编码特定类的对象。 2. 复制要编码的对象,并将任何无限/非数字的出现替换为None或其他一些您想要编码的对象。
一个不太健壮,但更简单的解决方案是修改编码后的数据,例如将所有的Infinity子字符串替换为null
>>> import re
>>> infty_regex = re.compile(r'\bInfinity\b')
>>> def replace_infinities(encoded):
...     regex = re.compile(r'\bInfinity\b')
...     return regex.sub('null', encoded)
... 
>>> import json
>>> replace_infinities(json.dumps([1, 2, 3, float('inf'), 4]))
'[1, 2, 3, null, 4]'

显然,您应该考虑字符串中的文本Infinity,因此即使在这里,一个强大的解决方案也不是立即可行的,也不够优雅。

必要的修改实际上非常简单。 - Marcin
在JSON编码之前替换Infinity可能更容易实现一个健壮的解决方案。这样,您只需使用float("inf")检查相等性即可。 - jwg

0

背景

我遇到了这个问题,但不想为项目增加额外的依赖来处理这种情况。此外,我的项目支持Python 2.6、2.7、3.3和3.4以及simplejson的用户。不幸的是,在这些版本之间有三个不同的iterencode实现,因此硬编码特定版本是不可取的。

希望这能帮助有类似需求的其他人!

限定条件

如果与您的json.dumps调用相关的编码时间/处理能力与项目的其他组件相比较小,您可以使用parse_constant关键字参数对JSON进行解码/重新编码,以获得所需的结果。

优点

  • 无论最终用户使用Python 2.x的json、Python 3.x的json还是使用simplejson(例如:import simplejson as json),都没有关系
  • 它只使用公共的json接口,这些接口不太可能改变。

注意事项

  • 这将需要 ~3倍的时间来编码事物
  • 此实现不处理 object_pairs_hook,因为这样它就不能在 python 2.6 中工作
  • 无效的分隔符将失败

代码

class StrictJSONEncoder(json.JSONEncoder):

    def default(self, o):
        """Make sure we don't instantly fail"""
        return o

    def coerce_to_strict(self, const):
        """
        This is used to ultimately *encode* into strict JSON, see `encode`

        """
        # before python 2.7, 'true', 'false', 'null', were include here.
        if const in ('Infinity', '-Infinity', 'NaN'):
            return None
        else:
            return const

    def encode(self, o):
        """
        Load and then dump the result using parse_constant kwarg

        Note that setting invalid separators will cause a failure at this step.

        """

        # this will raise errors in a normal-expected way
        encoded_o = super(StrictJSONEncoder, self).encode(o)

        # now:
        #    1. `loads` to switch Infinity, -Infinity, NaN to None
        #    2. `dumps` again so you get 'null' instead of extended JSON
        try:
            new_o = json.loads(encoded_o, parse_constant=self.coerce_to_strict)
        except ValueError:

            # invalid separators will fail here. raise a helpful exception
            raise ValueError(
                "Encoding into strict JSON failed. Did you set the separators "
                "valid JSON separators?"
            )
        else:
            return json.dumps(new_o, sort_keys=self.sort_keys,
                              indent=self.indent,
                              separators=(self.item_separator,
                                          self.key_separator))

这似乎对于 dump() 无效... 这会返回一个 null: json.dumps(float('nan'), cls=CustomJSONEncoder),但是这会返回一个 NaN: json.dump(float('nan'), open('/tmp/a', 'w'), cls=CustomJSONEncoder)。因为 dump() 使用 iterencodedumps 使用 encode() - AbdealiJK

-1
你可以按照以下方式进行操作:
import json
import math

target=[1.1,1,2.2,float('inf'),float('nan'),'a string',int(2)]

def ffloat(f):
    if not isinstance(f,float):
        return f
    if math.isnan(f):
        return 'custom NaN'
    if math.isinf(f):
        return 'custom inf'
    return f

print 'regular json:',json.dumps(target)      
print 'customized:',json.dumps(map(ffloat,target))     

输出:

regular json: [1.1, 1, 2.2, Infinity, NaN, "a string", 2]
customized: [1.1, 1, 2.2, "custom inf", "custom NaN", "a string", 2]

如果你想处理嵌套的数据结构,这也不是很难:

import json
import math
from collections import Mapping, Sequence

def nested_json(o):
    if isinstance(o, float):
        if math.isnan(o):
            return 'custom NaN'
        if math.isinf(o):
            return 'custom inf'
        return o
    elif isinstance(o, basestring):
        return o
    elif isinstance(o, Sequence):
        return [nested_json(item) for item in o]
    elif isinstance(o, Mapping):
        return dict((key, nested_json(value)) for key, value in o.iteritems())
    else:
        return o    

nested_tgt=[1.1,{1.1:float('inf'),3.3:5},(float('inf'),2.2),]

print 'regular json:',json.dumps(nested_tgt)      
print 'nested json',json.dumps(nested_json(nested_tgt))

输出:

regular json: [1.1, {"3.3": 5, "1.1": Infinity}, [Infinity, 2.2]]
nested json [1.1, {"3.3": 5, "1.1": "custom inf"}, ["custom inf", 2.2]]

1
除非处理平面结构,否则这并不真正有用。 - Marcin
@Marcin:这几乎不是一个完整的JSON编码器。而且代码是有效的。你可以使用JSONEncoder的子类拦截iterencode - 很好 - Python 2.7不使用iterencode,因此它不起作用。请展示一些有效的代码! - dawg
1
仍在等待看到能够打印单个平面和嵌套数据结构的工作代码。这比你想象的要难。请参考此链接 - dawg
1
我看不出来这比仅迭代数据结构以查找所需的浮点数更容易。你指出的代码有60行密集的内容,当新版本的JSON编码器发布时,可能会出现版本问题。为什么那样更容易呢? - dawg
实际上,我非常喜欢这个解决方案。符合我的逻辑简单性的需求。我不知道如何使用iterencode,所以这可能是我偏见的来源。 - cammil
显示剩余7条评论

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