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/false — True/False, null — None. Больше в формате ничего нет: ни дат, ни кортежей, ни множеств.
Отсюда первое практическое следствие: дата в 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 ставит после запятой и двоеточия.
Карта урока
Скажите своими словами
Не подглядывая, ответьте вслух или на бумаге. Ответы — в конце урока.
- Чем
json.loadsотличается отjson.load? - Почему
series[2025]перестаёт работать после записи в JSON и чтения обратно? - Что в 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 попадает в пропуски, а год превращается в число при разборе, а не после — потому что после уже поздно.
Долги. Мы разбираем ответ вручную и надеемся, что ключи на месте. Настоящая проверка формы — это схема, которая говорит «здесь ждали число, пришла строка»; до неё дойдём, когда к сводке приедет ответ языковой модели.
Ответы
Показать ответы
На вопросы
- Только входом:
loadsразбирает строку,load— открытый файл. Букваs— от «string». Так жеdumpsотдаёт строку, аdumpпишет в файл. - Потому что ключ объекта в JSON — всегда строка, и при записи
2025превращается в"2025". После чтения ключи остаются строками, и обращение по числу даётKeyError. - Приходит
None— отсутствие значения. Ноль — это значение, и путать их нельзя: пропуск, посчитанный нулём, занижает среднее.
К разминке
str None. Дата в JSON — строка, поэтому"2025"приезжает строкой, аnullстановитсяNone— не нулём и не пустой строкой.
str None
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}
- Ключ никуда не делся — он сменил тип:
dumpsзаписал2025как"2025", и послеloadsв словаре строка. По числу он больше не находится, поэтому либо обращаются по строке, либо — правильнее — возвращают ключам число сразу после чтения.
import json
series = {2025: 11.39}
back = json.loads(json.dumps(series))
print(back["2025"])
11.39
Источники
Если вы нашли ошибку или опечатку в тексте статьи, то сообщите нам об этом
Комментарии (0)
Войдите, чтобы оставить комментарий →
Пока нет комментариев. Будьте первым.