Сохранение и загрузка JSON файлов средствами Python

Раздел: Ввод-вывод и файловая система -> Чтение и запись структурированных данных

JSON (JavaScript Object Notation) – текстовый формат обмена данными. В Python для работы с ним используется встроенный модуль json. Две ключевые функции: json.dump для записи объекта в файл и json.load для чтения из файла. Также существуют json.dumps и json.loads для работы со строками. В статье рассматриваются различные сценарии применения этих функций.

Базовый сценарий: запись и чтение словаря

Самый распространённый случай – сохранить словарь в JSON-файл, а затем восстановить его.

import json

data = {
    "name": "Alice",
    "age": 30,
    "skills": ["Python", "SQL"]
}

# Запись в файл
with open("data.json", "w", encoding="utf-8") as f:
    json.dump(data, f)

# Чтение из файла
with open("data.json", "r", encoding="utf-8") as f:
    loaded = json.load(f)

print(loaded)  # {'name': 'Alice', 'age': 30, 'skills': ['Python', 'SQL']}

Python json in py file (чтение и запись json в файл python (json.load, json.dump))

Пояснение: open() открывает файл в режиме записи ('w') или чтения ('r'). Указание encoding='utf-8' обязательно, если в данных есть символы не из ASCII (русские буквы, знаки). json.dump автоматически преобразует Python-объекты (словари, списки, числа, строки, None, True/False) в JSON. json.load делает обратное преобразование.

Типичная ошибка №1: попытка записать в файл объект несериализуемого типа (например, datetime). Возникает TypeError: Object of type datetime is not JSON serializable. Решение – преобразовать объект в строку или использовать аргумент default (см. варианты).
Типичная ошибка №2: при чтении файла с повреждённым JSON (лишняя запятая, отсутствие кавычек) возникает json.JSONDecodeError. Всегда стоит оборачивать чтение в try/except.

Как записать JSON с отступами и сортировкой ключей?

Для улучшения читаемости файла используются аргументы indent и sort_keys.

import json

data = {"name": "Bob", "age": 25, "city": "Moscow"}

with open("pretty.json", "w", encoding="utf-8") as f:
    json.dump(data, f, indent=4, sort_keys=True)

# Файл pretty.json получится:
# {
#     "age": 25,
#     "city": "Moscow",
#     "name": "Bob"
# }

indent=4 задаёт количество пробелов отступа. sort_keys=True сортирует ключи по алфавиту. Это удобно для конфигурационных файлов, которые предполагается редактировать вручную.

Проблема: если файл уже существует и содержит некорректный JSON, при записи с 'w' он перезапишется. Желательно сначала создавать резервную копию или проверять валидность содержимого.

Как сохранить русские символы без экранирования (\uXXXX)?

По умолчанию json.dump заменяет не-ASCII символы на escape-последовательности (\u0410 и т.п.). Чтобы этого избежать, используется аргумент ensure_ascii=False.

import json

data = {"message": "Привет, мир!"}

with open("russian.json", "w", encoding="utf-8") as f:
    json.dump(data, f, ensure_ascii=False)

# Содержимое файла: {"message": "Привет, мир!"}   (без экранирования)

Без ensure_ascii=False строка стала бы "\u041f\u0440\u0438...". Аргумент полезен при создании человекочитаемых JSON-файлов, особенно если файл будет просматриваться в текстовом редакторе.

Внимание: если кодировка файла не UTF-8, символы могут быть повреждены. Всегда указывайте encoding='utf-8'.

Как прочитать JSON из строки (а не из файла)?

Если данные JSON находятся в переменной-строке, используйте json.loads(), а для обратного преобразования объекта в строку – json.dumps().

import json

json_string = '{"key": "value", "number": 42}'
parsed = json.loads(json_string)
print(parsed["key"])  # value

# Обратно в строку с отступами
new_string = json.dumps(parsed, indent=2)
print(new_string)
# {
#   "key": "value",
#   "number": 42
# }

Это удобно при работе с веб-API, когда JSON приходит в теле HTTP-ответа.

Ошибка: если строка не является валидным JSON, json.loads бросает JSONDecodeError. Используйте try/except.

Как сериализовать пользовательские объекты (например, дату)?

По умолчанию json.dump не знает, как преобразовать объекты произвольных классов в JSON. Нужно передать функцию через аргумент default или создать свой класс-кодировщик (наследник json.JSONEncoder).

import json
from datetime import datetime

def json_serializer(obj):
    if isinstance(obj, datetime):
        return obj.isoformat()
    raise TypeError(f"Type {type(obj)} not serializable")

data = {"event": "meeting", "time": datetime.now(), "done": False}

with open("event.json", "w", encoding="utf-8") as f:
    json.dump(data, f, default=json_serializer, indent=2)

# Результат: {"event": "meeting", "time": "2025-02-18T12:30:00", "done": false}

Альтернативно можно использовать json.JSONEncoder:

class CustomEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, datetime):
            return obj.isoformat()
        return super().default(obj)

with open("event2.json", "w", encoding="utf-8") as f:
    json.dump(data, f, cls=CustomEncoder, indent=2)

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

Проблема: если в данных встречаются объекты нескольких нестандартных типов, функция default должна обрабатывать каждый из них. Иначе – TypeError.

Как обновить запись в существующем JSON-файле?

Прямое редактирование не поддерживается. Сначала нужно прочитать файл, изменить Python-объект, затем снова записать.

import json
from pathlib import Path

file_path = Path("config.json")

# Проверка: если файл существует и не пуст
if file_path.exists() and file_path.stat().st_size > 0:
    with open(file_path, "r", encoding="utf-8") as f:
        config = json.load(f)
else:
    config = {}  # или значения по умолчанию

# Обновление
config["app_version"] = "2.1.0"
config["debug"] = False

# Запись обратно
with open(file_path, "w", encoding="utf-8") as f:
    json.dump(config, f, indent=4)

print("Файл обновлён.")

Это распространённый паттерн при работе с конфигурационными файлами. Для предотвращения потери данных при сбое записи можно использовать временный файл и последующую замену.

Риск: при ошибке во время записи исходный файл может быть повреждён. Надёжнее записать в отдельный файл, а затем переименовать (атомарная операция на многих файловых системах).

Как записать несколько JSON-объектов в один файл (NDJSON)?

NDJSON (Newline Delimited JSON) – формат, где каждая строка является отдельным JSON-объектом. Это удобно для логов или потоковой обработки.

import json

records = [
    {"id": 1, "value": "A"},
    {"id": 2, "value": "B"},
    {"id": 3, "value": "C"}
]

with open("records.ndjson", "w", encoding="utf-8") as f:
    for record in records:
        f.write(json.dumps(record, ensure_ascii=False) + "\n")

# Чтение такого файла построчно
loaded_records = []
with open("records.ndjson", "r", encoding="utf-8") as f:
    for line in f:
        if line.strip():  # игнорируем пустые строки
            loaded_records.append(json.loads(line))

print(loaded_records)
# [{'id': 1, 'value': 'A'}, {'id': 2, 'value': 'B'}, {'id': 3, 'value': 'C'}]

Каждый объект записывается в одну строку. При чтении строки десериализуются по отдельности. Это позволяет читать файл, не загружая его целиком в память.

Проблема: если объект содержит переносы строки внутри строки, это сломает формат. В таких случаях перед записью нужно убедиться, что строки не содержат символов новой строки, или использовать экранирование (но standard JSON допускает экранирование \n внутри строк).

Расширенные примеры

Пример 1: Кастомный кодировщик для нескольких типов

Пример
import json
from datetime import datetime
from decimal import Decimal

class AdvancedEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, datetime):
            return {"__datetime__": obj.isoformat()}
        if isinstance(obj, Decimal):
            return float(obj)
        if isinstance(obj, bytes):
            return obj.decode("latin-1")
        return super().default(obj)

# Десериализация с object_hook для восстановления даты
def object_hook(dct):
    if "__datetime__" in dct:
        return datetime.fromisoformat(dct["__datetime__"])
    return dct

data = {
    "created": datetime.now(),
    "price": Decimal("19.99"),
    "raw": b"\x48\x65\x6c\x6c\x6f"
}

json_str = json.dumps(data, cls=AdvancedEncoder, indent=2)
print("Строка JSON:")
print(json_str)

# Обратное преобразование
restored = json.loads(json_str, object_hook=object_hook)
print("\nВосстановленный объект:")
print(restored)
# {'created': datetime.datetime(...), 'price': 19.99, 'raw': 'Hello'}
Результат (дата изменится):
{
  "created": {"__datetime__": "2025-02-18T14:22:00.123456"},
  "price": 19.99,
  "raw": "Hello"
}

Пояснение: object_hook вызывается для каждого вложенного словаря, что позволяет восстановить оригинальные типы.


Пример 2: Обработка большого NDJSON файла без полной загрузки в память

Пример
import json

def read_ndjson(filepath):
    """Генератор, читающий NDJSON построчно."""
    with open(filepath, "r", encoding="utf-8") as f:
        for line in f:
            line = line.strip()
            if line:
                yield json.loads(line)

# Запись 1 миллиона объектов (для примера только 3)
with open("big_data.ndjson", "w", encoding="utf-8") as f:
    for i in range(3):
        obj = {"id": i, "value": f"obj_{i}"}
        f.write(json.dumps(obj) + "\n")

# Чтение и фильтрация
for record in read_ndjson("big_data.ndjson"):
    if record["id"] % 2 == 0:
        print(f"Чётный id: {record}")
Результат:
Чётный id: {'id': 0, 'value': 'obj_0'}
Чётный id: {'id': 2, 'value': 'obj_2'}

Генераторный подход позволяет обрабатывать файлы любого размера, не загружая их целиком.


Пример 3: Использование JSON для хранения параметров приложения

Пример
import json
from pathlib import Path

DEFAULT_CONFIG = {
    "theme": "light",
    "timeout": 10,
    "show_notifications": True
}

def load_config(config_path="app_config.json"):
    path = Path(config_path)
    if path.exists():
        with open(path, "r", encoding="utf-8") as f:
            try:
                config = json.load(f)
            except json.JSONDecodeError:
                print("Файл конфигурации повреждён, используется стандартный.")
                return DEFAULT_CONFIG.copy()
        # слияние с умолчаниями (добавляем отсутствующие ключи)
        for key, value in DEFAULT_CONFIG.items():
            config.setdefault(key, value)
        return config
    else:
        return DEFAULT_CONFIG.copy()

def save_config(config, config_path="app_config.json"):
    with open(config_path, "w", encoding="utf-8") as f:
        json.dump(config, f, indent=2, ensure_ascii=False)

# Пример работы
cfg = load_config()
cfg["theme"] = "dark"
save_config(cfg)
print("Конфигурация сохранена.")

Этот шаблон гарантирует, что приложение всегда имеет валидную конфигурацию, даже если файл был создан старой версией программы.


Пример 4: Сравнение скорости записи с разными параметрами

Пример
import json
import time

big_list = [{"index": i, "data": "x" * 100} for i in range(10000)]

def benchmark(func, *args, **kwargs):
    start = time.perf_counter()
    func(*args, **kwargs)
    return time.perf_counter() - start

# Вариант 1: без отступов
t1 = benchmark(lambda: json.dump(big_list, open("no_indent.json", "w")))
# Вариант 2: с отступами
t2 = benchmark(lambda: json.dump(big_list, open("with_indent.json", "w"), indent=2))

print(f"Без indent: {t1:.3f} сек")
print(f"С indent=2: {t2:.3f} сек")
# indented версия будет значительно дольше из-за большего объёма данных
Примерный вывод:
Без indent: 0.045 сек
С indent=2: 0.180 сек

Вывод: для больших файлов не стоит использовать indent, если не требуется читаемость. В продакшене часто используют компактный формат.


Пример 5: Обработка вложенных структур с object_pairs_hook

Пример
import json
from collections import OrderedDict

json_str = '{"b": 1, "a": 2, "c": {"d": 3, "e": 4}}'

# Получение упорядоченного словаря (сохраняет порядок ключей)
result = json.loads(json_str, object_pairs_hook=OrderedDict)
print(type(result))  # <class 'collections.OrderedDict'>
print(list(result.keys()))  # ['b', 'a', 'c'] - порядок как в строке
Результат:
<class 'collections.OrderedDict'>
['b', 'a', 'c']

object_pairs_hook позволяет перехватывать пары ключ-значение до построения словаря. Полезно для контроля порядка или валидации дублирующихся ключей.

Чтение и запись JSON в файл Python (json.load, json.dump) - comments

En
Python json in py file (python)