Зачем вообще говорить про CSV
CSV (Comma-Separated Values) — это самый старый и самый распространённый формат табличных данных. Ему примерно столько же лет, сколько и BASIC: первая публикация спецификации — 1972 год, IBM Fortran. С тех пор каждый аналитик хоть раз получал на почту файл report_2026_q1.csv с просьбой “посмотреть, что не так”. И каждый раз “не так” — это какой-нибудь один странный символ.
В мире data engineering текстовые форматы (CSV, JSON, JSONL) — это то, с чем мы сталкиваемся на границах систем: выгрузки из старых ERP, дампы из веб-форм, REST API ответы, экспорт из Google Sheets. Бинарные форматы (Parquet, Avro, ORC) живут глубоко внутри пайплайнов и хранилищ. Текстовые — на входе и выходе.
Эта пара (текст vs бинарь) — фундаментальное разделение. Текст читается человеком, отлаживается глазами, открывается в любом редакторе. Бинарь — компактный, типизированный, быстрый. Углубление по бинарным форматам — в курсе storage-formats; здесь мы фиксируем базу.
Структура CSV
CSV — это plain-text файл, где каждая строка — одна запись, поля разделены запятой, первая строка обычно содержит заголовки колонок.
order_id,customer_id,amount,currency,created_at
1001,42,150.00,USD,2026-05-17T10:32:00Z
1002,43,2300.50,EUR,2026-05-17T10:35:12Z
1003,42,99.99,USD,2026-05-17T11:00:00Z
Кажется простым. На практике в CSV кроется десяток подводных камней, и любая команда data engineering хоть раз ловила баг “у нас 1.2 млн строк, а должно быть 1.0 млн — что-то не парсится”.
Разделители
Запятая — это convention, но не закон. Разделителем может быть точка с запятой (Excel в локалях, где запятая — десятичный разделитель: Германия, Россия, Франция), табуляция (TSV, часто называют тот же CSV), пайп |, тильда. RFC 4180 фиксирует запятую как стандарт, но в реальности парсер должен уметь принять любой delimiter параметром.
import pandas as pd
df = pd.read_csv("orders.csv", sep=";") # европейский CSV
df = pd.read_csv("orders.tsv", sep="\t") # TSV
Кавычки и escape
Что если в поле есть сама запятая? Например, адрес "Москва, ул. Тверская, 7". Тогда поле оборачивается в кавычки. А если в поле есть кавычка? Тогда она удваивается: "Он сказал ""привет""".
order_id,customer_name,address
1001,"Smith, John","NYC, 5th Ave"
1002,"O""Brien","Boston, MA"
Этот механизм — quote/escape — самый частый источник багов. Аналитик выгружает CSV без кавычек, в адресе есть запятая, парсер ломается через пять часов работы, никто не понимает почему. Защита: всегда указывать quotechar='"' и escapechar явно, валидировать перед записью.
Encoding
CSV не несёт информации о кодировке. Файл может быть UTF-8, UTF-8 с BOM, Windows-1251, CP1252, ISO-8859-1. На macOS откроется как UTF-8, на старом Windows — как CP1252, и кириллица превратится в кракозябры. Правило: всегда писать UTF-8, всегда читать с явным указанием encoding="utf-8", валидировать первые байты.
Типы
В CSV всё — строка. Число 42 и строка "42" неразличимы. Дата 2026-05-17 — это строка, которую парсер должен интерпретировать. Пустое поле может быть пустой строкой, NULL, или значением “NA”. Никакого type system в CSV нет, всё на совести читающего.
От источника до целевой системы, и где обычно ломается
Плюсы CSV
CSV невозможно вытеснить, и это не баг, а фича. Причины:
- Универсальность. Любая программа открывает CSV — от Notepad до SAS до Excel до Python. Это lingua franca.
- Прозрачность. Можно открыть глазами, увидеть проблему, поправить руками.
- Простота генерации.
print("a,b,c")— уже CSV. Не нужны библиотеки. - Streaming-friendly. Файл читается построчно, без необходимости загружать целиком — критично для больших файлов.
- Diff-friendly. В git CSV меняется построчно, можно ревьюить изменения.
Минусы CSV
- Нет схемы. Типы теряются, схема живёт в README или в голове.
- Нет компрессии “из коробки”. Компрессию навешивают сверху (.csv.gz), но это всё равно текст внутри.
- Дорого парсить. Каждое число конвертируется из строки в int/float — это десятки наносекунд на значение, миллиарды значений = минуты.
- Нет nested структур. Массив или объект внутри ячейки — нет такого механизма, кроме как воткнуть JSON в строку.
- Чувствительность к разделителям, кавычкам, encoding. Один странный символ — и парсер падает или, что хуже, молча сдвигает колонки.
JSON: для вложенных структур
JSON (JavaScript Object Notation) появился в начале 2000-х как формат сериализации для JavaScript и быстро стал стандартом для REST API. В отличие от CSV, JSON изначально умеет вложенность.
{
"order_id": 1001,
"customer": {
"id": 42,
"name": "Smith, John",
"addresses": [
{"type": "billing", "city": "NYC"},
{"type": "shipping", "city": "Boston"}
]
},
"items": [
{"sku": "ABC", "qty": 2, "price": 75.00},
{"sku": "XYZ", "qty": 1, "price": 0.00}
]
}
JSON хорош для API ответов и для документ-ориентированных данных (заказ с item-ами, пользователь с настройками). Плох для табличных выгрузок: оверхэд на ключи в каждой записи раздувает файл в 3-5 раз против CSV.
Типы в JSON более выразительные: число, строка, булево, null, массив, объект. Но даты — снова просто строка, и формат даты не нормирован спецификацией.
JSONL / NDJSON: JSON для больших данных
Классический JSON — это один большой объект или массив. Прочитать 100 GB JSON-файл нельзя — нужно загрузить всё в память. Решение: JSON Lines (он же NDJSON — Newline Delimited JSON). Каждая строка — самостоятельный JSON-объект.
{"order_id":1001,"amount":150.00,"customer":{"id":42}}
{"order_id":1002,"amount":2300.50,"customer":{"id":43}}
{"order_id":1003,"amount":99.99,"customer":{"id":42}}
JSONL читается построчно, как CSV — каждая строка парсится независимо. Это любимый формат для логов (один event = одна строка), для дампов из NoSQL баз, для streaming-выгрузок из Kafka. Большинство data lake тулов (Spark, DuckDB, Athena) умеют читать JSONL напрямую.
Если приходит JSON-выгрузка размером больше 100 MB — почти всегда стоит сразу переконвертировать в JSONL. Чтение и парсинг ускорятся в разы, потому что не нужно держать весь файл в памяти.
Когда что выбирать
Берите CSV для табличного, JSONL для логов и nested, JSON только для API
Табличное, плоское
Excel, отчёты, выгрузкиCSV: табличные данные с фиксированной схемой, обмен между системами, выгрузки в Excel, ad-hoc отчёты. Если данные плоские — это CSV
Одиночные документы
API, конфигиJSON: одиночные API-ответы, конфиги, метаданные. НЕ для больших датасетов — невозможно стримить
События, логи, nested
Streaming-friendlyJSONL/NDJSON: логи, события, nested данные в большом объёме. Streaming-friendly, поддерживается Spark/DuckDB/Athena напрямую
Реальный пример: пайплайн обработки
Типичная задача: каждый день получаем CSV от партнёра с заказами, нужно сложить в data lake.
import pandas as pd
# 1. Читаем CSV с явными настройками
df = pd.read_csv(
"partner_orders_2026_05_17.csv",
sep=",",
encoding="utf-8",
quotechar='"',
dtype={"order_id": "int64", "customer_id": "int64"},
parse_dates=["created_at"]
)
# 2. Валидируем — нет ли пустых ключевых полей
assert df["order_id"].notna().all(), "Found null order_id"
# 3. Пишем в Parquet для long-term storage
df.to_parquet("s3://lake/orders/dt=2026-05-17/data.parquet")
CSV здесь — это формат транспорта: пришёл, спарсили, сложили в нормальное хранилище (Parquet). Долгосрочно CSV держать не нужно — он дорог и хрупок.
Никогда не используйте CSV как формат хранения данных в lake/warehouse. Только как формат обмена. Внутри пайплайна — Parquet, ORC, Avro. CSV умирает на больших объёмах: миллиард строк в CSV — это десятки минут парсинга, в Parquet — секунды.
Особенности обработки в больших инструментах
Spark: spark.read.csv("path", header=True, inferSchema=True) — но inferSchema сканирует данные дважды (один раз для типов, второй для чтения). На больших файлах лучше задать схему явно.
DuckDB: SELECT * FROM read_csv_auto('file.csv') — лучший в классе auto-detect, обрабатывает кавычки, encoding, дедукцию типов.
ClickHouse: INSERT INTO table FROM INFILE 'file.csv' FORMAT CSVWithNames — нативный быстрый импорт.
Snowflake / BigQuery: COPY INTO ... FROM @stage с явным указанием file_format. Промахи с разделителем/encoding — главная причина failed COPY.
Попробуй сам
- Возьми любой CSV-файл с заказами (можно сгенерировать в Excel). Открой его в текстовом редакторе. Посмотри на сырой формат — кавычки, разделители, encoding (попробуй сохранить файл в Windows-1251 и открыть в редакторе с UTF-8).
- Напиши Python-скрипт, который читает CSV и выводит для каждой колонки выведенный тип (
int,float,string,date). Подумай, как ты бы интерпретировал колонку, где 99% значений — числа, но есть пара строковых. - Сконвертируй JSON-файл (любой REST API ответ) в JSONL: одна запись на строку. Сравни размеры файлов.
- Загрузи JSONL в DuckDB:
SELECT * FROM read_json_auto('file.jsonl'). Посмотри, как DuckDB разворачивает nested структуры в колонки.