JSON и Python: есть контакт!
Описание объекта JavaScript (англ. JavaScript Object Notation, сокращенно JSON) представляет собой распространенный формат обмена данными между различными системами. Так, многие API возвращают результаты именно в этом формате. Поскольку он легко читается и обладает объектной структурой, было бы интересно узнать, как Python работает с данными JSON. В статье мы рассмотрим, в чем суть JSON и как его обрабатывать с помощью встроенного модуля json в Python.
Структура данных JSON
Данные JSON структурированы как объекты JSON, хранящие данные в виде пар ключ-значение, подобно словарям Python. Так выглядит типичный объект JSON:
По сути, объект JSON заключается в фигурные скобки, внутри которых хранятся пары ключ-значение. Ключи должны быть исключительно строками. Соблюдение данного требования обеспечивает стандартное взаимодействие между различными системами. Представленные значения включают строки и целые числа. При этом JSON также поддерживает и другие типы данных, такие как логическое значение, массивы и объекты.
- Строка (String): строковые литералы, заключенные в двойные кавычки;
- Число (Number): числовые литералы, включая целые и дробные;
- Логическое значение (Boolean): true или false ;
- Массив (Array): список поддерживаемых типов данных;
- Объект (Object): пары ключ-значение, заключенные в фигурные скобки;
- Null: пустое значение (null) для любого допустимого типа данных.
Следует отметить, что в отличие от строк Python, допускающих одинарные или двойные кавычки, строки JSON заключаются только в двойные. При неправильном применении одинарных кавычек данные JSON становятся недействительными и не подлежат обработке обычным парсером JSON.
Помимо вышеуказанных типов, JSON поддерживает вложенные структуры данных. Например, вы можете вложить объект JSON в другой объект. Кроме того, массив может состоять из любых поддерживаемых типов данных, включая объекты. Приведем примеры:
один объект находится внутри другого:
"one": 1,
"two":
>
массив состоит из нескольких объектов:
[
,
,
]
Смешанное использование различных типов данных предоставляет гибкость, позволяющую создавать очень сложные данные с четкой структурной информацией, поскольку все данные сохраняются в виде пар ключ-значение.
Сопоставление типов данных между JSON и Python
Будучи общим форматом обмена данными, типы данных JSON имеют соответствующие нативные структуры данных Python. Обратите внимание на двухсторонний принцип процесса: одно и то же правило (за рядом исключений) действует при преобразовании данных JSON в данные Python и наоборот.
+-----------+----------------+
| JSON | Python |
+-----------+----------------+
| String | str |
| Number | int or float |
| Boolean | bool |
| Array | list |
| Object | dict |
| Null | NoneType |
+-----------+----------------+
Эти преобразования не представляют затруднений, за исключением одного. У Python нет нативного типа данных, соответствующего числам в объектах JSON. Вместо этого для представления целых или вещественных чисел JSON задействуются int и float . Как вы могли заметить по таблице, в столбце данных Python отсутствуют кортежи (tuple) и множества (set). Примечательно, что кортеж преобразуется в массив, чего не скажешь о множестве.
Чтение строк JSON
Когда мы читаем и декодируем данные JSON в структуры данных других языков программирования, таких как Python, для дальнейшей обработки, мы говорим, что десериализуем данные JSON. Иными словами, процесс чтения и декодирования называется десериализацией. В стандартную библиотеку Python входит модуль json , который специализируется на десериализации данных JSON.
Как известно, веб-сервисы обычно используют объекты JSON в качестве ответов API. Допустим, вы получаете следующий ответ, который для простоты понимания представим в виде строкового объекта Python:
employee_json_data = """ "employee0": "firstName": "John",
"lastName": "Smith",
"age": 35,
"city": "San Francisco"
>,
"employee1": "firstName": "Zoe",
"lastName": "Thompson",
"age": 32,
"city": "Los Angeles"
>
>"""
Прочитаем эту строку JSON с помощью метода loads . Как показано ниже, после прочтения строки, содержащей вышеуказанный объект JSON, мы можем получить объект dict :
import json
employee_data = json.loads(employee_json_data)
print(employee_data)
# , 'employee1': >
Отметим гибкость метода loads . При наличии строки, представляющей список объектов JSON, этот метод самостоятельно определяет, как надлежит парсить данные. Обратимся к примеру:
employee_json_array = '[, ]' employee_list = json.loads(employee_json_array) print(employee_list) # [, ]
Помимо этих структурированных объектов JSON, метод loads способен парсить любые отличные от объектов типы данных JSON. Приведем примеры:
>>> json.loads("2.2") 2.2 >>> json.loads('"A string"') 'A string' >>> json.loads('false') False >>> json.loads('null') is None True
Чтение файлов JSON
В предыдущем разделе были затронуты различные аспекты, касающиеся десериализации строк JSON. Однако взаимодействовать приходится не только со строками. Иногда выпадает возможность поработать и с файлами JSON. Допустим, вы выполняете следующий код для создания файла, содержащего строки JSON:
# данные JSON,которые нужно сохранить json_to_write='' # запись данных JSON в файл with open("json_test.txt", "w") as file: file.write(json_to_write)
Конечно же, мы можем прочитать файл напрямую, чтобы создать строку и отправить ее в метод loads :
with open(“json_test.txt”) as file: json_string = file.read() parsed_json0 = json.loads(json_string) print(parsed_json0) # Вывод:
Примечательно, что модуль json предоставляет метод load , позволяющий работать напрямую с файлом для парсинга данных JSON:
with open(“json_test.txt”) as file: parsed_json1 = json.load(file) print(parsed_json1) # Вывод:
Данный способ несомненно более понятный, чем предыдущая реализация, поскольку он избавляет от необходимости создавать промежуточный строковый объект.
Итак, мы рассмотрели самые основные сценарии использования методов load и loads . Следует отметить, что парсинг данных JSON осуществляется посредством класса JSONDecoder . Несмотря на эффективность этого базового класса в решении большинства задач, мы можем определить более настраиваемое поведение путем создания подкласса класса JSONDecoder . Однако если вы намерены обойтись без подклассов, то методы load и loads предоставят другие параметры, с помощью которых вы сможете определить настраиваемое поведение парсинга. Удовлетворить любопытство и ознакомиться с дополнительной информацией можно в официальной документации.
Запись данных Python в формат JSON
По аналогии с чтением данных JSON запись данных Python в формат JSON включает два соответствующих метода, а именно dump и dumps . В противоположность десериализации процесс создания данных JSON называется сериализацией. Таким образом, когда мы преобразуем данные Python в данные JSON, мы говорим, что сериализуем объекты Python в данные JSON.
Подобно load и loads методы dump и dumps имеют почти идентичные сигнатуры вызовов. Главное отличие состоит в том, что метод dump записывает данные в файл JSON, тогда как dumps — в строку JSON. Для простоты остановимся только на методе dumps . Рассмотрим пример:
import json different_data = ['text', False, ] json.dumps(different_data) # Вывод: '["text", false, ]'
Как видно, метод dumps создает массив JSON, содержащий различные типы данных JSON. Особое внимание обращает на себя следующий факт: хотя исходный объект list использует нативные структуры данных Python, сгенерированная строка JSON содержит преобразованные структуры данных JSON. В соответствии с ранее изученной таблицей отметим следующие преобразования:
- Строка ‘text’ в одинарных кавычках теперь заключена в двойные — “text” .
- Логический объект Python False становится false .
- Объект None превращается в null .
- Поскольку ключами JSON могут быть только строки, число 1 автоматически преобразуется в его строковый аналог “1” .
Помимо автоматических преобразований мы часто задействуем две важные функциональности. Первая предназначена для создания объектов JSON в более читаемом формате посредством правильной установки отступов. Для этого в методе dumps задается параметр indent :
employee_data = [, ]
print(json.dumps(employee_data, indent=2))
# Вывод:
[
"name": "John",
"age": 35,
"city": "San Francisco"
>,
"name": "Zoe",
"age": 34,
"city": "Los Angeles"
>
]
Как показано выше, каждый уровень четко оформлен при помощи отступов, чтобы обозначить взаимосвязанную структуру объектов JSON и их пар ключ-значение.
Вторая полезная функциональность — указание параметра sort_keys . Установив его в значение True , мы создаем строки JSON, ключи которых отсортированы в алфавитном порядке. Этот прием упрощает поиск информации, особенно при наличии нескольких элементов. Обратимся к примеру:
employee_info =
print(json.dumps(employee_info, indent=2, sort_keys=True))
# output:
"age": 35,
"city": "San Francisco",
"home": "123 Main St.",
"name": "John",
"sex": "Male",
"zip_code": 12345
>
Теперь мы знаем, что методы load и loads применяются для десериализации, а dump и dumps — для сериализации. Во избежание недопонимания поясним, почему методы получили именно такие названия:
- Данные JSON являются внешними по отношению к Python. При необходимости получить к ним доступ нужно “загрузить” (“load”) их в Python. Следовательно, загрузка (loading) подразумевает чтение данных JSON.
- И наоборот, для экспорта данных Python в данные JSON мы “разгружаем” (“dump”) данные. Следовательно, под разгрузкой (dumping) имеется в виду запись данных JSON.
- Если входные или выходные данные JSON являются строками, то они обозначаются буквой “s”. Поэтому мы присоединяем “s” к методу load . Таким же образом, если нам необходимы строки JSON, мы добавляем “s” к названию метода dump .
Запись пользовательских экземпляров в данные JSON
Под прицелом нашего внимания — встроенные структуры данных Python. Во многих приложениях вы определяете собственные пользовательские классы при необходимости сериализации этих пользовательских объектов экземпляров в данные JSON. Рассмотрим следующий класс, из которого создадим экземпляр:
class Employee: def __init__(self, name, employee_id): self.name = name self.employee_id = employee_id employee = Employee("John Smith", 40)
Что произойдет, если мы попробуем вызвать dumps для employee ? Получим ли мы успешный результат? Проверяем:
json.dumps(employee)
# TypeError: Object of type Employee is not JSON serializable
Не сработало. Причина неудачи в том, что метод dumps пытается создать корректную строку JSON. Однако в случае с экземпляром пользовательского класса он не знает, какие данные подлежат кодированию. Один из вариантов решения — создать собственный класс JSONEncoder . Однако есть более быстрый способ: мы предоставляем методу dumps инструкции по кодированию, устанавливая аргумент default :
>>> json.dumps(employee, default=lambda x: x.__dict__)
''
Здесь указывается лямбда-функцию, которая извлекает представление dict экземпляра через доступ к специальному атрибуту __dict__ . Нам известно, что встроенный объект dict сериализуется в JSON, поэтому dumps “разгружает” объект dict .
Заключение
В статье были рассмотрены основные способы обработки данных JSON в Python. Сформулируем главные выводы:
- Данные JSON — это стандартный формат обмена данными. При создании API для всеобщего пользования JSON рекомендуется в качестве возможного формата для данных ответа.
- У Python есть отдельные методы для работы со строками и файлами JSON. Эти методы обладают похожими сигнатурами вызовов.
- Правильно устанавливайте отступы для улучшения читаемости данных JSON, особенно при создании соответствующей строки. Просто укажите параметр indent при сериализации объектов
- При наличии нескольких пар ключ-значение для объектов JSON рекомендуется сортировать ключи, тем самым упрощая поиск информации.
- Ключи JSON должны быть строками в двойных кавычках.
- Для сериализации пользовательского экземпляра необходимо предоставить конкретные инструкции.