J'essaie de sérialiser la sortie de l'analyse de certaines données binaires avec la bibliothèque Construct2.9. Je veux sérialiser le résultat en JSON.
packet
est une instance d'une classe Construct Container
.
Apparemment, il contient un _io
Caché de type BytesIO
- voir la sortie de dict(packet)
ci-dessous:
{
'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
}
Maintenant, appeler json.dumps(packet)
conduit évidemment à une 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
Cependant, ce qui me dérange, c'est que l'exécution de json.dumps(packet, skipkeys=True)
entraîne exactement la même erreur, alors que je m'attendrais à ce qu'il saute le champ _io
. Quel est le problème ici? Pourquoi skipkeys
ne me permet-il pas de sauter le champ _io
?
J'ai réussi à faire fonctionner le code en remplaçant JSONEncoder
et en retournant None
pour les champs de type BytesIO
, mais cela signifie que ma chaîne sérialisée contient des charges d'éléments "_io": null
, que je préférerais ne pas avoir du tout ...
Les clés avec un trait de soulignement _
Ne sont pas vraiment "cachées", ce sont juste des chaînes de plus pour JSON. La classe Construct Container
n'est qu'un dictionnaire avec ordre, la clé _io
N'a rien de spécial pour cette classe.
Vous avez deux options:
default
qui renvoie simplement une valeur de remplacement.et peut-être un troisième, mais une analyse informelle des pages du projet Construct ne me dit pas si elle est disponible: avoir Construct sortie JSON ou au moins un dictionnaire compatible JSON, peut-être en utilisant des adaptateurs.
Le hook par défaut ne peut pas empêcher l'ajout de la clé _io
À la sortie, mais vous permettrait au moins d'éviter l'erreur:
json.dumps(packet, default=lambda o: '<not serializable>')
Le filtrage peut être effectué récursivement; le @functools.singledispatch()
décorateur peut aider à garder un tel code propre:
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
J'ai également l'implémentation ci-dessus un argument skip_underscore
Supplémentaire, pour ignorer explicitement les clés qui ont un caractère _
Au début. Cela aiderait à ignorer tous les attributs "cachés" supplémentaires que la bibliothèque Construct utilise.
Puisque Container
est une sous-classe dict
, le code ci-dessus gérera automatiquement les instances telles que packet
.
skipkeys
ne fait pas ce que vous pensez qu'il fait - il demande au json.JSONEncoder
d'ignorer les touches qui ne sont pas d'un type de base , pas les valeurs des clés - c'est-à-dire que si vous aviez un dict
{object(): "foobar"}
il sauterait le object()
, tandis que sans skipkeys
réglé sur True
, cela augmenterait un TypeError
.
Vous pouvez surcharger JSONEncoder.iterencode()
(et son ventre) et y effectuer un filtrage prospectif, mais vous finirez par réécrire à peu près le module json
, en le ralentissant dans le processus car vous ne pourrez pas bénéficier des parties compilées. Je vous suggère de pré-traiter vos données via un filtrage itératif et d'ignorer les clés/types que vous ne voulez pas dans votre JSON final. Le module json
devrait alors pouvoir le traiter sans aucune instruction supplémentaire. Quelque chose comme:
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
Créez ensuite votre filtre:
import io
preprocessor = SkipFilter([io.BytesIO], ["_io"]) # double-whammy skip of io.BytesIO
Dans ce cas, sauter simplement par type devrait suffire, mais si la touche _io
Contient d'autres données indésirables, cela garantit qu'elle ne sera pas dans le résultat final. Quoi qu'il en soit, vous pouvez simplement filtrer les données avant de les transmettre à JSONEncoder
:
import json
json_data = json.dumps(preprocessor.filter(packet)) # no _io keys or io.BytesIO data...
Bien sûr, si votre structure contient d'autres données exotiques ou des données qui sont représentées différemment dans JSON en fonction de son type, cette approche peut gâcher car elle transforme tous les mappages en dict
et toutes les séquences en list
. Cependant, pour une utilisation générale, cela devrait être plus que suffisant.
Ignorer un champ non sérialisable nécessite une logique supplémentaire lourde, comme indiqué correctement dans toutes les réponses précédentes.
Si vous n'avez pas vraiment besoin d'exclure le champ, vous pouvez générer une valeur par défaut à la place:
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))
Cela produira ce résultat:
{"a": 1, "b": "<<non-serializable: bytes>>"}
Ce code affichera le nom du type, ce qui pourrait être utile si vous souhaitez implémenter vos sérialiseurs personnalisés ultérieurement.