Сохранение и загрузка 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 делает обратное преобразование.
Типичная ошибка №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 сортирует ключи по алфавиту. Это удобно для конфигурационных файлов, которые предполагается редактировать вручную.
Как сохранить русские символы без экранирования (\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-файлов, особенно если файл будет просматриваться в текстовом редакторе.
Как прочитать 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.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)
Этот подход применяется, когда нужно централизованно задать правила сериализации для разных типов.
Как обновить запись в существующем 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'}]
Каждый объект записывается в одну строку. При чтении строки десериализуются по отдельности. Это позволяет читать файл, не загружая его целиком в память.
Расширенные примеры
Пример 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 позволяет перехватывать пары ключ-значение до построения словаря. Полезно для контроля порядка или валидации дублирующихся ключей.