deшка
Урок 3.10

Файлы и форматы: CSV, JSON, JSON Lines, Parquet

Кодировки и BOM, with, почему CSV нельзя разбирать через split, JSON и JSON Lines, где начинается Parquet и как читать файл, который больше памяти.

Файлы это главный источник и главный приёмник данных для скриптов инженера: выгрузка из старой системы в CSV, события из очереди в JSON Lines, витрина в Parquet на S3. Ошибки здесь редко выглядят как исключения. Чаще это файл, который прочитался «успешно», но с кракозябрами вместо кириллицы, со сдвинутыми колонками из-за запятой в адресе или с датой, которая стала строкой. Урок про то, как читать и писать файлы так, чтобы результат совпадал с содержимым.

Открытие файла: кодировка и with

with open(path, encoding="utf-8") as f:
    for line in f:
        ...

Две вещи в этой строке обязательны. encoding="utf-8" явно: без него Python берёт кодировку системы, и на Windows это cp1251, а в контейнере на сервере UTF-8. Скрипт, который работает у вас и падает в Airflow с UnicodeDecodeError, почти всегда про это. Если файл пришёл из Excel или старой 1С, кодировка может быть cp1251, и это нужно знать, а не угадывать: chardet угадывает плохо.

with гарантирует закрытие файла при выходе из блока, в том числе при исключении. Без него файл закроется, когда объект соберёт сборщик мусора, то есть неизвестно когда; на записи это означает, что последний буфер может не долететь до диска до конца программы.

Файлы из Windows часто начинаются с BOM, трёх невидимых байт EF BB BF. При чтении с encoding="utf-8" они превращаются в символ  в начале первой строки, и первая колонка называется id вместо id. Кодировка utf-8-sig убирает BOM при чтении и не добавляет при записи:

with open(path, encoding="utf-8-sig") as f:
    header = f.readline().rstrip("\n").split(",")

Переводы строк: Python при чтении в текстовом режиме приводит \r\n к \n, при записи наоборот подставляет системный. Для CSV это важно, ниже будет newline="".

CSV нельзя разбирать через split

CSV выглядит просто, пока в поле нет запятой, кавычки или перевода строки. Адрес "Москва, ул. Ленина, 1" в кавычках это одно поле с двумя запятыми, и line.split(",") развалит его на три. Модуль csv знает правила кавычек:

import csv
 
with open(path, encoding="utf-8", newline="") as f:
    reader = csv.DictReader(f)
    for row in reader:
        print(row["order_id"], row["amount"])

DictReader берёт имена колонок из первой строки и отдаёт словари. newline="" обязателен по документации csv: модуль сам обрабатывает переводы строк внутри полей в кавычках, а текстовый режим без этого параметра их портит.

Все значения из CSV это строки. "100" не число, "" не None, "2026-07-01" не дата. Преобразование типов это ваша работа, и делать её нужно явно, с понятной ошибкой на плохом значении:

def parse_order(row):
    return {
        "order_id": int(row["order_id"]),
        "amount": int(row["amount"]) if row["amount"] != "" else None,
        "city": row["city"].strip() or None,
    }

Разделитель бывает не запятой: ; в выгрузках из русского Excel, табуляция в TSV. Это параметр delimiter. Другие параметры диалекта (quotechar, escapechar) нужны редко, но если файл странный, csv.Sniffer может помочь угадать.

Запись симметрична: csv.DictWriter с fieldnames и writeheader(). Он сам поставит кавычки там, где в значении есть разделитель.

JSON и JSON Lines

json.load(f) читает весь файл в объект, json.dump(obj, f) записывает. Для конфигов и ответов API этого достаточно. Два параметра, которые стоит помнить: ensure_ascii=False при записи, иначе кириллица превратится в Мо..., и default=str для объектов, которые JSON не умеет сериализовать (даты, Decimal).

JSON хранит типы: число останется числом, null станет None, true станет True. Но он не гарантирует ваш контракт: поле может отсутствовать, быть null вместо числа или строкой "100" вместо 100. Проверка на входе нужна так же, как для CSV.

Для потока событий один большой JSON-массив неудобен: его нельзя читать построчно и нельзя дописывать. Поэтому логи и события хранят в JSON Lines: один объект на строку.

import json
 
def read_events(path):
    with open(path, encoding="utf-8") as f:
        for number, line in enumerate(f, start=1):
            line = line.strip()
            if not line:
                continue
            try:
                yield json.loads(line)
            except json.JSONDecodeError as e:
                raise ValueError(f"строка {number}: {e}") from e

Читается по одной строке, память постоянная, битая строка даёт понятную ошибку с номером. Запись: f.write(json.dumps(event, ensure_ascii=False) + "\n").

Parquet: где заканчиваются текстовые форматы

CSV и JSON это текст: каждое число хранится как строка цифр, типов нет, сжатие слабое, чтобы прочитать одну колонку, нужно прочитать весь файл. Для витрин и озера данных используют Parquet: колоночный бинарный формат со схемой, сжатием и статистикой по блокам. Файл в Parquet обычно в 5–10 раз меньше того же CSV, а запрос по двум колонкам из пятидесяти читает только их.

В Python с Parquet работают через pyarrow:

import pyarrow.parquet as pq
 
table = pq.read_table("orders.parquet", columns=["order_id", "amount"])
df = table.to_pandas()
 
pq.write_table(table, "out.parquet", compression="zstd")

Схема хранится в файле: int64, timestamp[ms], string. Даты остаются датами, null остаётся null. Это снимает целый класс ошибок приведения, но добавляет другой: схема должна совпадать между файлами одной витрины, и int32 в одном файле с int64 в другом сломает чтение папки целиком. Для больших файлов Parquet читают по row group (pq.ParquetFile(path).iter_batches()), а не целиком.

В браузерной песочнице pyarrow не загружается, поэтому задачи ниже про CSV и JSON Lines. С Parquet вы встретитесь в модулях про S3 и Spark.

Файл больше памяти

Правило из урока про память работает для любого формата: читать по частям и не накапливать. Для текстовых файлов это построчный цикл, для CSV csv.reader поверх него, для JSON Lines построчный json.loads, для Parquet итерация по row group. Единственный формат, который нельзя читать частями стандартными средствами, это один большой JSON-массив: для него нужен потоковый парсер (ijson) или смена формата у источника.

Запись тоже потоковая: открыть файл один раз, писать по строке, закрыть. Собирать список строк и потом writelines это тот же список в памяти.

Ещё одно правило для записи: писать во временный файл рядом и переименовывать по завершении (os.replace). Иначе упавший на середине скрипт оставит половину файла, и следующий шаг пайплайна прочитает её как полную выгрузку.

Как отвечать на собеседовании

Как читать CSV в Python? Модулем csv, с явной кодировкой и newline="": он правильно разбирает кавычки и разделители внутри полей. Все значения приходят строками, типы приводятся явно.

Чем JSON Lines отличается от JSON? Один объект на строку: можно читать построчно и дописывать в конец, что нужно для логов и событий. Обычный JSON читается только целиком.

Зачем Parquet, если есть CSV? Колоночный формат со схемой и сжатием: меньше места, чтение только нужных колонок, типы сохраняются. Работают с ним через pyarrow, читают по row group.

Что такое BOM и utf-8-sig? Три байта в начале файла из Windows, которые при чтении в utf-8 становятся символом в имени первой колонки. utf-8-sig убирает их при чтении.

Практика

python 3.12 · песочницарешено 0 / 3

Прочитайте orders.csv функцией read_orders(path) и верните список словарей с типами: order_id как int, city как str, amount как int или None, если поле пустое. В городе может быть запятая внутри кавычек.

Заготовка разбирает строки через split и ломает запись с адресом «Москва, центр». Используйте модуль csv.

оболочка, не редактируется
import csv
Ctrl+Enter
ТЕСТЫ
ВводВаш выводОжидаемый вывод
for row in read_orders("orders.csv"): print(row)—{'order_id': 1, 'city': 'Москва, центр', 'amount': 100} {'order_id': 2, 'city': 'Тверь', 'amount': None} {'order_id': 3, 'city': 'Казань', 'amount': 250}
rows = read_orders("orders.csv") print(sum(r["amount"] for r in rows if r["amount"] is not None), type(rows[0]["order_id"]).__name__)—350 int
решённые задания сохранятся в прогресс после входа
Войти, чтобы отмечатьдальше →
3.11 →Базы из Python: psycopg, параметры, батчи и транзакции