如何在Python中使json.dumps忽略不可序列化的字段

时间:2018-08-03 13:54:01

标签: python json python-3.x construct

我正在尝试使用Construct2.9库序列化解析某些二进制数据的输出。我想将结果序列化为JSON。

packet是Construct类Container的实例。

显然,它包含类型为_io的隐藏BytesIO-请参见以下dict(packet)的输出:

{
'packet_length': 76, 'uart_sent_time': 1, 'frame_number': 42958, 
'subframe_number': 0, 'checksum': 33157, '_io': <_io.BytesIO object at 0x7f81c3153728>, 
'platform':661058, 'sync': 506660481457717506, 'frame_margin': 20642,
'num_tlvs': 1, 'track_process_time': 593, 'chirp_margin': 78,
'timestamp': 2586231182, 'version': 16908293
}

现在,调用json.dumps(packet)显然会导致TypeError:

...

File "/usr/lib/python3.5/json/__init__.py", line 237, in dumps
    **kw).encode(obj)
File "/usr/lib/python3.5/json/encoder.py", line 198, in encode
    chunks = self.iterencode(o, _one_shot=True)
File "/usr/lib/python3.5/json/encoder.py", line 256, in iterencode
    return _iterencode(o, 0)
File "/usr/lib/python3.5/json/encoder.py", line 179, in default
    raise TypeError(repr(o) + " is not JSON serializable")
TypeError: <_io.BytesIO object at 0x7f81c3153728> is not JSON serializable

但是我感到困惑的是,运行json.dumps(packet, skipkeys=True)会导致完全相同的错误,而我希望它会跳过_io字段。这里有什么问题?为什么skipkeys不允许我跳过_io字段?

我通过重写JSONEncoder并为None类型的字段返回BytesIO来工作,但这意味着我的序列化字符串包含"_io": null元素的加载,我宁愿完全没有...

3 个答案:

答案 0 :(得分:9)

带有_下划线的键并不是真正的“隐藏”键,它们只是JSON的更多字符串。 Construct Container类只是一个有序的字典,_io键对该类没有什么特殊要求。

您有两个选择:

  • 实现一个default挂钩,该挂钩仅返回替换值。
  • 过滤出在序列化之前 无法使用的键/值对。

也许还有三分之一,但是对Construct项目页面的随意扫描并没有告诉我是否可用:可以通过使用适配器使Construct输出JSON或至少一个与JSON兼容的字典。

默认钩子无法阻止将_io键添加到输出中,但是至少可以避免该错误:

json.dumps(packet, default=lambda o: '<not serializable>')

过滤可以递归进行; @functools.singledispatch() decorator可以帮助保持此类代码的干净:

from functools import singledispatch

_cant_serialize = object()

@singledispatch
def json_serializable(object, skip_underscore=False):
    """Filter a Python object to only include serializable object types

    In dictionaries, keys are converted to strings; if skip_underscore is true
    then keys starting with an underscore ("_") are skipped.

    """
    # default handler, called for anything without a specific
    # type registration.
    return _cant_serialize

@json_serializable.register(dict)
def _handle_dict(d, skip_underscore=False):
    converted = ((str(k), json_serializable(v, skip_underscore))
                 for k, v in d.items())
    if skip_underscore:
        converted = ((k, v) for k, v in converted if k[:1] != '_')
    return {k: v for k, v in converted if v is not _cant_serialize}

@json_serializable.register(list)
@json_serializable.register(tuple)
def _handle_sequence(seq, skip_underscore=False):
    converted = (json_serializable(v, skip_underscore) for v in seq)
    return [v for v in converted if v is not _cant_serialize]

@json_serializable.register(int)
@json_serializable.register(float)
@json_serializable.register(str)
@json_serializable.register(bool)  # redudant, supported as int subclass
@json_serializable.register(type(None))
def _handle_default_scalar_types(value, skip_underscore=False):
    return value

我在上面的实现中还添加了一个skip_underscore参数,以显式跳过开头带有_字符的键。这将有助于跳过Construct库正在使用的所有其他“隐藏”属性。

由于Containerdict的子类,因此上面的代码将自动处理诸如packet之类的实例。

答案 1 :(得分:1)

skipkeys并没有像您想象的那样执行-它指示json.JSONEncoder跳过不是 basic 类型的键,而不是 basic 类型的键。键-即如果您有一个dict {object(): "foobar"},它将跳过object()键,而如果没有将skipkeys设置为True,它将引发一个{{1} }。

您可以重载JSONEncoder.iterencode()(及其下腹部)并在那里进行超前过滤,但是最终将大量重写TypeError模块,从而在赢得过程中减慢了速度无法从已编译的部分中受益。我建议您通过迭代过滤来预处理数据,并跳过最终JSON中不需要的键/类型。然后json模块应该能够处理它,而无需任何其他说明。像这样:

json

然后创建您的过滤器:

import collections

class SkipFilter(object):

    def __init__(self, types=None, keys=None, allow_empty=False):
        self.types = tuple(types or [])
        self.keys = set(keys or [])
        self.allow_empty = allow_empty  # if True include empty filtered structures

    def filter(self, data):
        if isinstance(data, collections.Mapping):
            result = {}  # dict-like, use dict as a base
            for k, v in data.items():
                if k in self.keys or isinstance(v, self.types):  # skip key/type
                    continue
                try:
                    result[k] = self.filter(v)
                except ValueError:
                    pass
            if result or self.allow_empty:
                return result
        elif isinstance(data, collections.Sequence):
            result = []  # a sequence, use list as a base
            for v in data:
                if isinstance(v, self.types):  # skip type
                    continue
                try:
                    result.append(self.filter(v))
                except ValueError:
                    pass
            if result or self.allow_empty:
                return result
        else:  # we don't know how to traverse this structure...
            return data  # return it as-is, hope for the best...
        raise ValueError

在这种情况下,仅按类型跳过就足够了,但是如果import io preprocessor = SkipFilter([io.BytesIO], ["_io"]) # double-whammy skip of io.BytesIO 键包含其他一些不良数据,则可以保证它不会出现在最终结果中。无论如何,您可以先过滤数据,然后再将其传递到_io

JSONEncoder

当然,如果您的结构包含一些其他奇异数据或根据其类型以JSON不同方式表示的数据,则此方法可能会将其弄乱,因为它将所有映射都转换为import json json_data = json.dumps(preprocessor.filter(packet)) # no _io keys or io.BytesIO data... ,所有序列都转换为{{ 1}}。但是,对于一般用法,这应该绰绰有余。

答案 2 :(得分:0)

忽略所有无法序列化的字段需要大量额外的逻辑,如先前所有答案中正确指出的那样。

如果您确实不需要排除该字段,则可以改为生成默认值:

def safe_serialize(obj):
  default = lambda o: f"<<non-serializable: {type(o).__qualname__}>>"
  return json.dumps(obj, default=default)

obj = {"a": 1, "b": bytes()} # bytes is non-serializable by default
print(safe_serialize(obj))

这将产生以下结果:

{"a": 1, "b": "<<non-serializable: bytes>>"}

此代码将打印类型名称,如果以后要实现自定义序列化程序,这可能会很有用。