JSON и Python: есть контакт!

Описание объекта JavaScript (англ. JavaScript Object Notation, сокращенно JSON) представляет собой распространенный формат обмена данными между различными системами. Так, многие API возвращают результаты именно в этом формате. Поскольку он легко читается и обладает объектной структурой, было бы интересно узнать, как Python работает с данными JSON. В статье мы рассмотрим, в чем суть JSON и как его обрабатывать с помощью встроенного модуля json в Python.

Структура данных JSON

Данные JSON структурированы как объекты JSON, хранящие данные в виде пар ключ-значение, подобно словарям Python. Так выглядит типичный объект JSON: 

{  
"firstName": "John",  
"lastName": "Smith",  
"age": 35,  
"city": "San Francisco"
}

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

  • Строка (String): строковые литералы, заключенные в двойные кавычки; 
  • Число (Number): числовые литералы, включая целые и дробные; 
  • Логическое значение (Boolean): true или false;
  • Массив (Array): список поддерживаемых типов данных; 
  • Объект (Object): пары ключ-значение, заключенные в фигурные скобки; 
  • Null: пустое значение (null) для любого допустимого типа данных. 

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

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

один объект находится внутри другого:
{
"one": 1,
"two": {"one": 1}
}
массив состоит из нескольких объектов:
[
{"one": 1},
{"two": 2},
{"three": 3}
]

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

Сопоставление типов данных между 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)

# {'employee0': {'firstName': 'John', 'lastName': 'Smith', 'age': 35, 'city': 'San Francisco'}, 'employee1': {'firstName': 'Zoe', 'lastName': 'Thompson', 'age': 32, 'city': 'Los Angeles'}}

Отметим гибкость метода loads. При наличии строки, представляющей список объектов JSON, этот метод самостоятельно определяет, как надлежит парсить данные. Обратимся к примеру: 

employee_json_array = '[{"employee2": "data"}, {"employee3": "data"}]'

employee_list = json.loads(employee_json_array)
print(employee_list)

# [{'employee2': 'data'}, {'employee3': 'data'}]

Помимо этих структурированных объектов 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='{"name": "John", "age": 35}'

# запись данных 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)

# Вывод: {'name': 'John', 'age': 35}

Примечательно, что модуль json предоставляет метод load, позволяющий работать напрямую с файлом для парсинга данных JSON:

with open(“json_test.txt”) as file:
    parsed_json1 = json.load(file)
    print(parsed_json1)

# Вывод: {‘name’: ‘John’, ‘age’: 35}

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

Итак, мы рассмотрели самые основные сценарии использования методов 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, {"0": None, 1: [1.0, 2.0]}]

json.dumps(different_data)

# Вывод: '["text", false, {"0": null, "1": [1.0, 2.0]}]'

Как видно, метод dumps создает массив JSON, содержащий различные типы данных JSON. Особое внимание обращает на себя следующий факт: хотя исходный объект list использует нативные структуры данных Python, сгенерированная строка JSON содержит преобразованные структуры данных JSON. В соответствии с ранее изученной таблицей отметим следующие преобразования: 

  • Строка ‘text’ в одинарных кавычках теперь заключена в двойные  —  “text”
  • Логический объект Python False становится false
  • Объект None превращается в null.
  • Поскольку ключами JSON могут быть только строки, число 1 автоматически преобразуется в его строковый аналог “1”.

Помимо автоматических преобразований мы часто задействуем две важные функциональности. Первая предназначена для создания объектов JSON в более читаемом формате посредством правильной установки отступов. Для этого в методе dumps задается параметр indent

employee_data = [{"name": "John", "age": 35, "city": "San Francisco"}, {"name": "Zoe", "age": 34, "city": "Los Angeles"}]

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 = {"name": "John", "age": 35, "city": "San Francisco", "home": "123 Main St.", "zip_code": 12345, "sex": "Male"}

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__)
'{"name": "John Smith", "employee_id": 40}'

Здесь указывается лямбда-функцию, которая извлекает представление dict экземпляра через доступ к специальному атрибуту __dict__. Нам известно, что встроенный объект dict сериализуется в JSON, поэтому dumps “разгружает” объект dict

Заключение 

В статье были рассмотрены основные способы обработки данных JSON в Python. Сформулируем главные выводы: 

  1. Данные JSON  —  это стандартный формат обмена данными. При создании API для всеобщего пользования JSON рекомендуется в качестве возможного формата для данных ответа. 
  2. У Python есть отдельные методы для работы со строками и файлами JSON. Эти методы обладают похожими сигнатурами вызовов. 
  3. Правильно устанавливайте отступы для улучшения читаемости данных JSON, особенно при создании соответствующей строки. Просто укажите параметр indent при сериализации объектов 
  4. При наличии нескольких пар ключ-значение для объектов JSON рекомендуется сортировать ключи, тем самым упрощая поиск информации. 
  5. Ключи JSON должны быть строками в двойных кавычках. 
  6. Для сериализации пользовательского экземпляра необходимо предоставить конкретные инструкции. 

Читайте также:

Читайте нас в TelegramVK и Яндекс.Дзен


Перевод статьи Yong Cui: How to Make JSON and Python Talk to Each Other

Предыдущая статьяКак использовать JavaScript для расстановки элементов на веб-странице
Следующая статья2 инструмента для автоматизации тестирования производительности на стороне клиента