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 из 0
Прикладной
Вопрос 1 из 5. Junior читает CSV через open() + line.split(',') и обрабатывает поля. Файл вдруг начинает выдавать неправильное количество колонок на отдельных строках. Что вероятная причина?

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

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

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

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