Работа с данными JSON в Python

1 июня, 2020

Подписывайся на наш канал в Telegram, чтобы ежедневно совершенствоваться в Python - перейти

Оглавление

  • Краткая история JSON
  • Смотри, это JSON!
  • Python поддерживает JSON изначально!
    • Немного словарного запаса
    • Сериализация JSON
    • Простой пример сериализации
    • Некоторые полезные аргументы ключевого слова
    • Десериализация JSON
    • Простой пример десериализации
  • Пример из реального мира
  • Кодирование и декодирование пользовательских объектов Python
    • Упрощение структур данных
    • Кодирование пользовательских типов
    • Расшифровка пользовательских типов
  • Все сделано!

С самого начала JSON стал стандартом фактически для обмена информацией. Скорее всего, вы здесь, потому что вам нужно перенести некоторые данные из одного места в другое. Возможно, вы собираете информацию через API или храните свои данные в базе данных документов . Так или иначе, вы по уши в JSON и у вас есть выход на Python.

К счастью, это довольно распространенная задача, и, как и в случае большинства обычных задач, Python делает ее почти максимально легкой.

Итак, мы используем JSON для хранения и обмена данными? Да, ! Это не более чем стандартный формат, который сообщество использует для передачи данных. Имейте в виду, JSON – не единственный формат, доступный для такого рода работы, но есть XML и YAML , вероятно, единственные, которых стоит упомянуть в одном ряду.

Краткая история JSON

Не так удивительно,J avaS cript O bject N otation был вдохновлен подмножеством языка программирования JavaScript, имеющим дело с литеральным синтаксисом объекта. У них есть отличный сайт, который объясняет все это. Не беспокойтесь: JSON уже давно стал независимым от языка и существует как собственный стандарт, поэтому, к счастью, мы можем избежать использование JavaScript.

В конечном счете, сообщество в целом приняло JSON, потому что людям и машинам легко создавать и понимать.

Смотри, это JSON!

Приготовьтесь, мы покажем вам немного реальной жизни JSON – точно так же, как вы видели бы там в дикой природе. Это нормально: JSON должен быть доступен для чтения любому, кто использовал язык C-стиля, а Python таковым является!

{
    "firstName": "Jane",
    "lastName": "Doe",
    "hobbies": ["running", "sky diving", "singing"],
    "age": 35,
    "children": [
        {
            "firstName": "Alice",
            "age": 6
        },
        {
            "firstName": "Bob",
            "age": 8
        }
    ]
}

Как видите, JSON поддерживает примитивные типы, такие как строки и числа, а также вложенные списки и объекты.

Подождите, это похоже на словарь Python! Ведь да? На данный момент это довольно универсальная запись объектов, но я не думаю, что UON скатывается с языка так же хорошо.

Уф! Вы пережили свою первую встречу с каким-то диким JSON. Теперь вам просто нужно научиться приручать его.

Python поддерживает JSON изначально!

Python поставляется со встроенным пакетом json, предназначенным  для кодирования и декодирования данных JSON.

Просто подбросьте этого маленького парня вверху вашего файла:

import json

Немного словарного запаса

Процесс кодирования JSON обычно называется сериализацией . Этот термин относится к преобразованию данных в серию байтов (следовательно, последовательных ) для хранения или передачи по сети. Вы также можете услышать термин маршалинг , но это совсем другое. Естественно, десериализация – это взаимный процесс декодирования данных, которые были сохранены или доставлены в стандарте JSON.

Хлоп! Это звучит довольно технически. Определенно. Но на самом деле все, о чем мы здесь говорим – это чтение и письмо . Думайте об этом так: кодирование предназначено для записи данных на диск, а декодирование – для чтения данных в память.

Сериализация JSON

Что происходит после того, как компьютер обрабатывает много информации? Нужно взять дамп данных. Соответственно, json библиотека предоставляет dump() способ записи данных в файлы. Существует также dumps() метод (произносится как «dump-s») для записи в строку Python.

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

PythonJSON
dictobject
listtuplearray
strstring
intlong,floatnumber
Truetrue
Falsefalse
Nonenull

Простой пример сериализации

Представьте, что вы работаете с объектом Python в памяти, который выглядит примерно так:

data = {
    "president": {
        "name": "Zaphod Beeblebrox",
        "species": "Betelgeusian"
    }
}

Крайне важно сохранить эту информацию на диске, поэтому ваша задача – записать ее в файл.

Используя менеджер контекста Python, вы можете создать файл с именем data_file.json и открыть его в режиме записи. (Файлы JSON заканчиваются .json расширением.)

with open("data_file.json", "w") as write_file:
    json.dump(data, write_file)

Обратите внимание, что dump() принимает два позиционных аргумента: (1) объект данных для сериализации и (2) файлоподобный объект, в который будут записаны байты.

Или, если вы склонны продолжать использовать эти сериализованные данные JSON в своей программе, вы можете записать их в собственный str объект Python .

json_string = json.dumps(data)

Обратите внимание, что файлоподобный объект отсутствует, поскольку вы фактически не записываете на диск. Кроме этого, dumps() аналогичен dump().

Ура! Вы родили маленького мальчика JSON, и вы готовы выпустить его в дикую природу, чтобы он стал большим и сильным.

Некоторые полезные аргументы ключевого слова

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

ПРИМЕЧАНИЕ. Оба метода и используют dump() и dumps() одни и те же ключевые аргументы.

Первый вариант, который большинство людей хотят изменить, – это пробел. Вы можете использовать ключевое слово indent , чтобы указать размер отступа для вложенных структур. Проверьте разницу, используя data, который мы определили выше, запустив в консоли следующие команды:

>>> json.dumps(data)
>>> json.dumps(data, indent=4)

Другим вариантом форматирования является ключевое слово separators . По умолчанию это две строки разделителей (", ", ": "), но распространенной альтернативой для компактного JSON является (",", ":"). Посмотрите на образец JSON еще раз, чтобы увидеть, где эти разделители вступают в игру.

Есть и другие, sort_keys но я понятия не имею, что этот человек делает. Вы можете найти весь список в документах, если вам интересно.

Десериализация JSON

Теперь пришло время привести его в форму. В json библиотеке вы найдете load() и loads() для преобразования закодированных данных JSON в объекты Python.

Как и в случае с сериализацией, для десериализации существует простая таблица преобразования, хотя вы, вероятно, уже догадались, как она выглядит.

JSONPython
objectdict
arraylist
stringstr
number (целый)int
number (действительный)float
trueTrue
falseFalse
nullNone

Технически, это преобразование не является совершенной противоположностью таблице сериализации. По сути, это означает, что если вы кодируете объект сейчас, а потом декодируете его позже, вы не сможете получить точно такой же объект обратно. Я полагаю, это немного похоже на телепортацию: разбейте мои молекулы здесь и соедините их там. Я все еще тот же человек?

В действительности, это скорее похоже на то, как один друг переводит что-то на японский, а другой – обратно на русский. В любом случае, простейшим примером будет кодирование tuple и возврат list после декодирования, например так:

>>> blackjack_hand = (8, "Q")
>>> encoded_hand = json.dumps(blackjack_hand)
>>> decoded_hand = json.loads(encoded_hand)

>>> blackjack_hand == decoded_hand
False
>>> type(blackjack_hand)
<class 'tuple'>
>>> type(decoded_hand)
<class 'list'>
>>> blackjack_hand == tuple(decoded_hand)
True

Простой пример десериализации

На этот раз представьте, что на диске хранятся некоторые данные, которыми вы хотите манипулировать в памяти. Вы по-прежнему будете использовать менеджер контекста, но на этот раз вы откроете существующий data_file.json в режиме чтения.

with open("data_file.json", "r") as read_file:
    data = json.load(read_file)

Здесь все довольно просто, но имейте в виду, что результат этого метода может вернуть любой из разрешенных типов данных из таблицы преобразования. Это важно, только если вы загружаете данные, которые раньше не видели. В большинстве случаев корневым объектом будет dict или list.

Если вы извлекли данные JSON из другой программы или иным образом получили строку данных в формате JSON в Python, вы можете легко десериализовать их с помощью loads(), которая естественным образом загружается из строки:

json_string = """
{
    "researcher": {
        "name": "Ford Prefect",
        "species": "Betelgeusian",
        "relatives": [
            {
                "name": "Zaphod Beeblebrox",
                "species": "Betelgeusian"
            }
        ]
    }
}
"""
data = json.loads(json_string)

Вуаля!

Пример из реального мира (sort of)

В качестве вводного примера вы будете использовать JSONPlaceholder, отличный источник поддельных данных JSON для практических целей.

Сначала создайте файл сценария с именем scratch.py или по желанию. 

Вам нужно будет сделать запрос API к службе JSONPlaceholder, поэтому просто используйте requests пакет для выполнения тяжелой работы. Добавьте этот импорт вверху вашего файла:

import json
import requests

Теперь вы будете работать со списком TODO, потому что … вы знаете, это обряд или что-то в этом роде.

Продолжайте и сделайте запрос к API JSONPlaceholder для конечной точки /todos . Если вы не знакомы с requests, на самом деле есть удобный json() метод, который сделает всю работу за вас, но вы можете попрактиковаться в использовании json библиотеки для десериализации text атрибута ответа объекта . Это должно выглядеть примерно так:

response = requests.get("https://jsonplaceholder.typicode.com/todos")
todos = json.loads(response.text)

Вы не верите, что это работает? Хорошо, запустите файл в интерактивном режиме и проверьте его сами. Проверьте тип todos. Если вы чувствуете себя авантюрным, взгляните на первые 10 или около того пунктов в списке.

>>> todos == response.json()
True
>>> type(todos)
<class 'list'>
>>> todos[:10]
...

Что такое интерактивный режим? Вы знаете, как вы всегда прыгаете вперед и назад между вашим редактором и терминалом? Что ж, мы, подлые питонисты, используем интерактивный флаг -i , когда запускаем скрипт. Это отличный маленький трюк для тестирования кода, потому что он запускает скрипт, а затем открывает интерактивную командную строку с доступом ко всем данным из скрипта!

Хорошо, время для некоторых действий. Вы можете увидеть структуру данных, посетив конечную точку в браузере, но вот пример TODO:

{
    "userId": 1,
    "id": 1,
    "title": "delectus aut autem",
    "completed": false
}

Есть несколько пользователей, каждый с уникальным userId, и каждая задача имеет логическое completed свойство. Можете ли вы определить, какие пользователи выполнили больше всего задач?

# Map of userId to number of complete TODOs for that user
todos_by_user = {}

# Increment complete TODOs count for each user.
for todo in todos:
    if todo["completed"]:
        try:
            # Increment the existing user's count.
            todos_by_user[todo["userId"]] += 1
        except KeyError:
            # This user has not been seen. Set their count to 1.
            todos_by_user[todo["userId"]] = 1

# Create a sorted list of (userId, num_complete) pairs.
top_users = sorted(todos_by_user.items(), 
                   key=lambda x: x[1], reverse=True)

# Get the maximum number of complete TODOs.
max_complete = top_users[0][1]

# Create a list of all users who have completed
# the maximum number of TODOs.
users = []
for user, num_complete in top_users:
    if num_complete < max_complete:
        break
    users.append(str(user))

max_users = " and ".join(users)

Да, ваша реализация лучше, но суть в том, что теперь вы можете манипулировать данными JSON как обычным объектом Python!

Я не знаю о вас, но когда я снова запускаю скрипт в интерактивном режиме, я получаю следующие результаты:

>>> s = "s" if len(users) > 1 else ""
>>> print(f"user{s} {max_users} completed {max_complete} TODOs")
users 5 and 10 completed 12 TODOs

Это круто и все, но вы здесь, чтобы узнать о JSON. Для окончательного задания вы создадите файл JSON, который содержит заполненные TODO для каждого из пользователей, выполнивших максимальное количество TODO.

Все, что вам нужно сделать, это отфильтровать todos и записать полученный список в файл. Ради оригинальности вы можете вызвать выходной файл filtered_data_file.json. Есть несколько способов, которыми вы могли бы пойти по этому поводу, но вот один:

# Define a function to filter out completed TODOs 
# of users with max completed TODOS.
def keep(todo):
    is_complete = todo["completed"]
    has_max_count = str(todo["userId"]) in users
    return is_complete and has_max_count

# Write filtered TODOs to file.
with open("filtered_data_file.json", "w") as data_file:
    filtered_todos = list(filter(keep, todos))
    json.dump(filtered_todos, data_file, indent=2)

Отлично, вы избавились от всех данных, которые вам не нужны, и сохранили полезные вещи в совершенно новый файл! Запустите скрипт еще раз и проверьте, filtered_data_file.json все ли работает. Он будет в том же каталоге, что и scratch.py при запуске.

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

Кодирование и декодирование пользовательских объектов Python

Что происходит, когда мы пытаемся сериализовать Elf класс из того приложения Dungeons & Dragons, над которым вы работаете?

class Elf:
    def __init__(self, level, ability_scores=None):
        self.level = level
        self.ability_scores = {
            "str": 11, "dex": 12, "con": 10,
            "int": 16, "wis": 14, "cha": 13
        } if ability_scores is None else ability_scores
        self.hp = 10 + self.ability_scores["con"]

Неудивительно, что Python жалуется, что Elf не сериализуем (что вы бы знали, если бы когда-нибудь попытались написать Elf иначе):

>>> elf = Elf(level=4)
>>> json.dumps(elf)
TypeError: Object of type 'Elf' is not JSON serializable

Хотя json модуль может обрабатывать большинство встроенных типов Python, он не понимает, как кодировать настраиваемые типы данных по умолчанию. Это все равно что пытаться втиснуть квадратный колышек в круглое отверстие.

Упрощение структур данных

Теперь вопрос в том, как бороться с более сложными структурами данных. Ну, вы можете попытаться кодировать и декодировать JSON вручную, но есть немного более умное решение, которое сэкономит вам немного работы. Вместо прямого перехода от пользовательского типа данных к JSON вы можете добавить промежуточный шаг.

Все, что вам нужно сделать, это представить ваши данные в терминах json уже понятных встроенных типов . По сути, вы переводите более сложный объект в более простое представление, которое json модуль затем переводит в JSON. Это как переходное свойство в математике: если A = B и B = C, то A = C.

Чтобы освоить это, вам понадобится сложный объект. Вы можете использовать любой пользовательский класс, который вам нравится, но в Python есть встроенный тип complex, который вызывается для представления комплексных чисел, и он по умолчанию не сериализуем. Итак, ради этих примеров, ваш сложный объект будет complex объектом:

>>> z = 3 + 8j
>>> type(z)
<class 'complex'>
>>> json.dumps(z)
TypeError: Object of type 'complex' is not JSON serializable

Откуда берутся комплексные числа? Видите ли, когда действительное число и воображаемое число очень любят друг друга, они складываются вместе, образуя число, которое (оправданно) называется сложным .

Хороший вопрос, который нужно задать себе при работе с пользовательскими типами: какой минимальный объем информации необходим для воссоздания этого объекта? В случае комплексных чисел вам нужно знать только действительные и мнимые части, к которым вы можете обращаться как к атрибутам complexобъекта:

>>> z.real
3.0
>>> z.imag
8.0

Передача одинаковых чисел в complex конструктор достаточна для выполнения __eq__ оператора сравнения:>>>

>>> complex(3, 8) == z
True

Разбиение пользовательских типов данных на их основные компоненты имеет решающее значение как для процессов сериализации, так и десериализации.

Кодирование пользовательских типов

Чтобы перевести пользовательский объект в JSON, все, что вам нужно сделать, это предоставить функцию кодирования для параметра метода  dump() default.  json модуль будет вызывать эту функцию на любых объектах, которые изначально не сериализованы. Вот простая функция декодирования, которую вы можете использовать на практике:

def encode_complex(z):
    if isinstance(z, complex):
        return (z.real, z.imag)
    else:
        type_name = z.__class__.__name__
        raise TypeError(f"Object of type '{type_name}' is not JSON serializable")

Обратите внимание, что вы должны поднять TypeError если вы не получите объект, который вы ожидали. Таким образом, вы избежите случайной сериализации любых Elf. Теперь вы можете попробовать кодировать сложные объекты!

>>> json.dumps(9 + 5j, default=encode_complex)
'[9.0, 5.0]'
>>> json.dumps(elf, default=encode_complex)
TypeError: Object of type 'Elf' is not JSON serializable

Почему мы закодировали комплексное число как tupleОтличный вопрос! Это, конечно, не был единственный выбор, и при этом это не обязательно лучший выбор. На самом деле, это не очень хорошее представление, если вы когда-нибудь захотите декодировать объект позже, как вы вскоре увидите.

Другой распространенный подход – создать подкласс стандарта JSONEncoder и переопределить его default() метод:

class ComplexEncoder(json.JSONEncoder):
    def default(self, z):
        if isinstance(z, complex):
            return (z.real, z.imag)
        else:
            return super().default(z)

Вместо того, чтобы поднимать TypeErrorсебя, вы можете просто позволить базовому классу справиться с этим. Вы можете использовать это либо непосредственно в dump()методе через clsпараметр, либо создав экземпляр кодировщика и вызвав его encode()метод:>>>

>>> json.dumps(2 + 5j, cls=ComplexEncoder)
'[2.0, 5.0]'

>>> encoder = ComplexEncoder()
>>> encoder.encode(3 + 6j)
'[3.0, 6.0]'

Расшифровка пользовательских типов

Хотя действительные и мнимые части комплексного числа абсолютно необходимы, на самом деле их недостаточно для воссоздания объекта. Вот что происходит, когда вы пытаетесь кодировать комплексное число с помощью ComplexEncoder а затем декодировать результат:

>>> complex_json = json.dumps(4 + 17j, cls=ComplexEncoder)
>>> json.loads(complex_json)
[4.0, 17.0]

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

Я полагаю, что вопрос, который вы действительно должны задать себе: каков минимальный объем информации, который необходим и достаточен для воссоздания этого объекта?

json модуль ожидает, что все пользовательские типы должны быть выражены как objects в стандарте JSON. Для разнообразия вы можете создать файл JSON на этот раз complex_data.json и добавить следующее object представляющее комплексное число:

{
    "__complex__": true,
    "real": 42,
    "imag": 36
}

Видите умный бит? Этот "__complex__" ключ – метаданные, о которых мы только что говорили. На самом деле не имеет значения, что это за значение. Чтобы заставить этот маленький взломать работать, все, что вам нужно сделать, это убедиться, что ключ существует:

def decode_complex(dct):
    if "__complex__" in dct:
        return complex(dct["real"], dct["imag"])
    return dct

Если "__complex__" нет в словаре, вы можете просто вернуть объект и позволить декодеру по умолчанию справиться с ним.

Каждый раз, когда load() метод пытается выполнить синтаксический анализ object, вам предоставляется возможность вмешаться до того, как декодер по умолчанию доберется до данных. Вы можете сделать это, передав функцию декодирования параметру object_hook.

Теперь то же, что и раньше:

>>> with open("complex_data.json") as complex_data:
...     data = complex_data.read()
...     z = json.loads(data, object_hook=decode_complex)
... 
>>> type(z)
<class 'complex'>

Хотя object_hook может показаться аналогом параметра dump() метода default, аналогия действительно начинается и заканчивается там.

Это не просто работает с одним объектом. Попробуйте вставить этот список комплексных чисел complex_data.json и снова запустить скрипт:

[
  {
    "__complex__":true,
    "real":42,
    "imag":36
  },
  {
    "__complex__":true,
    "real":64,
    "imag":11
  }
]

Если все пойдет хорошо, вы получите список complex объектов:

>>> with open("complex_data.json") as complex_data:
...     data = complex_data.read()
...     numbers = json.loads(data, object_hook=decode_complex)
... 
>>> numbers
[(42+36j), (64+11j)]

Вы также можете попробовать создать подкласс JSONDecoder и переопределить object_hook, но лучше по возможности придерживаться облегченного решения.

Все сделано!

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

Хотя примеры, с которыми вы работали здесь, безусловно, надуманы и чрезмерно упрощены, они иллюстрируют рабочий процесс, который вы можете применить к более общим задачам:

  1. Импортируйте json пакет.
  2. Прочитайте данные с помощью load() или loads().
  3. Обработайте данные.
  4. Запишите измененные данные с помощью dump() или dumps().

То, что вы будете делать со своими данными после их загрузки в память, будет зависеть от вашего варианта использования. Как правило, ваша цель будет собирать данные из источника, извлекать полезную информацию и передавать эту информацию вместе или вести ее учет.


Совершенствуй знания каждый день у нас в Телеграм-каналах

Вопросы, реклама — VK | Telegram