Программирование с нуля Файлы и форматы данных: текст, CSV, JSON
0%

Файлы и форматы данных: текст, CSV, JSON

Файлы и форматы данных: текст, CSV, JSON

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

Это не недоработка Python. Так устроен компьютер. В статье Как устроен компьютер мы говорили о двух видах памяти: быстрой оперативной (RAM), которая очищается при выключении, и медленном, но долговечном диске. Все переменные из статьи Переменные и типы данных живут в оперативной памяти. Чтобы данные пережили закрытие программы, их нужно записать на диск — то есть в файл.

Этим мы сегодня и займёмся. И заодно разберёмся, в каком виде данные в файл класть: просто текстом, таблицей CSV или структурой JSON.

Что такое файл на самом деле

Файл — это именованная последовательность байтов на диске. Всё. Никакой магии.

Байт, как мы помним, — это число от 0 до 255. Значит, любой файл — просто длинный ряд чисел. А чем эти числа считать, решает программа, которая файл открывает:

  • если числа договорились считать кодами символов — получится текстовый файл;
  • если числа означают яркость точек — получится картинка;
  • если это инструкции процессору — получится программа.

Расширение (.txt, .jpg, .py) — это подсказка человеку и операционной системе, а не свойство самого файла. Переименовав фото.jpg в фото.txt, вы не превратите картинку в текст: байты останутся теми же, просто блокнот попытается прочитать их как символы и покажет мусор.

Аналогия. Файл — это папка-скоросшиватель в архиве: у неё есть имя на корешке, внутри лежат страницы подряд, и её можно открыть, дочитать, дописать в конец, закрыть.

Где аналогия ломается. В бумажной папке вы можете вставить лист в середину, чуть подвинув остальные. В файле так нельзя: чтобы вставить строку в середину, нужно перезаписать весь файл целиком. Это одна из главных причин, почему программисты чаще читают файл полностью, меняют данные в памяти и пишут обратно — а не «правят на месте».

Текстовые и двоичные файлы

Все файлы делятся на две группы.

Текстовые — те, которые человек может открыть в блокноте и прочитать: .txt, .py, .csv, .json, .html. Внутри — символы, разбитые на строки.

Двоичные (binary) — все остальные: .jpg, .mp3, .zip, .exe. Открывать их блокнотом бессмысленно.

Мы будем работать только с текстовыми — с них начинают все, и на них устроено большинство форматов обмена данными.

Текстовый файл — лента символов, разрезанная переводами строк

Ключевая деталь на картинке: перевод строки — это обычный символ, просто невидимый. В коде он записывается как \n (обратный слэш и буква n). Когда вы жмёте Enter в блокноте, в файл добавляется именно он. Забыв про \n, новички получают склеенный текст в одну строку — мы это ещё увидим.

Читаем файл: open и with

Работа с файлом всегда идёт по одной схеме: открыть → прочитать или записать → закрыть. Закрывать обязательно: пока файл открыт, операционная система держит его «занятым», а записанные данные могут ещё не долететь до диска.

Чтобы не забывать про закрытие, в Python есть конструкция with. Она закрывает файл автоматически — даже если внутри произошла ошибка.

Сначала создадим файл, с которым будем работать. Запустите:

# "w" означает write — записать (создать заново)
with open("shopping.txt", "w", encoding="utf-8") as f:
    f.write("хлеб\n")
    f.write("молоко\n")
    f.write("яйца\n")

print("Файл записан")

Разберём построчно:

  • open("shopping.txt", "w", encoding="utf-8") — открываем файл. Первый аргумент — имя, второй — режим ("w" = запись), третий — кодировка (о ней ниже).
  • as f — результат кладём в переменную f. Это не сам текст, а «пульт управления файлом», по-научному файловый объект.
  • Отступ внутри with — блок, где файл открыт. Как только блок кончился, файл закрыт.
  • f.write("хлеб\n") — записывает строку. write не добавляет перевод строки сам, поэтому \n мы пишем руками.

Ожидаемый вывод: Файл записан. Рядом с вашим .py-файлом появится shopping.txt. Откройте его блокнотом — там три строки.

Теперь прочитаем его тремя способами.

Способ 1: всё целиком в одну строку

with open("shopping.txt", encoding="utf-8") as f:
    content = f.read()

print(content)
print("Длина:", len(content))
хлеб
молоко
яйца

Длина: 17

Обратите внимание: режим мы не указали — по умолчанию это "r" (read, чтение). А длина 17, а не 15: три символа \n тоже считаются.

read() удобен для маленьких файлов. Для файла на 10 гигабайт он попытается уместить всё в оперативную память — и программа упадёт.

Способ 2: построчно (так делают чаще всего)

with open("shopping.txt", encoding="utf-8") as f:
    for line in f:
        print(repr(line))
'хлеб\n'
'молоко\n'
'яйца\n'

Файловый объект можно перебирать циклом for (см. Циклы) — на каждом шаге он отдаёт одну строку. Файл читается кусочками, поэтому размер не важен.

repr(...) — команда «покажи значение так, как оно выглядит в коде». Я использовал её нарочно, чтобы стал виден \n на конце. Это и есть главная ловушка чтения файлов.

Способ 3: построчно и без \n

with open("shopping.txt", encoding="utf-8") as f:
    for number, line in enumerate(f, start=1):
        print(number, line.strip())
1 хлеб
2 молоко
3 яйца

line.strip() убирает пробелы и переводы строк с начала и конца. Правило простое: прочитали строку из файла — сразу .strip(), иначе "хлеб\n" == "хлеб" даст False, и вы полчаса будете искать, почему сравнение не работает.

enumerate(f, start=1) — помощник, который выдаёт пару «номер, значение», нумеруя с единицы.

Если нужен сразу список всех строк:

with open("shopping.txt", encoding="utf-8") as f:
    lines = [line.strip() for line in f]

print(lines)   # ['хлеб', 'молоко', 'яйца']

Режимы открытия: w стирает, a дописывает

Режим Что делает Если файла нет Если файл есть
"r" чтение (по умолчанию) ошибка FileNotFoundError читает
"w" запись создаёт стирает содержимое и пишет заново
"a" дозапись (append) создаёт добавляет в конец
"x" создание создаёт ошибка — файл уже есть

Запомните строчку про "w": она молча уничтожает старое содержимое. Ни вопроса, ни предупреждения. Это самая дорогая ошибка новичка в этой теме — открыл файл с данными в режиме "w", чтобы «дописать пару строк», и потерял всё.

# правильный способ дописать
with open("shopping.txt", "a", encoding="utf-8") as f:
    f.write("кофе\n")

with open("shopping.txt", encoding="utf-8") as f:
    print(f.read())
хлеб
молоко
яйца
кофе

Кодировка: почему encoding="utf-8" нужно писать всегда

Компьютер хранит числа, а мы хотим буквы. Кодировка — это таблица соответствия «число ↔ символ». Разные таблицы дают разные буквы для одного и того же байта.

Аналогия. Кодировка — как язык, на котором написана записка. Одна и та же последовательность букв «cola» по-русски и по-испански читается одинаково, а вот «pan» означает хлеб в Испании и сковороду в Швеции. Прочитать текст «не тем языком» — получить бессмыслицу.

Сегодня стандарт — UTF-8: в нём есть все алфавиты мира, эмодзи и почти всё остальное. Но в Windows у Python исторически может быть другая кодировка по умолчанию, поэтому один и тот же код работает у вас и ломается у коллеги. Лечится это одной привычкой:

Всегда пишите encoding="utf-8" при открытии текстового файла. Всегда.

Если кодировка не та, вы увидите:

UnicodeDecodeError: 'utf-8' codec can't decode byte 0xcf in position 0: invalid continuation byte

Дословно: «кодек utf-8 не может расшифровать байт 0xcf на позиции 0». Значит, файл записан в другой кодировке — чаще всего это старая русская cp1251 (она же Windows-1251). Попробуйте encoding="cp1251".

Пути к файлам: где программа ищет shopping.txt

Имя "shopping.txt" — это относительный путь: файл ищется в текущей рабочей директории, то есть в той папке, откуда программу запустили. Не там, где лежит .py-файл, а именно откуда запустили — это разные вещи, и на этом спотыкаются все.

Абсолютный путь начинается от корня диска и не зависит ни от чего: /home/anna/data/shopping.txt в Linux и macOS, C:\Users\Anna\data\shopping.txt в Windows.

В Windows обратный слэш в Python-строке — спецсимвол, поэтому пишите либо "C:\\Users\\Anna" (двойной), либо r"C:\Users\Anna" (буква r перед кавычкой отключает спецсимволы).

Удобнее пользоваться модулем pathlib, который сам разбирается с различиями систем:

from pathlib import Path

p = Path("shopping.txt")
print(p.exists())    # True — файл существует?
print(p.suffix)      # .txt — расширение

# прочитать файл целиком одной командой
print(Path("shopping.txt").read_text(encoding="utf-8").splitlines())
# ['хлеб', 'молоко', 'яйца', 'кофе']

splitlines() разрезает текст по переводам строк и сразу отдаёт список без \n. Документация: docs.python.org/3/library/pathlib.html.

Три типичные ошибки и что они означают дословно

FileNotFoundError: [Errno 2] No such file or directory: 'shoping.txt'

«Нет такого файла или каталога». Проверьте: опечатку в имени (shoping вместо shopping), из какой папки запускаете программу, не забыли ли расширение. В 90 % случаев это опечатка или неверная рабочая директория.

PermissionError: [Errno 13] Permission denied: 'report.csv'

«Доступ запрещён». Обычно файл открыт в Excel — Windows не даёт писать в занятый файл. Закройте программу, которая его держит.

IsADirectoryError: [Errno 21] Is a directory: 'data'

«Это каталог». Вы пытаетесь открыть папку как файл — вероятно, забыли дописать имя файла к пути.

Простой текст — это мало. Знакомьтесь: CSV

Пока мы хранили список слов, простого текста хватало. Но данные редко бывают такими плоскими. Допустим, вы записываете расходы: дата, категория, сумма, комментарий. Как уместить четыре значения в одну строку файла?

Ответ очевиден: разделить их каким-нибудь символом. Именно это и делает формат CSVComma-Separated Values, «значения, разделённые запятыми».

Анатомия CSV-файла: заголовок, строки, разделители, кавычки

CSV — это по-прежнему обычный текстовый файл. Его открывает блокнот. Его же открывает Excel, Google Sheets и любая программа для аналитики — поэтому CSV стал универсальным языком обмена таблицами.

Создадим файл purchases.csv со строкой заголовка и пятью записями:

import csv

rows = [
    ["дата", "категория", "сумма", "комментарий"],
    ["2026-03-01", "еда", "540.50", "обед"],
    ["2026-03-02", "транспорт", "60", "метро"],
    ["2026-03-03", "еда", "1200.00", "кафе, с другом"],
    ["2026-03-04", "развлечения", "900", "кино"],
    ["2026-03-05", "еда", "310", "продукты"],
]

with open("purchases.csv", "w", encoding="utf-8", newline="") as f:
    writer = csv.writer(f)
    writer.writerows(rows)

print("Готово")
  • import csv — подключаем встроенный модуль для работы с CSV. Он идёт в комплекте с Python, устанавливать ничего не надо.
  • newline="" — обязательный аргумент именно при работе с csv. Без него в Windows между строками появятся лишние пустые строки. Правило: файл под csv открываем с newline="".
  • csv.writer(f) — создаёт «писателя», который умеет превращать списки в строки CSV.
  • writerows(rows) — записывает сразу все строки.

Содержимое получившегося файла:

дата,категория,сумма,комментарий
2026-03-01,еда,540.50,обед
2026-03-02,транспорт,60,метро
2026-03-03,еда,1200.00,"кафе, с другом"
2026-03-04,развлечения,900,кино
2026-03-05,еда,310,продукты

Заметили кавычки вокруг "кафе, с другом"? Модуль сам увидел запятую внутри значения и защитил её кавычками, чтобы поле не развалилось на два. Это и есть главная причина не парсить CSV вручную через line.split(","): ваш самодельный разбор сломается на первом же комментарии с запятой.

Чтение CSV: csv.reader и csv.DictReader

import csv

with open("purchases.csv", encoding="utf-8", newline="") as f:
    reader = csv.reader(f)
    for row in reader:
        print(row)
['дата', 'категория', 'сумма', 'комментарий']
['2026-03-01', 'еда', '540.50', 'обед']
['2026-03-02', 'транспорт', '60', 'метро']
['2026-03-03', 'еда', '1200.00', 'кафе, с другом']
['2026-03-04', 'развлечения', '900', 'кино']
['2026-03-05', 'еда', '310', 'продукты']

Каждая строка превратилась в список. Кавычки убраны, запятая внутри поля сохранена — модуль всё сделал правильно.

Но обращаться к данным как row[2] неудобно и опасно: добавят столбец в начало — и все номера съедут. Гораздо лучше csv.DictReader: он берёт первую строку как имена столбцов и отдаёт каждую запись словарём (словари мы разбирали в статье Списки, словари и множества).

import csv

with open("purchases.csv", encoding="utf-8", newline="") as f:
    for row in csv.DictReader(f):
        print(row["дата"], row["категория"], row["сумма"])
2026-03-01 еда 540.50
2026-03-02 транспорт 60
2026-03-03 еда 1200.00
2026-03-04 развлечения 900
2026-03-05 еда 310

Самая частая ошибка с CSV: всё приходит текстом

row["сумма"] + 100
TypeError: can only concatenate str (not "int") to str

Дословно: «строку можно склеивать только со строкой, а не с целым числом». CSV — текстовый формат, он не хранит типы. Число 540.50 в файле — это четыре символа плюс точка, а не дробное число. Преобразуйте явно: float(row["сумма"]) или int(row["сумма"]).

Посчитаем расходы по категориям:

import csv

totals = {}   # пустой словарь: категория -> сумма

with open("purchases.csv", encoding="utf-8", newline="") as f:
    for row in csv.DictReader(f):
        category = row["категория"]
        amount = float(row["сумма"])          # текст -> число
        totals[category] = totals.get(category, 0) + amount

for category, total in totals.items():
    print(f"{category}: {total:.2f}")
еда: 2050.50
транспорт: 60.00
развлечения: 900.00

totals.get(category, 0) — «дай значение по ключу, а если ключа ещё нет, дай 0». Без этого на первой же новой категории программа упала бы с KeyError.

{total:.2f} внутри f-строки означает «выведи число с двумя знаками после запятой».

Запись CSV из словарей

import csv

report = [
    {"категория": "еда", "потрачено": 2050.5},
    {"категория": "транспорт", "потрачено": 60.0},
]

with open("report.csv", "w", encoding="utf-8", newline="") as f:
    writer = csv.DictWriter(f, fieldnames=["категория", "потрачено"])
    writer.writeheader()          # пишет строку заголовка
    writer.writerows(report)

fieldnames задаёт и имена столбцов, и их порядок. Если в словаре окажется ключ, которого нет в fieldnames, Python сообщит об этом ошибкой — и это хорошо: лучше упасть, чем молча потерять столбец.

Когда таблицы мало: JSON

CSV прекрасен, пока данные — плоская таблица. Но попробуйте уместить в таблицу такое: у пользователя есть имя, возраст, список хобби и вложенный адрес с городом и улицей. Сколько столбцов отвести под хобби? Три? А если их пять?

Для вложенных данных используют JSONJavaScript Object Notation. Несмотря на название, к JavaScript вы привязаны не будете: JSON понимают все языки, и это сегодня главный формат обмена данными между программами. Когда мобильное приложение спрашивает у сервера погоду (см. Как работают интернет и сайты), ответ почти наверняка приходит в JSON.

Выглядит он так:

{
  "имя": "Аня",
  "возраст": 29,
  "любит_кофе": true,
  "хобби": ["бег", "фото"],
  "адрес": null
}

Если вам это напомнило словарь Python — вы правы, сходство почти полное. JSON строится из тех же кирпичиков:

Соответствие типов Python и JSON:

Python JSON Примечание
dict объект {} ключи в JSON — только строки
list массив []
str строка только двойные кавычки
int, float число
True / False true / false с маленькой буквы
None null

Три отличия от Python, из-за которых чаще всего ломается JSON, написанный руками: только двойные кавычки, true/false/null с маленькой буквы, и запятая после последнего элемента запрещена.

Записываем и читаем JSON

import json

user = {
    "имя": "Аня",
    "возраст": 29,
    "любит_кофе": True,
    "хобби": ["бег", "фото"],
    "адрес": None,
}

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

print("Сохранено")
  • json.dump(данные, файл, ...) — записать структуру в файл.
  • ensure_ascii=Falseважно для русского языка. Без него Python запишет кириллицу кодами вида имя. Файл останется правильным JSON, но человек его не прочитает.
  • indent=2 — красивое форматирование с отступом в 2 пробела. Без него всё склеится в одну длинную строку (что экономит место, но нечитаемо).

Файл user.json:

{
  "имя": "Аня",
  "возраст": 29,
  "любит_кофе": true,
  "хобби": [
    "бег",
    "фото"
  ],
  "адрес": null
}

Обратное чтение — одна команда:

import json

with open("user.json", encoding="utf-8") as f:
    data = json.load(f)

print(type(data))                # <class 'dict'>
print(data["имя"])               # Аня
print(data["возраст"] + 1)       # 30  — это настоящее число!
print(data["хобби"][0])          # бег

Главное преимущество перед CSV: data["возраст"] — это число, а не текст. JSON сохраняет типы, преобразовывать ничего не надо.

Четыре команды модуля легко перепутать, вот шпаргалка:

Команда Что делает
json.dump(данные, файл) Python → файл
json.load(файл) файл → Python
json.dumps(данные) Python → строка (буква s = string)
json.loads(строка) строка → Python

Версии со s нужны, когда JSON приходит не из файла, а, например, из интернета.

Ошибки JSON дословно

json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes: line 1 column 2 (char 1)

«Ожидалось имя свойства в двойных кавычках, строка 1, столбец 2». Классика: в JSON использованы одинарные кавычки {'имя': 'Аня'}. Замените на двойные.

json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

«Ожидалось значение в самом начале». Обычно файл пустой или вы читаете не то, что думаете.

TypeError: Object of type set is not JSON serializable

«Объект типа множество нельзя превратить в JSON». В JSON нет множеств, дат и ваших собственных объектов — только шесть типов из таблицы выше. Множество превращают в список: list(мое_множество), дату — в строку.

Как выбрать формат

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

Стоит знать ещё пару имён, которые вы встретите: XML — старший родственник JSON, многословнее, до сих пор живёт в корпоративных системах; YAML — формат для файлов конфигурации, читается как обычный текст с отступами; Parquet — двоичный формат для больших аналитических данных.

Собираем всё вместе: отчёт из CSV в JSON

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

import csv
import json

totals = {}

# 1. Читаем таблицу и считаем суммы по категориям
with open("purchases.csv", encoding="utf-8", newline="") as f:
    for row in csv.DictReader(f):
        category = row["категория"]
        amount = float(row["сумма"])
        totals[category] = round(totals.get(category, 0) + amount, 2)

# 2. Собираем отчёт: общая сумма плюс разбивка
result = {
    "всего": round(sum(totals.values()), 2),
    "по_категориям": totals,
}

# 3. Сохраняем в JSON
with open("report.json", "w", encoding="utf-8") as f:
    json.dump(result, f, ensure_ascii=False, indent=2)

print("Отчёт готов:", result["всего"])

Вывод в консоль: Отчёт готов: 3010.5, а в файле report.json:

{
  "всего": 3010.5,
  "по_категориям": {
    "еда": 2050.5,
    "транспорт": 60.0,
    "развлечения": 900.0
  }
}

Двадцать строк кода — и вы сделали то, за что в реальной работе платят деньги. round(..., 2) здесь не для красоты: дробные числа в компьютере хранятся приближённо, и без округления вы рискуете увидеть в отчёте 2050.4999999999995. Для денег в серьёзных проектах используют специальный тип decimal.Decimal, но это тема для другого раза.

Правила, которые спасут вам нервы

  1. Всегда with. Никаких f = open(...) без закрытия.
  2. Всегда encoding="utf-8". Одна привычка, ноль проблем с кириллицей.
  3. Помните про "w". Он стирает файл молча. Дописывать — только "a".
  4. .strip() после чтения строки. Невидимый \n — источник половины загадочных багов.
  5. CSV — только через модуль csv, и всегда с newline="".
  6. CSV возвращает текст. Хотите считать — преобразуйте через int() или float().
  7. JSON с русским — только с ensure_ascii=False.
  8. Не правьте важные файлы «на месте». Прочитали, изменили в памяти, записали в новый файл, потом заменили старый. Если программа упадёт посередине, у вас останется целый оригинал.

Практика

Задания идут от простого к сложному. Данные для них — файл purchases.csv из статьи; если его нет, создайте кодом из раздела про CSV.

1. Личный дневник. Напишите программу, которая спрашивает у пользователя одну строку (input()) и дописывает её в файл diary.txt вместе с переводом строки. Запустите три раза подряд и убедитесь, что все три записи на месте. Подсказка: режим "a", и не забудьте \n в конце.

2. Считаем строки и слова. Прочитайте diary.txt и выведите: сколько в нём строк и сколько всего слов. Подсказка: для слов пригодится line.split() — без аргументов он режет строку по пробелам и возвращает список.

3. Фильтр по категории. Прочитайте purchases.csv и создайте food.csv, куда попадут только записи с категорией «еда» — вместе со строкой заголовка. Подсказка: csv.DictReader для чтения, csv.DictWriter с fieldnames=reader.fieldnames для записи. У объекта DictReader есть свойство .fieldnames — это как раз список имён столбцов.

4. Самая дорогая покупка. Найдите в purchases.csv запись с наибольшей суммой и выведите её дату, категорию и сумму. Подсказка: заведите переменную most_expensive = None, идите циклом по строкам, сравнивайте float(row["сумма"]) с текущим максимумом. Сравнивать текст нельзя: "900" > "1200.00" даст True, потому что строки сравниваются посимвольно, а 9 больше 1.

5. Счётчик запусков (со звёздочкой). Сделайте программу, которая при каждом запуске увеличивает счётчик на единицу и печатает: «Вы запустили меня N раз». Счётчик храните в state.json. Подсказка: при первом запуске файла ещё нет — оберните чтение в try / except FileNotFoundError и в этом случае начинайте с нуля. Конструкцию try подробно разберём в следующей статье, а пока используйте её как готовый рецепт:

try:
    with open("state.json", encoding="utf-8") as f:
        state = json.load(f)
except FileNotFoundError:
    state = {"запусков": 0}

Мини-итог

Файл — это последовательность байтов на диске, и он позволяет данным пережить закрытие программы. Текстовые файлы читают и пишут через with open(...), обязательно указывая encoding="utf-8", и помнят, что режим "w" стирает содержимое, а строки приходят с \n на конце.

Когда данные становятся таблицей — берут CSV и работают с ним через модуль csv, помня, что все значения приходят текстом. Когда данные вложенные — берут JSON, который сохраняет и структуру, и типы, и требует ensure_ascii=False для кириллицы.

Дальше — официальная документация, она короткая и полезная: чтение и запись файлов, модуль csv, модуль json. Спецификация формата JSON на одной странице: json.org/json-ru.html.

Что дальше

Вы наверняка заметили: в этой статье сообщений об ошибках было больше, чем в любой предыдущей. Это не совпадение — как только программа начинает трогать внешний мир (файлы, сеть, ввод пользователя), всё начинает ломаться по-настоящему. Файла нет, кодировка не та, в столбце с числом оказалось слово.

Пора научиться не бояться красного текста в консоли, а читать его как инструкцию.

Ошибки и отладка: как читать сообщения и находить причину

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов