Работа с данными JSON в Python
Оглавление
- А (очень) краткая история JSON
- Смотрите, это JSON!
- Python поддерживает JSON нативно!
- Пример реального мира (вроде того)
- Кодирование и декодирование пользовательских объектов Python
- Все готово!
С момента своего появления JSON быстро стал стандартом де-факто для обмена информацией. Скорее всего, вы находитесь здесь, потому что вам нужно переместить некоторые данные из одного места в другое. Возможно, вы собираете информацию с помощью API или храните данные в базе данных документов. Так или иначе, вы по уши увязли в JSON, и вам придется искать выход из ситуации с помощью Python.
К счастью, это довольно распространенная задача, и, как и в случае с большинством других распространенных задач, Python делает ее почти отвратительно простой. Не бойтесь, товарищи питонисты и питонистки. Это будет проще простого!
Итак, мы используем JSON для хранения и обмена данными? Да, вы поняли! Это не что иное, как стандартизированный формат, который сообщество использует для передачи данных. Не забывайте, что JSON - не единственный формат, доступный для такой работы, но XML и YAML, пожалуй, единственные, которые стоит упомянуть на одном дыхании.
А (очень) краткая история JSON
Не так уж удивительно, что JavaScript Object Notation был вдохновлен подмножеством JavaScript языка программирования , имеющим дело с синтаксисом объектных литералов. У них есть отличный веб-сайт, на котором все это объясняется. Однако не волнуйтесь: JSON уже давно стал независимым от языка и существует как собственный стандарт, так что мы, к счастью, можем обойтись без JavaScript в рамках этого обсуждения.
В конечном счете, сообщество в целом приняло JSON, потому что его легко создавать и понимать как людям, так и машинам.
Смотрите, это JSON!
Приготовьтесь. Сейчас я покажу вам реальный JSON - такой, какой можно встретить в природе. Все в порядке: JSON должен быть доступен для чтения любому, кто использовал язык в стиле C, а Python - это язык в стиле C... так что это вы!
{
"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 в соответствии с достаточно интуитивным преобразованием.
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-данные в своей программе, вы можете записать их в собственный объект Python str
.
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
, но я понятия не имею, что они делают. Весь список можно найти в docs, если вам интересно.
Десериализация 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 |
С технической точки зрения, это преобразование не является идеальной инверсией таблицы сериализации. Это означает, что если вы закодируете объект сейчас, а затем декодируете его позже, вы можете не получить точно такой же объект обратно. Я представляю себе это как телепортацию: разбейте мои молекулы на части здесь и соберите их обратно там. Я все еще тот же человек?
В реальности это больше похоже на то, как если бы один друг переводил что-то на японский, а другой - на английский. В любом случае, простейшим примером будет кодирование 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)
Войла! Вы приручили дикий JSON, и теперь он находится под вашим контролем. Но что вы будете делать с этой властью, зависит только от вас. Вы можете кормить его, ухаживать за ним и даже учить его трюкам. Не то чтобы я вам не доверял... но держите его на поводке, хорошо?
Пример из реального мира (вроде того)
Для вводного примера вы будете использовать 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(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
, если не получите тот объект, который ожидали. Таким образом, вы избежите случайной сериализации эльфов. Теперь вы можете сами попробовать кодировать сложные объекты!
>>> 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
может показаться, что является аналогом default
параметра метода dump()
, на самом деле аналогия начинается и заканчивается на этом.
Это работает не только с одним объектом. Попробуйте поместить этот список комплексных чисел в 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 и успели вернуться к ужину! В качестве дополнительного бонуса, изучение пакета json
сделает изучение pickle
и marshal
легким делом.
Удачи во всех твоих будущих питонических начинаниях!
Вернуться на верх