You are currently viewing Работа с данными JSON в Python

Работа с данными JSON в Python

С момента своего создания 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 в соответствии с довольно интуитивно понятным преобразованием.

PythonJSON
dictobject
listtuplearray
strstring
intlongfloatnumber
Truetrue
Falsefalse
Nonenull

Простой пример сериализации

Представьте, что вы работаете с объектом 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.

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

JSONPython
objectdict
arraylist
stringstr
number (int)int
number (реально)float
trueTrue
falseFalse
nullNone

Технически это преобразование не является идеальным обратным к таблице сериализации. Это в основном означает, что если вы закодируете объект сейчас, а затем снова декодируете его позже, вы можете не получить точно такой же объект обратно. Я представляю, что это немного похоже на телепортацию: разбейте мои молекулы здесь и соберите их обратно там. Я все тот же человек?

На самом деле, это, вероятно, больше похоже на то, чтобы один друг перевел что-то на японский, а другой друг перевел это обратно на английский. Независимо от этого, самым простым примером было бы кодирование 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() метода defaultjson Модуль вызовет эту функцию для любых объектов, которые изначально не сериализуемы. Вот простая функция декодирования, которую вы можете использовать для практики:

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.

Хотя примеры, с которыми вы здесь работали, безусловно, надуманны и чрезмерно упрощены, они иллюстрируют рабочий процесс, который вы можете применить к более общим задачам:

  1. Импортируйте json пакет.
  2. Считайте данные с load() помощью или loads().
  3. Обработайте данные.
  4. Запишите измененные данные с dump() помощью или dumps().

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

Сегодня вы отправились в путешествие: вы поймали и приручили какого-то дикого Джейсона, и вы вернулись как раз к ужину! В качестве дополнительного бонуса, изучение json пакета сделает обучение pickle легким и marshal быстрым.