С момента своего создания JSON быстро стал стандартом де-факто для обмена информацией. Скорее всего, вы здесь, потому что вам нужно перенести некоторые данные отсюда туда. Возможно, вы собираете информацию через API или храните свои данные в базе данных документов. Так или иначе, вы по уши увязли в JSON, и вам нужно найти выход на Python.
К счастью, это довольно распространенная задача, и как и в большинстве распространенных задач — Python делает ее почти отвратительно простой. Не бойтесь, друзья-питонисты и питонисты. Это будет легкий ветерок!
(Очень) Краткая история JSON
Неудивительно, что создание JavaScript Object было вдохновлено подмножеством языка программирования JavaScript, имеющего дело с синтаксисом объектных литералов. У них есть отличный веб-сайт, который все объясняет. Однако не волнуйтесь: JSON уже давно стал агностиком языка и существует как собственный стандарт, поэтому мы, к счастью, можем избежать JavaScript ради этого обсуждения.
В конечном счете, сообщество в целом приняло JSON, потому что его легко создавать и понимать как людям, так и машинам.
Смотри, это JSON!
Приготовиться. Я собираюсь показать вам кое—что из реальной жизни JSON-точно так же, как вы видели бы там, в дикой природе. Все в порядке: JSON должен быть доступен для чтения любому, кто использовал язык в стиле C, а Python-это стиль C language…so это ты!
{
"firstName": "Jane",
"lastName": "Doe",
"hobbies": ["running", "sky diving", "singing"],
"age": 35,
"children": [
{
"firstName": "Alice",
"age": 6
},
{
"firstName": "Bob",
"age": 8
}
]
}
Как вы можете видеть, JSON поддерживает примитивные типы, такие как строки и числа, а также вложенные списки и объекты.
Подождите, это похоже на словарь Python! Я знаю, верно? На данный момент это в значительной степени универсальная объектная нотация, но я не думаю, что она так же хорошо слетает с языка. Не стесняйтесь обсуждать альтернативы в комментариях.
Ух ты! Вы пережили свою первую встречу с каким-то диким JSON. Теперь вам просто нужно научиться приручать его.
Python изначально поддерживает JSON!
Python поставляется со встроенным пакетом json
, предназначенным для кодирования и декодирования данных JSON.
Просто брось этого маленького парня наверх своего досье:
import json
Немного словарного запаса
Процесс кодирования JSON обычно называется стерилизацией. Этот термин относится к преобразованию данных в серию байтов (следовательно, последовательных) для хранения или передачи по сети. Вы также можете услышать термин «маршалинг«, но это совсем другое обсуждение. Естественно, десериализация-это взаимный процесс декодирования данных, которые были сохранены или доставлены в стандарте JSON.
Блин! Это звучит довольно технически, определенно. Но на самом деле все, о чем мы здесь говорим, — это чтение и письмо. Подумайте об этом так: кодирование предназначено для записи данных на диск, а декодирование-для чтения данных в память.
Сериализация JSON
Что происходит после того, как компьютер обрабатывает большое количество информации? Ему нужно создать дамп данных. Соответственно, json
библиотека предоставляет dump()
метод записи данных в файлы. Существует также dumps()
метод (произносится как “dump-s”) для записи в строку Python.
Простые объекты Python переводятся в JSON в соответствии с довольно интуитивно понятным преобразованием.
Python | JSON |
---|---|
dict | object |
list , tuple | array |
str | string |
int , long , float | number |
True | true |
False | false |
None | null |
Простой пример сериализации
Представьте, что вы работаете с объектом 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 предназначен для легкого чтения людьми, но читаемого синтаксиса недостаточно, если все это сжато вместе. Кроме того, у вас, вероятно, другой стиль программирования, чем у меня, и вам может быть легче читать код, когда он отформатирован по вашему вкусу.
ПРИМЕЧАНИЕ: Оба метода
dump()
иdumps()
используют одни и те же аргументы ключевого слова.
Первый вариант, который большинство людей хотят изменить, — это пробелы. Вы можете использовать аргумент indent
ключевого слова для указания размера отступа для вложенных структур. Проверьте разницу сами , используя data
, как мы определили выше, и выполнив следующие команды в консоли:>>>
>>> json.dumps(data)
>>> json.dumps(data, indent=4)
Другим вариантом форматирования является аргумент separators
ключевого слова. По умолчанию это 2 кортежа строк-разделителей(", ", ": ")
, но распространенной альтернативой для компактного JSON является (",", ":")
. Взгляните еще раз на образец JSON, чтобы увидеть, где эти разделители вступают в игру.
Есть и другие, например sort_keys
, но я понятия не имею, что делает этот. Вы можете найти целый список в документах, если вам интересно.
Десериализация JSON
Отлично, похоже, ты поймал себе какой-то дикий JSON! Теперь пришло время привести его в форму. В json
библиотеке вы найдете load()
и loads()
для преобразования данных, закодированных в формате JSON, в объекты Python.
Так же, как и сериализация, существует простая таблица преобразования для десериализации, хотя вы, вероятно, уже можете догадаться, как она выглядит.
JSON | Python |
---|---|
object | dict |
array | list |
string | str |
number (int) | int |
number (реально) | float |
true | True |
false | False |
null | None |
Технически это преобразование не является идеальным обратным к таблице сериализации. Это в основном означает, что если вы закодируете объект сейчас, а затем снова декодируете его позже, вы можете не получить точно такой же объект обратно. Я представляю, что это немного похоже на телепортацию: разбейте мои молекулы здесь и соберите их обратно там. Я все тот же человек?
На самом деле, это, вероятно, больше похоже на то, чтобы один друг перевел что-то на японский, а другой друг перевел это обратно на английский. Независимо от этого, самым простым примером было бы кодирование a tuple
и возврат a 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)
Здесь все довольно просто, но имейте в виду, что результат этого метода может возвращать любой из разрешенных типов данных из таблицы преобразования. Это важно только в том случае, если вы загружаете данные, которые вы раньше не видели. В большинстве случаев корневым объектом будет a dict
или a list
.
Если вы извлекли данные JSON из другой программы или иным образом получили строку данных в формате JSON на Python, вы можете легко десериализовать ее с loads()
помощью , которая, естественно, загружается из строки:
json_string = """
{
"researcher": {
"name": "Ford Prefect",
"species": "Betelgeusian",
"relatives": [
{
"name": "Zaphod Beeblebrox",
"species": "Betelgeusian"
}
]
}
}
"""
data = json.loads(json_string)
Voilà! Вы приручили дикого JSON, и теперь он под вашим контролем. Но что вы будете делать с этой силой, зависит от вас. Вы могли бы кормить его, лелеять и даже учить трюкам. Дело не в том, что я тебе не доверяю…но держи это на привязи, ладно?
Пример из реального мира (вроде того)
В качестве вводного примера вы будете использовать JSON Placeholder, отличный источник поддельных данных JSON для практических целей.
Сначала создайте файл сценария под названием scratch.py
или как вы хотите. Я действительно не могу остановить тебя.
Вам нужно будет отправить запрос API в службу JSON Placeholder, поэтому просто используйте requests
пакет, чтобы выполнить тяжелую работу. Добавьте эти импортные данные в верхнюю часть вашего файла:
import json
import requests
Теперь ты будешь работать со списком дел, потому что…ну, знаешь, это обряд посвящения или что-то в этом роде.
Продолжайте и сделайте запрос к API-интерфейсу JSON Placeholder для /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
интерактивный флаг, когда запускаем скрипт. Это отличный маленький трюк для тестирования кода, потому что он запускает сценарий, а затем открывает интерактивную командную строку с доступом ко всем данным из сценария!
Ладно, пора действовать. Вы можете увидеть структуру данных, посетив конечную точку в браузере, но вот пример задачи:
{
"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, содержащий завершенные задачи для каждого пользователя, выполнившего максимальное количество задач.
Все, что вам нужно сделать, это отфильтровать 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(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")
Обратите внимание, что вы должны поднять a, TypeError
если не получите тот объект, который ожидали. Таким образом, вы избежите случайной сериализации любых эльфов. Теперь вы можете попробовать кодировать сложные объекты самостоятельно!>>>
>>> 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()
, когда метод пытается проанализировать anobject
, вам предоставляется возможность вмешаться до того, как декодер по умолчанию справится с данными. Вы можете сделать это, передав функцию декодирования 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.
Хотя примеры, с которыми вы здесь работали, безусловно, надуманны и чрезмерно упрощены, они иллюстрируют рабочий процесс, который вы можете применить к более общим задачам:
- Импортируйте
json
пакет. - Считайте данные с
load()
помощью илиloads()
. - Обработайте данные.
- Запишите измененные данные с
dump()
помощью илиdumps()
.
То, что вы будете делать со своими данными после их загрузки в память, будет зависеть от вашего варианта использования. Как правило, вашей целью будет сбор данных из источника, извлечение полезной информации и передача этой информации или ее учет.
Сегодня вы отправились в путешествие: вы поймали и приручили какого-то дикого Джейсона, и вы вернулись как раз к ужину! В качестве дополнительного бонуса, изучение json
пакета сделает обучение pickle
легким и marshal
быстрым.