Перейти к содержанию
Learning Platform
Глоссарий Troubleshooting
Урок 05.02 · 22 мин
Начальный
CSVTSVRFC 4180EncodingPandas

CSV: «простой» формат, в котором всё непросто

CSV выглядит как примитивный формат: строки разделены \n, поля — запятыми. На самом деле это самый «грязный» формат в data engineering — нет одного стандарта (RFC 4180 — best-effort, не обязательный), нет встроенного encoding, дата в одном файле может быть 2026-05-15, в другом 15.05.2026, в третьем 15/05/26. Никаких типов: всё — строки. Junior работающий с CSV впервые столкнётся со всеми возможными граблями.

В этом уроке разберём RFC 4180, диалекты, экранирование, парсинг через csv module, использование pandas, и подводные камни encoding/BOM/дат.


RFC 4180: формальный (но не обязательный) стандарт

RFC 4180 — это «common format» для CSV, опубликован в 2005 году. Он не нормативный (informational), а описательный — фиксирует то, как Excel и большинство инструментов работают.

Основные правила:

  • Поля разделены запятыми.
  • Записи разделены CRLF (\r\n).
  • Поле может быть в двойных кавычках. Внутри кавычек — любые символы, включая запятые и newlines.
  • Двойная кавычка внутри quoted-поля экранируется удвоением: "He said ""hello""".
  • Первая строка может быть header (опционально).
name,age,city
Alice,30,Moscow
"Smith, John",25,"New York"
"He said ""hi""",40,SPb

В реальности у вас будет 1000 файлов, и почти ни один не следует RFC 4180 полностью:

  • Разделители: запятая, точка с запятой (Excel в Европе, где запятая — десятичный разделитель), tab (TSV), pipe |.
  • Newlines: \r\n (RFC), \n (Unix), \r (старый Mac).
  • Кавычки: иногда single, иногда отсутствуют, иногда обязательны для всех полей.
  • Header: иногда есть, иногда нет.
  • Encoding: чаще всего UTF-8, но cp1251 (Windows-1251) для русских файлов из старых систем.
Где сломается простой парсер

Грабли, которые возникают при наивном парсинге через split(',')

Smith, JohnЗапятая внутри поля. Без quoting split вернёт 4 поля вместо 3. Корректно: поле должно быть в кавычках 'Smith, John'
Multi-lineNewline внутри поля. Если поле в кавычках -- допустимо, иначе ломает построчное чтение. Часто встречается в полях с описаниями, адресами
He said "hi"Кавычка внутри поля. По RFC должна быть удвоена: He said ""hi""
 header...BOM (Byte Order Mark) -- три байта 0xEF 0xBB 0xBF в начале UTF-8 файла. Excel часто пишет с BOM. Парсер увидит первый column как 'header' с невидимыми символами в начале
cp1251 vs UTF-8Файл с русскими словами в кодировке Windows-1251. Открыли как UTF-8 -- получили mojibake (Алиса вместо Алиса). Нужно знать или угадать encoding
01.05.26 vs 2026-01-05Даты без типизации. Один файл -- DD.MM.YY, другой -- ISO 8601, третий -- Excel serial number (45657). Парсер не угадает

Поэтому работа с CSV — это не «прочитать строки». Это «распознать формат, выбрать парсер с правильными настройками, обработать ошибки, attendant to encoding».


Python csv module: базовый инструмент

Stdlib csv — это правильный выбор для простой работы с CSV. Он понимает quoting, escape, разные диалекты.

import csv

# Запись
with open("users.csv", "w", encoding="utf-8", newline="") as f:
    writer = csv.writer(f)
    writer.writerow(["name", "age", "city"])
    writer.writerow(["Alice", 30, "Moscow"])
    writer.writerow(["Smith, John", 25, "New York"])  # запятая в имени
    # Файл:
    # name,age,city
    # Alice,30,Moscow
    # "Smith, John",25,New York

# Чтение
with open("users.csv", encoding="utf-8") as f:
    reader = csv.reader(f)
    header = next(reader)
    for row in reader:
        print(dict(zip(header, row)))
        # {'name': 'Alice', 'age': '30', 'city': 'Moscow'}
        # Заметьте: age -- string, не int!

# DictReader/DictWriter -- удобнее, header автоматически
with open("users.csv", encoding="utf-8") as f:
    reader = csv.DictReader(f)
    for row in reader:
        print(row['name'], row['city'])

newline="" критичен. По спецификации csv-модуль сам управляет newlines. Если открыть с дефолтным newline= (преобразование linebreaks по platform), при чтении CSV с многострочными полями получите битые данные.


Диалекты: предустановки для разных форматов

Csv-module поддерживает «диалекты» — наборы параметров для разных форматов.

import csv

# Список встроенных диалектов
print(csv.list_dialects())  # ['excel', 'excel-tab', 'unix']

# excel -- default. Comma-separated, double-quote quoting
# excel-tab -- TSV. Tab-separated, double-quote quoting
# unix -- comma-separated, all fields quoted, LF newlines

# Использовать
with open("data.tsv", encoding="utf-8") as f:
    reader = csv.reader(f, dialect='excel-tab')
    # Эквивалентно: csv.reader(f, delimiter='\t')

# Кастомный диалект -- для европейского CSV (semicolon-separated)
csv.register_dialect('eu', delimiter=';', quotechar='"', quoting=csv.QUOTE_MINIMAL)
with open("eu_data.csv") as f:
    reader = csv.reader(f, dialect='eu')

# Или через параметры напрямую
reader = csv.reader(f, delimiter=';', quotechar='"')

Важные параметры:

  • delimiter: разделитель полей (, ; \t |).
  • quotechar: символ quoting (" или ').
  • quoting: когда quoting’ать. QUOTE_MINIMAL (только если нужно), QUOTE_ALL (все поля), QUOTE_NONE (никогда — ломается на запятых в значениях).
  • escapechar: альтернатива удвоению кавычек. Если задан, кавычка внутри значения экранируется через этот символ (например \).

Sniffer: угадывание диалекта

Если вы не знаете формат заранее, csv.Sniffer анализирует первые байты и угадывает.

import csv

with open("unknown.csv", encoding="utf-8") as f:
    sample = f.read(8192)  # первые 8 КБ для анализа
    f.seek(0)

    sniffer = csv.Sniffer()
    dialect = sniffer.sniff(sample)
    print(f"Delimiter: '{dialect.delimiter}'")
    print(f"Quote char: '{dialect.quotechar}'")
    print(f"Has header: {sniffer.has_header(sample)}")

    reader = csv.reader(f, dialect)
    for row in reader:
        print(row)

Sniffer не безошибочен (может перепутать tab и пробел, не справиться с экзотическими форматами), но для типичных CSV работает.


Pandas read_csv: для tabular данных

Для аналитики и больших файлов лучше pandas. read_csv — мощный, но и сложный (десятки параметров).

import pandas as pd

# Базовое чтение
df = pd.read_csv("data.csv")

# С контролем типов
df = pd.read_csv(
    "data.csv",
    encoding="utf-8",         # Encoding файла. По умолчанию utf-8
    sep=",",                   # Разделитель. ',' default. Для TSV: '\t'
    header=0,                  # Какая строка -- header. 0 (default) = первая. None = нет header
    names=["a", "b", "c"],    # Кастомные имена колонок (если header=None)
    dtype={"id": "int64", "name": "string"},  # Типы колонок
    parse_dates=["created_at"],  # Парсить колонку как datetime
    na_values=["", "N/A", "null", "-"],  # Что считать NaN
    encoding_errors="replace",  # Что делать с битыми символами: replace, ignore, strict
    chunksize=10_000,          # Читать по 10k rows за раз -- для больших файлов
)

# Запись
df.to_csv(
    "out.csv",
    index=False,               # Не писать row index как колонку
    encoding="utf-8",
    date_format="%Y-%m-%d",   # Формат дат при выводе
    quoting=csv.QUOTE_MINIMAL,
)

Главные параметры, которые junior часто упускает:

  • dtype: без него pandas угадывает типы по первым строкам, что может быть медленно и неправильно (например, ID 12345 будет int, а ID 12345-X приведёт к dtype object для всей колонки). Указание dtype={"id": "string"} экономит время.
  • parse_dates: дата как строка занимает больше места и медленнее в операциях. Парсинг сразу в datetime — быстрее и удобнее.
  • na_values: что считать missing. Дефолт — '', 'NA', 'NaN', 'null' и ещё ~20 значений. Если в данных есть '-' или 'unknown' — добавить в na_values.
  • chunksize: для файлов больше памяти. Возвращает iterator чанков, не df.

BOM: невидимая засада

UTF-8 BOM (Byte Order Mark) — три байта 0xEF 0xBB 0xBF в начале файла. UTF-8 не нуждается в BOM (порядок байт в UTF-8 фиксирован), но Excel при сохранении CSV в UTF-8 ставит BOM, чтобы при открытии распознать кодировку.

Python по умолчанию не обрабатывает BOM. При чтении первой строки она будет начинаться с  — невидимый символ, который ломает имя первой колонки.

# Открыть файл с BOM как обычный utf-8
with open("excel_export.csv", encoding="utf-8") as f:
    header = next(csv.reader(f))
    print(repr(header))
    # ['name', 'age', 'city']  -- первая колонка испорчена!
    # df['name'] -- KeyError, потому что колонка называется 'name'

# Решение: использовать utf-8-sig -- automatic BOM stripping
with open("excel_export.csv", encoding="utf-8-sig") as f:
    header = next(csv.reader(f))
    print(repr(header))
    # ['name', 'age', 'city']  -- OK

# В pandas
df = pd.read_csv("excel_export.csv", encoding="utf-8-sig")

utf-8-sig — это «utf-8 with stripping BOM if present, no BOM written by default». Безопасно использовать всегда — лишний BOM не появится, существующий уберётся.


Encoding: главная боль с русскими файлами

Когда вы получаете CSV из неизвестного источника — encoding неизвестен. Если ошибётесь — получите mojibake (нечитаемый текст).

Распространённые encodings
utf-8Универсальная кодировка. Все Unicode символы. Default в Python 3, Linux, modern web. Если данные нормальные кириллические -- это самый вероятный кандидат
utf-8-sigUTF-8 с автоматической обработкой BOM. Используйте для файлов из Excel
cp1251 / Windows-1251Кодировка Windows для русского текста. Старые системы (1С, Windows-приложения, экспорты из Word) часто отдают именно это. Один байт = один символ для кириллицы
koi8-rСтарая русская кодировка для Unix. Сейчас почти не встречается, но в legacy системах возможна

Обнаружение encoding — два пути:

1. Детектор chardet/charset_normalizer.

# pip install charset-normalizer  (или chardet -- оба работают)
from charset_normalizer import detect

with open("unknown.csv", "rb") as f:
    raw = f.read(10_000)  # первые 10 КБ
result = detect(raw)
print(result)
# {'encoding': 'cp1251', 'confidence': 0.95, 'language': 'Russian'}

# Использовать
df = pd.read_csv("unknown.csv", encoding=result['encoding'])

2. Попробовать по очереди.

encodings = ['utf-8-sig', 'utf-8', 'cp1251', 'koi8-r', 'latin-1']
for enc in encodings:
    try:
        df = pd.read_csv("unknown.csv", encoding=enc)
        # Проверка: если decoded успешно и данные выглядят osmysleno
        print(f"OK with {enc}: {df.head()}")
        break
    except UnicodeDecodeError:
        continue

latin-1 (= ISO-8859-1) никогда не даёт UnicodeDecodeError — каждый байт мапится в один символ. Это удобно как fallback, но текст будет читаться как мусор. Используйте только если ничего другое не работает.

WARNING

Если конвертируете cp1251 -> utf-8 для дальнейшей обработки — делайте это на raw bytes перед csv-парсингом, а не побайтово при чтении csv. Open с правильным encoding и потом всё хранить в utf-8 (внутренний формат Python).


Даты: всё — строки, пока вы не сказали обратное

В CSV типов нет. Дата 2026-05-15 — это строка '2026-05-15', и её надо явно парсить.

import pandas as pd

# Без parse_dates -- всё string
df = pd.read_csv("data.csv")
print(df['created_at'].dtype)  # object (string)

# С parse_dates -- datetime64
df = pd.read_csv("data.csv", parse_dates=['created_at'])
print(df['created_at'].dtype)  # datetime64[ns]
print(df['created_at'].dt.year.head())  # можно использовать .dt accessor

# Кастомный формат -- date_format
df = pd.read_csv(
    "data.csv",
    parse_dates=['date'],
    date_format='%d.%m.%Y',  # для русского формата 15.05.2026
)

# Для разных колонок с разным форматом -- парсить вручную после read_csv
df['date1'] = pd.to_datetime(df['date1'], format='%Y-%m-%d')
df['date2'] = pd.to_datetime(df['date2'], format='%d/%m/%Y')

Excel хранит даты как float (число дней с 1900-01-01). При экспорте в CSV может вывести 45657 вместо 2026-01-15. Pandas с parse_dates это не поймёт — нужно конвертировать вручную:

# Excel serial date -> datetime
df['date'] = pd.to_datetime(df['date_excel_serial'], unit='D', origin='1899-12-30')
# (origin 1899-12-30 -- потому что Excel неправильно считает 1900-02-29 как валидную дату)

Подводные камни

CSV: дьявол в деталях CSV: csv.reader, csv.DictReader, dialects, quoting

Числа с десятичным разделителем-запятой. В Европе 19,99 — это число «девятнадцать целых девяноста девять». Если CSV использует ; как разделитель и , как десятичный, pandas нужно сказать:

df = pd.read_csv("eu.csv", sep=';', decimal=',')

Leading zeros у ID. ID типа 00123 парсер читает как int 123 — потерян ведущий ноль. Решение: dtype={'id': 'string'}.

Колонка с inconsistent типами. Первые 1000 rows — int, дальше встречаются 'unknown'. Pandas угадал int, потом упал. Решение: dtype={'col': 'string'} или low_memory=False (читает весь файл, чтобы определить тип).

Большие файлы. CSV 5 ГБ не лезет в память. Используйте chunksize:

for chunk in pd.read_csv("big.csv", chunksize=100_000):
    process(chunk)  # обрабатываем по 100k rows

Попробуй сам

import csv
import pandas as pd

# 1. Создать CSV с проблемными значениями
data = [
    ["name", "age", "comment"],
    ["Alice", "30", "Hello"],
    ["Smith, John", "25", 'He said "hi"'],
    ["Multi\nLine", "40", "Has newline"],
]
with open("test.csv", "w", encoding="utf-8", newline="") as f:
    writer = csv.writer(f, quoting=csv.QUOTE_MINIMAL)
    writer.writerows(data)

# 2. Посмотреть raw содержимое
with open("test.csv", "rb") as f:
    print(f.read())
# Должны увидеть quoting вокруг проблемных полей и удвоенные кавычки

# 3. Sniffer -- угадаем диалект
with open("test.csv", encoding="utf-8") as f:
    sample = f.read(1024)
    f.seek(0)
    dialect = csv.Sniffer().sniff(sample)
    print(f"Delimiter: '{dialect.delimiter}', Quote: '{dialect.quotechar}'")

# 4. Encoding mystery
# Создаём файл в cp1251
with open("ru.csv", "w", encoding="cp1251") as f:
    f.write("name,city\nАлиса,Москва\nБоб,Питер\n")

# Читаем как utf-8 -- mojibake
try:
    df = pd.read_csv("ru.csv", encoding="utf-8")
    print("UTF-8:", df)
except UnicodeDecodeError as e:
    print("UnicodeDecodeError on utf-8:", e)

# Читаем правильно
df = pd.read_csv("ru.csv", encoding="cp1251")
print("CP1251:", df)

# 5. Excel BOM
with open("excel_like.csv", "w", encoding="utf-8-sig") as f:
    f.write("name,age\nAlice,30\n")

# Открываем как чистый utf-8 -- first column corrupted
with open("excel_like.csv", encoding="utf-8") as f:
    print(repr(next(csv.reader(f))))
    # ['name', 'age']

# С utf-8-sig -- нормально
with open("excel_like.csv", encoding="utf-8-sig") as f:
    print(repr(next(csv.reader(f))))
    # ['name', 'age']

Проверка знанийKnowledge check
Junior получил CSV-файл от партнёра: 50 МБ, имена колонок на русском, в одной колонке есть запятые внутри значений (адреса), в другой -- даты в формате 'DD.MM.YYYY', в третьей -- суммы как '1.234,56' (немецкий формат). pd.read_csv падает с UnicodeDecodeError. Опиши пошаговый план диагностики и финальный код для корректного парсинга.
ОтветAnswer
Шаг 1: определить encoding. Открыть файл как rb, посмотреть head на bytes (head -c 1000 file.csv | hexdump). Если видны байты в диапазоне 0xC0-0xFF -- это либо cp1251 (1 байт на кириллический символ), либо UTF-8 (2 байта). Используем charset-normalizer: from charset_normalizer import detect; result = detect(raw_bytes). Скорее всего cp1251 (если из 1С/Windows-системы) или utf-8-sig (если из Excel). Шаг 2: определить разделитель. Открыть как text с правильным encoding, посмотреть первую строку. Если в строке header много ';' -- это европейский формат (semicolon-separated, потому что запятая занята под десятичный разделитель). Шаг 3: проверить кавычки. Если в адресах есть запятые, должны быть quoted. Если нет -- файл некорректный, нужно вручную чистить. Финальный код: import pandas as pd; df = pd.read_csv('partner.csv', encoding='cp1251', sep=';', decimal=',', thousands='.', parse_dates=['date_column'], date_format='%d.%m.%Y', dtype={'phone': 'string', 'zip': 'string'}, na_values=['', 'N/A', '-', 'нет данных']). Объяснение каждого параметра: encoding -- для русских символов; sep=';' -- европейский разделитель; decimal=',' и thousands='.' -- суммы 1.234,56 = 1234.56; parse_dates + date_format -- даты сразу в datetime; dtype для полей где могут быть leading zeros или дефисы (телефоны, индексы); na_values -- расширенный список missing-маркеров. Дополнительно: если файл больше памяти -- добавить chunksize=10000 и обрабатывать чанками. Если есть multi-line поля (адреса с переносами строк) -- обязательно открыть с newline='' при работе через csv-module, в pandas это обрабатывается автоматически.

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5