Shanraq.org Shanraq.org
JSON: ответ API, который в первом уроке мы разобрали на веру
IT

Python: от данных до своей сводки Урок 13 из 56

JSON: ответ API, который в первом уроке мы разобрали на веру

Тринадцатый урок курса по Python. Ответ Всемирного банка — список из двух элементов: служебная часть и записи. Разбираем его руками и находим то, что портит расчёты молча: `null` становится `None`, а ключи-числа после круга через JSON возвращаются строками — `2025` уходит, `"2025"` приходит.

Зачем это нужно

В первом уроке программа сходила в сеть, получила ответ и достала из него числа. Мы тогда сказали: запустите, ничего не понимая. Сегодня — понимаем.

JSON — то, на чём говорит почти каждый API: банк, погода, курс валют, языковая модель. Формат простой: словари, списки, строки, числа, true, false, null. Ровно эти шесть вещей.

Простота обманчива. Два места в переводе из JSON в Python и обратно портят данные молча, и оба видно на нашем же ответе.

Сразу целиком

Файл json_demo.py. Запуск: python json_demo.py из окружения.

Обязательное — первые два блока: разобрать ответ и достать из него ряд. Третий и четвёртый показывают ловушки, ради которых урок и написан.

"""Урок 13: JSON — то, что приехало из сети, до того как стало числами.

Это кусок настоящего ответа Всемирного банка: сверху служебная часть, снизу
записи. В первом уроке программа разбирала его, ничего не объясняя. Разберём.

Ответ укорочен до трёх записей, а значение за 2023 год заменено на null: в
настоящем ответе оно есть, но пропуск нам сегодня нужен, и честнее сказать,
что он поставлен нарочно.
"""

import json

ANSWER = """[
  {"page": 1, "pages": 1, "per_page": 3, "total": 3},
  [
    {"country": {"id": "KZ", "value": "Kazakhstan"}, "date": "2025", "value": 11.39},
    {"country": {"id": "KZ", "value": "Kazakhstan"}, "date": "2024", "value": 8.69},
    {"country": {"id": "KZ", "value": "Kazakhstan"}, "date": "2023", "value": null}
  ]
]"""

data = json.loads(ANSWER)

print("== что приехало")
print("верхний уровень:", type(data).__name__, "из", len(data), "элементов")
print("служебная часть:", data[0])
print("записей:", len(data[1]))

print()
print("== разбираем записи")
series = {}
for row in data[1]:
    year = int(row["date"])
    series[year] = row["value"]
    print(f"{row['country']['value']} {year}: {row['value']}")

print()
print("== ключи после круга через JSON")
text = json.dumps(series)
back = json.loads(text)
print("до:   ", series)
print("текст:", text)
print("после:", back)

print()
print("== кириллица и отступы")
note = {"note": "нет данных"}
print("по умолчанию:  ", json.dumps(note))
print("ensure_ascii=False:", json.dumps(note, ensure_ascii=False))
print("байт:", len(json.dumps(note).encode()), "с экранированием,",
      len(json.dumps(note, ensure_ascii=False).encode()), "без него")
print(json.dumps(series, ensure_ascii=False, indent=2))

Выводит:

== что приехало
верхний уровень: list из 2 элементов
служебная часть: {'page': 1, 'pages': 1, 'per_page': 3, 'total': 3}
записей: 3

== разбираем записи
Kazakhstan 2025: 11.39
Kazakhstan 2024: 8.69
Kazakhstan 2023: None

== ключи после круга через JSON
до:    {2025: 11.39, 2024: 8.69, 2023: None}
текст: {"2025": 11.39, "2024": 8.69, "2023": null}
после: {'2025': 11.39, '2024': 8.69, '2023': None}

== кириллица и отступы
по умолчанию:   {"note": "\u043d\u0435\u0442 \u0434\u0430\u043d\u043d\u044b\u0445"}
ensure_ascii=False: {"note": "нет данных"}
байт: 67 с экранированием, 31 без него
{
  "2025": 11.39,
  "2024": 8.69,
  "2023": null
}

Разбор

Шесть вещей, из которых состоит JSON

Объект {...} становится словарём, массив [...] — списком, строка — строкой, число — int или float, true/falseTrue/False, nullNone. Больше в формате ничего нет: ни дат, ни кортежей, ни множеств.

Отсюда первое практическое следствие: дата в JSON — это строка. В нашем ответе год приезжает как "2025", и int(row["date"]) в разборе стоит не от лишней осторожности.

Образ. Посылка с описью. В коробке могут лежать только шесть видов вещей, и опись перечисляет их по именам. Всё остальное — ваза, кот, обещание — упаковывается как одна из этих шести или не едет вовсе.

loads и load — одна буква и разный вход

json.loads(text) разбирает строку, json.load(file) — открытый файл. Так же и обратно: dumps отдаёт строку, dump пишет в файл. Буква s в конце — от «string», и это единственная разница.

В первом уроке стояло json.load(answer), где answer — ответ сети, который ведёт себя как файл. Сегодня у нас строка в программе, поэтому loads.

Служебная часть и записи

верхний уровень: list из 2 элементов

Всемирный банк отвечает списком: нулевой элемент — сколько всего записей и страниц, первый — сами записи. Это не общее правило JSON, а договор конкретного API; у погоды или у Нацбанка он будет другим.

Отсюда привычка, экономящая вечер: первым делом напечатайте, что приехало. type(), len(), ключи первой записи — три строки, после которых видно форму ответа. Гадать по документации дольше.

Вложенность разбирают по одному шагу: row["country"]["value"] — сначала словарь страны, потом её имя. Если ключа может не быть, берут .get: row.get("unit", "") вернёт пустую строку вместо KeyError.

null — это None, а не ноль

За 2023 год в ответе стоит null, и в Python он становится None. Это ровно тот пропуск, о котором был седьмой урок: не ноль, не пустая строка — отсутствие значения.

Проверка та же: if value is None. Ошибка та же: посчитать пропуск нулём и получить среднее ниже настоящего.

Ключи-числа возвращаются строками

Самое дорогое место урока:

до:    {2025: 11.39, 2024: 8.69, 2023: None}
текст: {"2025": 11.39, "2024": 8.69, "2023": null}
после: {'2025': 11.39, '2024': 8.69, '2023': None}

Ключи ушли числами, а вернулись строками. Так устроен сам формат: ключ объекта в JSON — всегда строка, и json.dumps молча превращает 2025 в "2025".

Дальше в программе series[2025] даёт KeyError, хотя данные на месте и глазами всё видно. Лечится решением, а не заплаткой: либо ключи приводят обратно при чтении ({int(k): v for k, v in ...} — как в нашем int(row["date"])), либо год хранят внутри записи, а не в ключе.

Кириллица и отступы

по умолчанию:   {"note": "\u043d\u0435\u0442 \u0434\u0430\u043d\u043d\u044b\u0445"}
ensure_ascii=False: {"note": "нет данных"}
байт: 67 с экранированием, 31 без него

По умолчанию json.dumps записывает всё, кроме латиницы, escape-последовательностями. Файл остаётся правильным JSON, и любая программа прочитает его верно — но человек в нём ничего не увидит.

Для файлов, которые открывают глазами, пишут ensure_ascii=False, а для читаемости добавляют indent=2.

Умолчание выбрано не ради длины: с экранированием наша строка занимает 67 байт, без него — 31, и это измерено программой. Одна кириллическая буква стоит шести символов \uXXXX вместо двух байтов UTF-8. Выигрыш у умолчания другой — чистый ASCII проходит везде, включая канал, который кодировку переврёт. А экономят на пробелах: separators=(",", ":") убирает те, что json.dumps ставит после запятой и двоеточия.

Карта урока

Карта урока: форма ответа, null и ключи

Скажите своими словами

Не подглядывая, ответьте вслух или на бумаге. Ответы — в конце урока.

  1. Чем json.loads отличается от json.load?
  2. Почему series[2025] перестаёт работать после записи в JSON и чтения обратно?
  3. Что в Python приходит на месте null и чем это отличается от нуля?

Разминка

Три коротких шага перед заданием: предсказать, дописать, починить. Ответы — в конце урока, но сначала ответьте сами.

1. Предскажите. Что напечатает эта программа?

import json

data = json.loads('{"year": "2025", "value": null}')
print(type(data["year"]).__name__, data["value"])

2. Заполните пропуск. Вместо ... поставьте то, что вернёт ключам их числовой вид.

import json

series = {2025: 11.39, 2024: 8.69}
back = json.loads(json.dumps(series))
fixed = {}
for key, value in back.items():
    fixed[...] = value
print(fixed)

3. Почините. Программа падает с KeyError, хотя данные на месте. Объясните, куда делся ключ, и почините чтение.

import json

series = {2025: 11.39}
back = json.loads(json.dumps(series))
print(back[2025])

Задание

Обязательное. Дано:

ANSWER = """[
  {"page": 1, "pages": 1, "per_page": 4, "total": 4},
  [
    {"country": {"id": "KZ", "value": "Kazakhstan"}, "date": "2025", "value": 11.39},
    {"country": {"id": "KZ", "value": "Kazakhstan"}, "date": "2024", "value": 8.69},
    {"country": {"id": "KZ", "value": "Kazakhstan"}, "date": "2023", "value": null},
    {"country": {"id": "KZ", "value": "Kazakhstan"}, "date": "2022", "value": 15.0}
  ]
]"""

Разберите ответ и напечатайте, сколько в нём записей. Соберите словарь «год → значение», где год — число, а записи с null уходят в список пропусков. Напечатайте, сколько лет с числом и среднее по ним с двумя знаками, потом список пропусков. Затем сделайте круг через JSON — dumps и обратно loads — и напечатайте тип ключа до и после, а следом почините ключи и напечатайте значение за 2025 год.

Ожидаемый вывод:

записей: 4
лет с числом: 3, среднее: 11.69
пропуски: [2023]
ключ до: int | после: str
после починки: 11.39

Готово, когда: вывод совпадает построчно; год становится числом при разборе, а не после; null попал в пропуски, а не в среднее; после круга через JSON обращение по числу снова работает.

На своих данных. Возьмите свой словарь «год → значение», в котором есть один None. Запишите его в файл через json.dump с ensure_ascii=False и indent=2, прочитайте обратно через json.load и напечатайте типы ключей до и после. Затем почините чтение так, чтобы ключи снова стали числами.

По желанию.

  • Разберите настоящий ответ из первого урока и напечатайте ключи первой записи.
  • Попробуйте json.dumps на множестве {1, 2} и прочитайте ошибку.
  • Сохраните один и тот же словарь с ensure_ascii=False и без — сравните размеры файлов.

Куда это встанет в проекте

Сводка перестаёт верить ответу на слово. Форма проверяется, null попадает в пропуски, а год превращается в число при разборе, а не после — потому что после уже поздно.

Долги. Мы разбираем ответ вручную и надеемся, что ключи на месте. Настоящая проверка формы — это схема, которая говорит «здесь ждали число, пришла строка»; до неё дойдём, когда к сводке приедет ответ языковой модели.

Ответы

Показать ответы

На вопросы

  1. Только входом: loads разбирает строку, load — открытый файл. Буква s — от «string». Так же dumps отдаёт строку, а dump пишет в файл.
  2. Потому что ключ объекта в JSON — всегда строка, и при записи 2025 превращается в "2025". После чтения ключи остаются строками, и обращение по числу даёт KeyError.
  3. Приходит None — отсутствие значения. Ноль — это значение, и путать их нельзя: пропуск, посчитанный нулём, занижает среднее.

К разминке

  1. str None. Дата в JSON — строка, поэтому "2025" приезжает строкой, а null становится None — не нулём и не пустой строкой.
str None
  1. int(key). Ключ объекта в JSON — всегда строка, и обратно в число его приводят при чтении; иначе series[2025] перестанет находить год, который никуда не девался.
import json

series = {2025: 11.39, 2024: 8.69}
back = json.loads(json.dumps(series))
fixed = {}
for key, value in back.items():
    fixed[int(key)] = value
print(fixed)
{2025: 11.39, 2024: 8.69}
  1. Ключ никуда не делся — он сменил тип: dumps записал 2025 как "2025", и после loads в словаре строка. По числу он больше не находится, поэтому либо обращаются по строке, либо — правильнее — возвращают ключам число сразу после чтения.
import json

series = {2025: 11.39}
back = json.loads(json.dumps(series))
print(back["2025"])
11.39

Источники

Если вы нашли ошибку или опечатку в тексте статьи, то сообщите нам об этом

Проверить задание

Сначала решите и запустите в VS Code — редактор покажет ошибку на месте. Готовое решение вставьте сюда. Проверяет модель: она укажет на ошибку, но не даст готовый ответ.

Чтобы проверить, нужно войти. Войти

Комментарии (0)

Пока нет комментариев. Будьте первым.