Как преобразовать данные JSON в объект Python

189

Я хочу использовать Python для преобразования данных JSON в объект Python.

Я получаю объекты данных JSON из API Facebook, которые хочу сохранить в моей базе данных.

Мой текущий вид в Django (Python) (request.POST содержит JSON):

response = request.POST
user = FbApiUser(user_id = response['id'])
user.name = response['name']
user.username = response['username']
user.save()
  • Это прекрасно работает, но как мне обрабатывать сложные объекты данных JSON?

  • Не было бы намного лучше, если бы я мог каким-то образом преобразовать этот объект JSON в объект Python для удобства использования?

  • 0
    Обычно JSON преобразуется в ванильные списки или диктовки. Это то, что вы хотите? Или вы надеетесь конвертировать JSON прямо в пользовательский тип?
  • 0
    Я хочу преобразовать его в объект, к которому я могу получить доступ, используя «.» , Как и в примере выше -> reponse.name, response.education.id и т. Д.
Показать ещё 1 комментарий
Теги:

13 ответов

262
Лучший ответ

Вы можете сделать это в одной строке, используя namedtuple и object_hook:

import json
from collections import namedtuple

data = '{"name": "John Smith", "hometown": {"name": "New York", "id": 123}}'

# Parse JSON into an object with attributes corresponding to dict keys.
x = json.loads(data, object_hook=lambda d: namedtuple('X', d.keys())(*d.values()))
print x.name, x.hometown.name, x.hometown.id

или, чтобы повторно использовать это легко:

def _json_object_hook(d): return namedtuple('X', d.keys())(*d.values())
def json2obj(data): return json.loads(data, object_hook=_json_object_hook)

x = json2obj(data)

Если вы хотите, чтобы он обрабатывал ключи, которые не являются хорошими именами атрибутов, проверьте namedtuple rename параметр.

  • 5
    это может привести к ошибке Value, ValueError: Имена типов и имена полей не могут начинаться с цифры: «123»
  • 3
    Как новичок в Python, меня интересует, будет ли это безопасным делом, когда проблема безопасности.
Показать ещё 15 комментариев
116

Ознакомьтесь с разделом " Специализирование декодирования объектов JSON" в документации по модулю json. Вы можете использовать это для декодирования объекта JSON в определенный тип Python.

Вот пример:

class User(object):
    def __init__(self, name, username):
        self.name = name
        self.username = username

import json
def object_decoder(obj):
    if '__type__' in obj and obj['__type__'] == 'User':
        return User(obj['name'], obj['username'])
    return obj

json.loads('{"__type__": "User", "name": "John Smith", "username": "jsmith"}',
           object_hook=object_decoder)

print type(User)  # -> <type 'type'>

Обновить

Если вы хотите получить доступ к данным в словаре через модуль json, сделайте это:

user = json.loads('{"__type__": "User", "name": "John Smith", "username": "jsmith"}')
print user['name']
print user['username']

Так же, как обычный словарь.

  • 1
    Эй, я только что прочитал и понял, что словари вполне подойдут, только мне было интересно, как преобразовать объекты JSON в словари и как получить доступ к этим данным из словаря?
  • 0
    Удивительно, это почти понятно, просто хотелось узнать еще одну маленькую вещь: если есть этот объект -> {'education': {'name1': 456, 'name2': 567}}, как мне получить доступ к этим данным?
Показать ещё 5 комментариев
69

Это не кодекс гольфа, но вот мой самый короткий трюк, используя types.SimpleNamespace в качестве контейнера для объектов JSON.

По сравнению с ведущим решением namedtuple это:

  • вероятно, быстрее/меньше, так как он не создает класс для каждого объекта
  • короче
  • no rename и, возможно, такое же ограничение на ключи, которые не являются допустимыми идентификаторами (использует setattr под обложками)

Пример:

from __future__ import print_function
import json

try:
    from types import SimpleNamespace as Namespace
except ImportError:
    # Python 2.x fallback
    from argparse import Namespace

data = '{"name": "John Smith", "hometown": {"name": "New York", "id": 123}}'

x = json.loads(data, object_hook=lambda d: Namespace(**d))

print (x.name, x.hometown.name, x.hometown.id)
  • 2
    Кстати, библиотека сериализации Marshmallow предлагает аналогичную функцию с декоратором @post_load . marshmallow.readthedocs.io/en/latest/...
  • 2
    Чтобы избежать зависимости от argparse: замените импорт argparse from types import SimpleNamespace и используйте: x = json.loads(data, object_hook=lambda d: SimpleNamespace(**d))
Показать ещё 3 комментария
55

Вы можете попробовать следующее:

class User(object):
    def __init__(self, name, username, *args, **kwargs):
        self.name = name
        self.username = username

import json
j = json.loads(your_json)
u = User(**j)

Просто создайте новый объект и передайте параметры как карту.

  • 0
    Я получаю TypeError: объект «Пользователь» не является подписным
22

Здесь быстрая и грязная альтернатива рассола json

import json

class User:
    def __init__(self, name, username):
        self.name = name
        self.username = username

    def to_json(self):
        return json.dumps(self.__dict__)

    @classmethod
    def from_json(cls, json_str):
        json_dict = json.loads(json_str)
        return cls(**json_dict)

# example usage
User("tbrown", "Tom Brown").to_json()
User.from_json(User("tbrown", "Tom Brown").to_json()).to_json()
  • 2
    Спасибо за предоставление такого простого решения для сериализации и десериализации json.
  • 0
    Это должно быть выбрано в качестве ответа.
14

Для сложных объектов вы можете использовать JSON Pickle

Библиотека Python для сериализации любого произвольного графа объектов в JSON. Он может взять практически любой объект Python и превратить его в JSON. Кроме того, он может восстановить объект обратно в Python.

  • 5
    Я думаю, что jsonstruct лучше. jsonstruct originally a fork of jsonpickle (Thanks guys!). The key difference between this library and jsonpickle is that during deserialization, jsonpickle requires Python types to be recorded as part of the JSON. This library intends to remove this requirement, instead, requires a class to be passed in as an argument so that its definition can be inspected. It will then return an instance of the given class. This approach is similar to how Jackson (of Java) works.
  • 2
    Проблемы с jsonstruct состоят в том, что он не поддерживается (на самом деле он выглядит заброшенным) и не может преобразовать список объектов, таких как '[{"name":"object1"},{"name":"object2"}]' . Jsonpickle тоже не очень хорошо с этим справляется.
Показать ещё 3 комментария
5

Я написал небольшую (де) сериализующую структуру под названием any2any, которая помогает выполнять сложные преобразования между двумя типами Python.

В вашем случае, я думаю, вы хотите преобразовать из словаря (полученного с помощью json.loads) в сложный объект response.education ; response.name с вложенной структурой response.education.id и т.д.... Так что именно для этой рамки. Документация пока невелика, но, используя any2any.simple.MappingToObject, вы сможете сделать это очень легко. Пожалуйста, спросите, нужна ли вам помощь.

  • 0
    Sebpiq, установил any2any и у меня возникли проблемы с пониманием предполагаемой последовательности вызовов методов. Не могли бы вы привести простой пример преобразования словаря в объект Python со свойством для каждого ключа?
  • 0
    Привет @ sansjoe! Если вы установили его из pypi, версия полностью устарела, я сделал полный рефакторинг несколько недель назад. Вы должны использовать версию GitHub (мне нужно сделать правильный релиз!)
Показать ещё 2 комментария
3

Если вы используете Python 3. 5+, вы можете использовать jsons для сериализации и десериализации простых старых объектов Python:

import jsons

response = request.POST

# You'll need your class attributes to match your dict keys, so in your case do:
response['id'] = response.pop('user_id')

# Then you can load that dict into your class:
user = jsons.load(response, FbApiUser)

user.save()

Вы также можете сделать FbApiUser наследовать от jsons.JsonSerializable для большей элегантности:

user = FbApiUser.from_json(response)

Эти примеры будут работать, если ваш класс состоит из типов Python по умолчанию, таких как строки, целые числа, списки, даты и т.д. Для jsons требуются подсказки типов для пользовательских типов.

2

Модификация ответа @DS немного, для загрузки из файла:

def _json_object_hook(d): return namedtuple('X', d.keys())(*d.values())
def load_data(file_name):
  with open(file_name, 'r') as file_data:
    return file_data.read().replace('\n', '')
def json2obj(file_name): return json.loads(load_data(file_name), object_hook=_json_object_hook)

Одно: это не может загружать элементы с числами впереди. Вот так:

{
  "1_first_item": {
    "A": "1",
    "B": "2"
  }
}

Потому что "1_first_item" не является допустимым именем поля python.

1

В поисках решения я наткнулся на этот пост в блоге: https://blog.mosthege.net/2016/11/12/json-deserialization-of-nested-objects/

Он использует ту же технику, что и в предыдущих ответах, но с использованием декораторов. Еще одна вещь, которая мне показалась полезной, это то, что она возвращает типизированный объект в конце десериализации

class JsonConvert(object):
    class_mappings = {}

    @classmethod
    def class_mapper(cls, d):
        for keys, cls in clsself.mappings.items():
            if keys.issuperset(d.keys()):   # are all required arguments present?
                return cls(**d)
        else:
            # Raise exception instead of silently returning None
            raise ValueError('Unable to find a matching class for object: {!s}'.format(d))

    @classmethod
    def complex_handler(cls, Obj):
        if hasattr(Obj, '__dict__'):
            return Obj.__dict__
        else:
            raise TypeError('Object of type %s with value of %s is not JSON serializable' % (type(Obj), repr(Obj)))

    @classmethod
    def register(cls, claz):
        clsself.mappings[frozenset(tuple([attr for attr,val in cls().__dict__.items()]))] = cls
        return cls

    @classmethod
    def to_json(cls, obj):
        return json.dumps(obj.__dict__, default=cls.complex_handler, indent=4)

    @classmethod
    def from_json(cls, json_str):
        return json.loads(json_str, object_hook=cls.class_mapper)

Использование:

@JsonConvert.register
class Employee(object):
    def __init__(self, Name:int=None, Age:int=None):
        self.Name = Name
        self.Age = Age
        return

@JsonConvert.register
class Company(object):
    def __init__(self, Name:str="", Employees:[Employee]=None):
        self.Name = Name
        self.Employees = [] if Employees is None else Employees
        return

company = Company("Contonso")
company.Employees.append(Employee("Werner", 38))
company.Employees.append(Employee("Mary"))

as_json = JsonConvert.to_json(company)
from_json = JsonConvert.from_json(as_json)
as_json_from_json = JsonConvert.to_json(from_json)

assert(as_json_from_json == as_json)

print(as_json_from_json)
1

Python3.x

Наилучшим подходом, которого я мог достичь с помощью моих знаний, было это.
Обратите внимание, что этот код также обрабатывает set().
Этот подход является общим, просто нуждающимся в расширении класса (во втором примере).
Обратите внимание, что я просто делаю это с файлами, но легко изменить поведение на свой вкус.

Однако это кодек.

Приложив немного больше работы, вы можете построить свой класс другими способами. Я предполагаю, что конструктор по умолчанию его создает, а затем обновляю класс dict.

import json
import collections


class JsonClassSerializable(json.JSONEncoder):

    REGISTERED_CLASS = {}

    def register(ctype):
        JsonClassSerializable.REGISTERED_CLASS[ctype.__name__] = ctype

    def default(self, obj):
        if isinstance(obj, collections.Set):
            return dict(_set_object=list(obj))
        if isinstance(obj, JsonClassSerializable):
            jclass = {}
            jclass["name"] = type(obj).__name__
            jclass["dict"] = obj.__dict__
            return dict(_class_object=jclass)
        else:
            return json.JSONEncoder.default(self, obj)

    def json_to_class(self, dct):
        if '_set_object' in dct:
            return set(dct['_set_object'])
        elif '_class_object' in dct:
            cclass = dct['_class_object']
            cclass_name = cclass["name"]
            if cclass_name not in self.REGISTERED_CLASS:
                raise RuntimeError(
                    "Class {} not registered in JSON Parser"
                    .format(cclass["name"])
                )
            instance = self.REGISTERED_CLASS[cclass_name]()
            instance.__dict__ = cclass["dict"]
            return instance
        return dct

    def encode_(self, file):
        with open(file, 'w') as outfile:
            json.dump(
                self.__dict__, outfile,
                cls=JsonClassSerializable,
                indent=4,
                sort_keys=True
            )

    def decode_(self, file):
        try:
            with open(file, 'r') as infile:
                self.__dict__ = json.load(
                    infile,
                    object_hook=self.json_to_class
                )
        except FileNotFoundError:
            print("Persistence load failed "
                  "'{}' do not exists".format(file)
                  )


class C(JsonClassSerializable):

    def __init__(self):
        self.mill = "s"


JsonClassSerializable.register(C)


class B(JsonClassSerializable):

    def __init__(self):
        self.a = 1230
        self.c = C()


JsonClassSerializable.register(B)


class A(JsonClassSerializable):

    def __init__(self):
        self.a = 1
        self.b = {1, 2}
        self.c = B()

JsonClassSerializable.register(A)

A().encode_("test")
b = A()
b.decode_("test")
print(b.a)
print(b.b)
print(b.c.a)

редактировать

Проведя дополнительные исследования, я нашел способ обобщения без необходимости вызова метода регистра SUPERCLASS с использованием метакласса.

import json
import collections

REGISTERED_CLASS = {}

class MetaSerializable(type):

    def __call__(cls, *args, **kwargs):
        if cls.__name__ not in REGISTERED_CLASS:
            REGISTERED_CLASS[cls.__name__] = cls
        return super(MetaSerializable, cls).__call__(*args, **kwargs)


class JsonClassSerializable(json.JSONEncoder, metaclass=MetaSerializable):

    def default(self, obj):
        if isinstance(obj, collections.Set):
            return dict(_set_object=list(obj))
        if isinstance(obj, JsonClassSerializable):
            jclass = {}
            jclass["name"] = type(obj).__name__
            jclass["dict"] = obj.__dict__
            return dict(_class_object=jclass)
        else:
            return json.JSONEncoder.default(self, obj)

    def json_to_class(self, dct):
        if '_set_object' in dct:
            return set(dct['_set_object'])
        elif '_class_object' in dct:
            cclass = dct['_class_object']
            cclass_name = cclass["name"]
            if cclass_name not in REGISTERED_CLASS:
                raise RuntimeError(
                    "Class {} not registered in JSON Parser"
                    .format(cclass["name"])
                )
            instance = REGISTERED_CLASS[cclass_name]()
            instance.__dict__ = cclass["dict"]
            return instance
        return dct

    def encode_(self, file):
        with open(file, 'w') as outfile:
            json.dump(
                self.__dict__, outfile,
                cls=JsonClassSerializable,
                indent=4,
                sort_keys=True
            )

    def decode_(self, file):
        try:
            with open(file, 'r') as infile:
                self.__dict__ = json.load(
                    infile,
                    object_hook=self.json_to_class
                )
        except FileNotFoundError:
            print("Persistence load failed "
                  "'{}' do not exists".format(file)
                  )


class C(JsonClassSerializable):

    def __init__(self):
        self.mill = "s"


class B(JsonClassSerializable):

    def __init__(self):
        self.a = 1230
        self.c = C()


class A(JsonClassSerializable):

    def __init__(self):
        self.a = 1
        self.b = {1, 2}
        self.c = B()


A().encode_("test")
b = A()
b.decode_("test")
print(b.a)
# 1
print(b.b)
# {1, 2}
print(b.c.a)
# 1230
print(b.c.c.mill)
# s
0

Развернув немного ответ DS, если вам нужно, чтобы объект был изменяемым (который не называется namedtuple), вы можете использовать библиотеку recordclass вместо namedtuple:

import json
from recordclass import recordclass

data = '{"name": "John Smith", "hometown": {"name": "New York", "id": 123}}'

# Parse into a mutable object
x = json.loads(data, object_hook=lambda d: recordclass('X', d.keys())(*d.values()))

Модифицированный объект может быть легко преобразован обратно в json с помощью simplejson:

x.name = "John Doe"
new_json = simplejson.dumps(x)
-4

Используйте json module (new в Python 2.6) или simplejson, который почти всегда установлен.

  • 2
    Привет, спасибо за ответ. Не могли бы вы опубликовать пример того, как декодировать JSON, а затем получить доступ к этим данным?
  • 0
    Эй, теперь вы поняли, но почему-то я предпочитаю обходиться без знания, а затем перепроектировать это: D.
Показать ещё 1 комментарий

Ещё вопросы

Сообщество Overcoder
Наверх
Меню