Сразу после появления, JSON быстро стал де факто стандартом обмена информации. Вероятно вы здесь из-за того, что вы хотите переместить данные из одного места в другое. Возможно вы получаете данные через API, или храните их в документной базе данных. Так или иначе, вы заинтересовались JSON, и вам нужно пользоваться им через Python.
Содержание
- Подробнее про JSON
- Структура JSON
- Python поддерживает JSON
- Небольшой словарь
- Сериализация JSON
- Пример сериализации JSON Python
- Несколько полезных аргументов
- Десериализация JSON
- Пример десериализации JSON Python
- Пример работы с JSON Python
- Кодирование и декодирование объектов Python
- Упрощение структур данных
- Кодирование пользовательских типов
- Декодирование пользовательских типов
- Готово!
К счастью, это достаточно тривиальная задача, и как и с большинством тривиальных задач, Python делает все до омерзения простым.
Итак, используем ли мы JSON для хранения и обмена данными? Именно так. Это не более, чем стандартизированный формат, который используется сообществом для передачи данных. Помните, что JSON не является единственным доступным форматом для такой работы, XML и YAML наверное, единственные альтернативные способы, которые стоит упомянуть.
Подробнее про JSON
Не удивительно, что JavaScript Object Notation был вдохновен подмножеством языка программирования JavaScript, связанным с синтаксисом объектного литерала. У них есть отличный сайт, в котором все прекрасно объясняется. Не переживайте: JSON уже давно стал агностиком языка и существует как отдельный стандарт, по этому мы можем убрать JavaScript из этой дискуссии.
Есть вопросы по Python?
На нашем форуме вы можете задать любой вопрос и получить ответ от всего нашего сообщества!
Паблик VK
Одно из самых больших сообществ по Python в социальной сети ВК. Видео уроки и книги для вас!
В конечном счете, большая часть сообщества приняла JSON благодаря его простоте как для людей, так и для машин.
Смотрите, это JSON!
Структура JSON
Готовьтесь. Я собираюсь показать реальный пример JSON— такой же, какой вы встретите в реальной жизни. Это нормально, подразумевается что JSON является читаемым для любого, кто пользовался С-языками, а Python – это С-язык, так что мы говорим о вас!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
{ "firstName": "Jane", "lastName": "Doe", "hobbies": ["running", "sky diving", "singing"], "age": 35, "children": [ { "firstName": "Alice", "age": 6 }, { "firstName": "Bob", "age": 8 } ] } |
Как видите, JSON поддерживает примитивные типы, такие как строки python и числа, а также вложенные списки и объекты.
Погодите, это выглядит как словарь Python, верно? На данный момент это достаточно универсальная нотация объектов, и не думаю что UON может так же легко отскакивать от зубов. Кстати, предлагайте альтернативы в комментариях!
НУ что же, вы пережили первый контакт с диким JSON. Теперь вам нужно научиться приручать его!
Python поддерживает JSON
Python содержит встроенный модуль под названием json для кодирования и декодирования данных JSON.
Просто импортируйте модуль в начале вашего файла:
1 |
import json |
Небольшой словарь
Как правило, процесс кодирования JSON называется сериализация. Этот термин обозначает трансформацию данных в серию байтов (следовательно, серийных) для хранения или передачи по сети. Также вы, возможно, уже слышали о термине «маршалинг», но это уже совсем другая область.
Естественно, десериализация — является противоположным процессом декодирования данных, которые хранятся или направлены в стандарт JSON.
Звучит как много технических терминов. Определенно. Но в реальности, все, о чем мы сейчас говорим — это чтение и написание. Представьте это следующим образом: кодирование это запись данных на диск, в то время как декодирование — это чтение данных в памяти.
Сериализация JSON
Что происходит после того, как компьютер обрабатывает большие объемы информации? Ему нужно принять дамп данных. Соответственно, модуль json предоставляет метод dump() для записи данных в файлы. Также есть метод dumps() для записей в строку Python.
Простые объекты Python переводятся в JSON согласно с весьма интуитивной конверсией.
Python | JSON |
dict | object |
list, tuple | array |
str | string |
int, long, float | number |
True | true |
False | false |
None | null |
Пример сериализации JSON Python
Представьте, что вы работаете с объектом Python в памяти, который выглядит следующим образом:
1 2 3 4 5 6 |
data = { "president": { "name": "Zaphod Beeblebrox", "species": "Betelgeusian" } } |
Сохранить эту информацию на диск — критично, так что ваша задача — записать на файл.
Используя контекстный менеджер Python, вы можете создать файл под названием data_file.json и открыть его в режиме write (файлы JSON имеют расширение .json).
1 2 |
with open("data_file.json", "w") as write_file: json.dump(data, write_file) |
Обратите внимание на то, что dump() принимает два позиционных аргумента: (1) объект данных, который сериализуется и (2), файловый объект, в который будут вписаны байты.
Или, если вы склонны продолжать использовать эти сериалзированные данные JSON в вашей программе, вы можете работать как со строкой.
1 |
json_string = json.dumps(data) |
Обратите внимание, что файловый объект является пустым, так как вы на самом деле не выполняете запись на диск. Кроме того, dumps() аналогичен dump().
Ура! У вас получился малыш JSON и вы можете выпустить его в реальный мир, чтобы он вырос большим и сильным.
Несколько полезных аргументов
Помните, что JSON создан таким образом, чтобы быть читаемым для людей. Но читаемого синтаксиса недостаточно, если все слеплено вместе. Кроме этого, ваш стиль программирования скорее всего отличается от моего, и вам будет проще читать код, если он отформатирован по вашему вкусу.
Обратите внимание: Методы dump() и dumps() пользуются одними и теми же аргументами ключевых слов.
Первая опция, которую большинство людей хотят поменять, это пробел. Вы можете использовать аргумент indent для определения размера отступа вложенных структур. Ощутите разницу лично, используя данные, упомянутые выше и выполните следующие команды в консоли:
1 2 |
json.dumps(data) json.dumps(data, indent=4) |
Еще один вариант форматирования — это аргумент separators. По умолчанию, это двойной кортеж строк разделителя («, «, «: «), но обычно в качестве альтернативы для компактного JSON является («,», «:»). Взгляните на пример JSON еще раз, чтобы понять, где в игру вступают разделители.
Есть и другие аргументы, такие как sort_keys, но я не имею понятия, что он делает. Вы можете найти полный список в документации, если вам интересно.
Десериализация JSON
Отлично, похоже вам удалось поймать экземпляр дикого JSON! Теперь нам нужно предать ему форму. В модуле json вы найдете load() и loads() для превращения кодированных данных JSON в объекты Python.
Как и сериализация, есть простая таблица конверсии для десериализации, так что вы можете иметь представление о том, как все выглядит.
JSON | Python |
object | dict |
array | list |
string | str |
number (int) | int |
number (real) | float |
true | True |
false | False |
null | None |
Технически, эта конверсия не является идеальной инверсией таблицы сериализации. По сути, это значит что если вы кодируете объект сейчас, а затем декодируете его в будущем, вы можете не получить тот же объект назад. Я представляю это как своего рода телепортацию: мои молекулы распадаются в точке А и собираются в точке Б. Буду ли я тем же самым человеком?
В реальности, это как попросить одного друга перевести что-нибудь на японский, а потом попросить другого друга перевести это обратно на русский. В любом случае, самым простым примером будет кодировать кортеж и получить назад список после декодирования, вот так:
1 2 3 4 5 6 7 8 9 10 |
blackjack_hand = (8, "Q") encoded_hand = json.dumps(blackjack_hand) decoded_hand = json.loads(encoded_hand) print(blackjack_hand == decoded_hand) # False print(type(blackjack_hand)) # <class 'tuple'> print(type(decoded_hand)) # <class 'list'> print(blackjack_hand == tuple(decoded_hand)) # True |
Пример десериализации JSON Python
На этот раз, представьте что у вас есть некие данные, хранящиеся на диске, которыми вы хотите манипулировать в памяти. Вам все еще нужно будет воспользоваться контекстным менеджером, но на этот раз, вам нужно будет открыть существующий data_file.json в режиме для чтения.
1 2 |
with open("data_file.json", "r") as read_file: data = json.load(read_file) |
Здесь все достаточно прямолинейно, но помните, что результат этого метода может вернуть любые доступные типы данных из таблицы конверсий. Это важно только в том случае, если вы загружаете данные, которые вы ранее не видели. В большинстве случаев, корневым объектом будет dict или list.
Если вы внесли данные JSON из другой программы, или полученную каким-либо другим способом строку JSON форматированных данных в Python, вы можете легко десериализировать это при помощи loads(), который естественно загружается из строки:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
json_string = """ { "researcher": { "name": "Ford Prefect", "species": "Betelgeusian", "relatives": [ { "name": "Zaphod Beeblebrox", "species": "Betelgeusian" } ] } } """ data = json.loads(json_string) |
Ву а ля! Вам удалось укротить дикого JSON, теперь он под вашим контролем. Но что вы будете делать с этой силой — остается за вами. Вы можете кормить его, выращивать, и даже научить новым приемам. Не то чтобы я не доверяю вам, но держите его на привязи, хорошо?
Пример работы с JSON Python
Для тестового API, мы воспользуемся JSONPlaceholder, отличный источник фейковых данных JSON для практических целей.
Для начала, создайте файл под названием scratch.py, или как вам удобно. Здесь я не могу вас контролировать.
Вам нужно выполнить запрос API в сервис JSONPlaceholder, так что просто используйте пакет requests, чтобы он сделал за вас всю грязную работу. Добавьте следующие импорты вверху файла:
1 2 |
import json import requests |
Теперь вам предстоит поработать со списком TODOs, потому что… это своего рода обряд посвящения, вроде того.
Идем дальше и создаем запрос в API JSONPlaceholder для конечной точки GET /todos. Если вы слабо знакомы с запросами, есть очень удобный метод json(), который выполнит за вас всю работу, но вы можете попрактиковаться в использовании модуля json для десериализации атрибута текста объекта response. Это должно выглядеть следующим образом:
1 2 |
response = requests.get("https://jsonplaceholder.typicode.com/todos") todos = json.loads(response.text) |
Не верится, что это работает? Хорошо, запустите файл в интерактивном режиме и проверьте лично. Пока вы там, проверьте тип todos. Если вам любопытно, обратите внимание на первые 10 элементов в списке.
1 2 3 4 5 6 7 |
response = requests.get("https://jsonplaceholder.typicode.com/todos") todos = json.loads(response.text) print(todos == response.json()) # True print(type(todos)) # <class 'list'> print(todos[:10]) # ... |
Как видите, никто вас не обманывает, и мы ценим здравый скептицизм.
Что такое интерактивный режим? Я уже думал вы не спросите! Вы знакомы с тем, что приходится постоянно прыгать туда-сюда между вашим редактором и терминалом? Мы, хитрые питонисты, используем интерактивный флаг -i, когда запускаем скрипт. Это отличный небольшой трюк для тестирования кода, так как он запускает скрипт, и затем открывает интерактивную командную строку с доступом ко всем данным скрипта!
Хорошо, теперь перейдем к действиям. Вы можете видеть структуру данных полученную от тестового API, посетив адрес в браузере https://jsonplaceholder.typicode.com/todos:
1 2 3 4 5 6 |
{ "userId": 1, "id": 1, "title": "delectus aut autem", "completed": false } |
Здесь несколько пользователей, каждый из которых имеет уникальный userId, а каждая задача имеет свойство Boolean. Можете определить, какие пользователи выполнили большую часть задач?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
response = requests.get("https://jsonplaceholder.typicode.com/todos") todos = json.loads(response.text) # Соотношение userId с числом выполненных пользователем задач. todos_by_user = {} # Увеличение выполненных задач каждым пользователем. for todo in todos: if todo["completed"]: try: # Увеличение количества существующих пользователей. todos_by_user[todo["userId"]] += 1 except KeyError: # Новый пользователь, ставим кол-во 1. todos_by_user[todo["userId"]] = 1 # Создание отсортированного списка пар (userId, num_complete). top_users = sorted(todos_by_user.items(), key=lambda x: x[1], reverse=True) #Получение максимального количества выполненных задач. max_complete = top_users[0][1] # Создание списка всех пользователей, которые выполнили # максимальное количество задач. 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!
Не знаю как вы, но я запустил скрипт в интерактивном режиме еще раз, и получил следующий результат:
1 2 3 |
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) для каждого пользователя, который выполнил максимальное количество задач. Здесь мы использовали F-Строки Python.
Все, что вам нужно сделать, это отфильтровать задачи и вписать итоговый список в файл. Ради оригинальности, вы можете назвать файл выдачи filtered_data_file.json. Существует много способов сделать это, вот один из них:
1 2 3 4 5 6 7 8 9 10 11 |
# Определить функцию для фильтра выполненных задач # с пользователями с максимально выполненными задачами. def keep(todo): is_complete = todo["completed"] has_max_count = todo["userId"] in users return is_complete and has_max_count # Записать отфильтрованные задачи в файл 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, с которым вы работаете?
1 2 3 4 5 6 7 8 |
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"] |
Ничего удивительного, Возникла ошибка, что класс Elf нельзя сериализировать:
1 2 3 |
elf = Elf(level=4) json.dumps(elf) TypeError: Object of type 'Elf' is not JSON serializable |
Хотя, модуль json может обрабатывать большинство типов Python, он не понимает, как кодировать пользовательские типы данных по умолчанию. Это как пытаться поместить кубик в круглое отверстие — вам понадобится бензопила и надзор специалиста.
Упрощение структур данных
Сейчас вопрос в том, что делать с более сложными структурами данных. Например, вы можете попробовать кодировать и декодировать JSON вручную, но есть более разумное решение, которое избавит вас от лишней работы. Вместо того, чтобы идти непосредственно от пользовательского типа данных к JSON, вы можете перейти к промежуточному шагу.
Все что вам нужно, это отобразить ваши данные в контексте встроенных типов, которые изначально понятны json. По сути, вы переводите более сложный объект в его упрощенное представление, которое модуль json затем переводит в JSON.
Это наглядно представляется в математике: А = В, и В = С, значит А = С.
Чтобы добиться этого, вам нужен сложный объект для работы. Вы можете использовать любой пользовательский класс на ваш вкус, но Python имеет встроенный тип под названием complex, для представления сложных чисел, и он не может быть сериализированным по умолчанию. Итак, ради этих примеров, ваш сложный объект станет сложным объектом. Уже запутались?
1 2 3 4 5 |
z = 3 + 8j print(type(z)) # <class 'complex'> json.dumps(z) TypeError: Object of type 'complex' is not JSON serializable |
Откуда приходят комплексные числа? Когда реальное число и представляемое число очень любят друг друга, они складываются вместе для создания числа, которое (справедливо) называется комплексным.
Хороший вопрос, который стоит задать себе при работе со сложными типами: «Какой минимальный объем информации необходим для воссоздания этого объекта?» В случае со комплексными числами, вам нужно знать только реальное и представляемое число, доступ к которым (в качестве атрибутов) доступен в объекте complex:
1 2 3 4 |
z = 3 + 8j print(z.real) # 3.0 print(z.imag) # 8.0 |
Передачи одних и тех же чисел в сложный конструктор достаточно для удовлетворения оператора сравнения __eq__:
1 2 3 |
z = 3 + 8j print( complex(3, 8) == z ) # True |
Разбивать пользовательские типы данных на их составляющие компоненты — критически важно как для процесса сериализации, так и для десериализации.
Кодирование пользовательских типов
Для перевода пользовательского объекта в JSON, все что вам нужно — это предоставить функцию кодирования параметру по умолчанию метода dump(). Модуль json вызовет эту функцию для любых объектов, которые не являются естественно сериализируемыми. Вот простая функция декодирования, которую вы можете использовать на практике:
1 2 3 4 5 6 |
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, если вы не получите ожидаемый тип объекта. Таким образом, вы избегаете случайной сериализации любых пользовательских типов. Теперь вы можете кодировать сложные объекты самостоятельно!
1 2 3 4 5 |
>>> 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 |
Почему мы кодируем комплексное число как кортеж? Хороший вопрос. Это определенно не является единственными выбором, впрочем, как и не является лучшим выбором. Фактически, это не может быть хорошим отображением, если вы планируете декодировать объект в будущем, что вы и увидите дальше.
Еще один частый подход — создать дочерний класс JSONEncoder и переопределить его метод default():
1 2 3 4 5 6 |
class ComplexEncoder(json.JSONEncoder): def default(self, z): if isinstance(z, complex): return (z.real, z.imag) else: super().default(self, z) |
Вместо создания ошибки TypeError, вы можете дать классу base справиться с ней. Вы можете использовать его как напрямую в метод dump() при помощи параметра cls, или создав экземпляр encoder-а и вызова метода encode():
1 2 3 4 5 6 |
>>> json.dumps(2 + 5j, cls=ComplexEncoder) '[2.0, 5.0]' >>> encoder = ComplexEncoder() >>> encoder.encode(3 + 6j) '[3.0, 6.0]' |
Декодирование пользовательских типов
В то время, как реальные и представляемые части сложных чисел абсолютно необходимы, на самом деле их не совсем достаточно для воссоздания объекта. Вот что случается, если вы попробуете кодировать комплексное число при помощи ComplexEncoder, а затем декодировать результат:
1 2 3 |
>>> complex_json = json.dumps(4 + 17j, cls=ComplexEncoder) >>> json.loads(complex_json) [4.0, 17.0] |
Все что вы получаете обратно — это список, и вы можете передать значения в конструктор, если вы хотите получить этот сложный объект еще раз. Напоминаю о нашем разговоре о телепортации. Чего нам в итоге не хватает? Метаданные, или информации о типа данных, которые вы кодируете.
Я предполагаю, что вопрос, который действительно важно задать себе, это «Какое минимальное количество информации, которая необходима, и которой достаточно для воссоздания объекта?»
Модуль json ожидает, что все пользовательские типы будут отображаться как объекты стандарта JSON. Для разнообразия, вы можете создать файл JSON, на этот раз назовем его complex_data.json и добавим следующий объект, отображающий комплексное число:
1 2 3 4 5 |
{ "__complex__": true, "real": 42, "imag": 36 } |
Заметили хитрую часть? Вот ключ «__complex__» является метаданными, которые мы только что упоминали. Неважно, какое ассоциируемое значение мы имеем. Чтобы эта хитрость сработала, все что вам нужно, это подтвердить существование ключа:
1 2 3 4 |
def decode_complex(dct): if "__complex__" in dct: return complex(dct["real"], dct["imag"]) return dct |
Если «__complex__» не находится в словаре, вы можете просто вернуть объект и позволить декодеру по умолчанию разобраться с этим.
Каждый раз, когда метод load() пытается парсить объект, у вас есть возможность выступить в роли посредника, перед тем как декодер пройдет свой путь с данными. Вы можете сделать это, спарсив вашу функцию декодирования с параметром object_hook.
Теперь сыграем в ту же игру, что и раньше:
1 2 3 4 5 |
with open("complex_data.json") as complex_data: data = complex_data.read() z = json.loads(data, object_hook=decode_complex) print(type(z)) # <class 'complex'> |
Хотя объект object_hook может показаться аналогом параметра по умолчанию метода dump(), на самом деле аналогия здесь же и заканчивается.
Это не просто работает с одним объектом. Попробуйте внести этот список сложных чисел в complex_data.json и запустить скрипт еще раз:
1 2 3 4 5 6 7 8 9 10 11 12 |
[ { "__complex__":true, "real":42, "imag":36 }, { "__complex__":true, "real":64, "imag":11 } ] |
Если все идет гладко, вы получите список комплексных объектов:
1 2 3 4 5 6 |
with open("complex_data.json") as complex_data: data = complex_data.read() numbers = json.loads(data, object_hook=decode_complex) print(numbers) # [(42+36j), (64+11j)] |
Вы также можете попробовать создать подкласс JSONDecoder и переопределить object_hook, но лучше придерживаться более легких решений при каждой возможности.
Итоги
Поздравляю, теперь вы обладаете могущественной силой JSON для любых ваших потребностей в Python.
Хотя примеры, с которыми вы работали, безусловно, оригинальные и чрезмерно упрощены, они демонстрируют рабочий процесс, который вы можете применить к более общим задачам:
- Импорт модуля json
- Чтение данных с load() или loads()
- Обработка данных
- Запись измененных данных при помощи dump() или dumps()
Что вы будете делать с данными, после того как они загрузились в память — зависит от вашего случая. В целом, ваша задача — собрать данные из источника, извлечение полезной информации, и передача этой информации (или ее запись).
Сегодня вы проделали небольшое путешествие: вы поймали и приручили JSON, и вернулись домой как раз к ужину! И в качестве бонуса, научившись работать с модулем json, можете начать изучение модулей pickle и marshal.
Спасибо за внимание, и удачи с вашими начинаниями в Python!
Являюсь администратором нескольких порталов по обучению языков программирования Python, Golang и Kotlin. В составе небольшой команды единомышленников, мы занимаемся популяризацией языков программирования на русскоязычную аудиторию. Большая часть статей была адаптирована нами на русский язык и распространяется бесплатно.
E-mail: vasile.buldumac@ati.utm.md
Образование
Universitatea Tehnică a Moldovei (utm.md)
- 2014 — 2018 Технический Университет Молдовы, ИТ-Инженер. Тема дипломной работы «Автоматизация покупки и продажи криптовалюты используя технический анализ»
- 2018 — 2020 Технический Университет Молдовы, Магистр, Магистерская диссертация «Идентификация человека в киберпространстве по фотографии лица»