CSV (Comma-Separated Values, «значения через запятую») — это текстовый формат для таблиц: одна строка файла — одна запись, поля внутри записи разделены запятой. Если в поле есть запятая, двойная кавычка или перевод строки, поле берут в двойные кавычки, а кавычку внутри удваивают. Описание формата закреплено в RFC 4180, но на практике у CSV много «диалектов».
Содержание
- Три понятия, которые путают
- Как устроен CSV по RFC 4180
- Почему не split(«,»)
- Чтение и запись: минимальный рабочий пример
- DictReader и DictWriter: доступ по именам столбцов
- Зачем newline=»»
- Разделитель и диалект
- Кодировки: UTF-8, BOM и cp1251
- pandas.read_csv коротко
- Безопасность: CSV-инъекция при экспорте
- Частые ошибки: симптом -> причина -> что делать
- Выводы
- Где применяется / связь с практикой
- FAQ
Разберем, как устроен файл, как читать и записывать его модулем csv из стандартной библиотеки Python, какие кодировки выбирать и как не выгрузить пользователям опасную формулу. Примеры проверены на Python 3.14.
Три понятия, которые путают
- Разделитель (delimiter) — символ между полями: запятая, точка с запятой, табуляция (TSV).
- Кавычки (quotechar) — символ, внутри которого разделитель и перевод строки считаются частью значения.
- Кодировка — как текст превращается в байты (UTF-8, UTF-8 с BOM, cp1251). Сам CSV ее не хранит, ее нужно знать или угадывать.
Набор из разделителя, правил кавычек и конца строки в модуле csv называется диалектом.
Как устроен CSV по RFC 4180
Файл из трех записей с заголовком:
id,name,comment
1,Анна,"любит Python, SQL"
2,Борис,"сказал ""привет"""
3,Вера,"две
строки"
Основные правила RFC 4180:
- Записи разделены переводом строки CRLF (
\r\n). - Строка заголовка необязательна.
- Во всех записях одинаковое число полей.
- Поле с запятой, кавычкой или переводом строки берется в двойные кавычки; кавычка внутри удваивается (
"").
RFC 4180 (2005 год) имеет статус информационного документа, а не обязательного стандарта. Поэтому реальные файлы отступают от него:
| Что | По RFC 4180 | Что встречается на практике |
|---|---|---|
| Разделитель | запятая | ; (Excel в русской локали, где запятая — десятичный знак), табуляция |
| Конец строки | \r\n |
\n в выгрузках из Linux |
| Кодировка | не задана жестко: параметр charset, распространенный вариант — US-ASCII |
UTF-8, UTF-8 с BOM, cp1251 |
| Заголовок | необязателен | обычно есть, но может отсутствовать |
Отсюда главное правило: CSV нельзя разбирать через split(","), нужен парсер, который знает про кавычки.
Почему не split(«,»)
Неверный подход — резать строки файла по запятой:
# Наивный разбор: split по запятой
with open("people.csv", encoding="utf-8") as f:
for line in f:
print(line.rstrip("\n").split(","))
Результат на файле выше — поля разъехались, кавычки остались, запись с переносом разорвана на две:
['id', 'name', 'comment']
['1', 'Анна', '"любит Python', ' SQL"']
['2', 'Борис', '"сказал ""привет"""']
['3', 'Вера', '"две']
['строки"']
Исправление — модуль csv, он входит в стандартную библиотеку и ставить его не нужно.
Чтение и запись: минимальный рабочий пример
Полный пример: записываем таблицу и читаем ее обратно.
import csv
rows = [
["id", "name", "comment"],
[1, "Анна", "любит Python, SQL"],
[2, "Борис", 'сказал "привет"'],
[3, "Вера", "две\nстроки"],
]
with open("people.csv", "w", newline="", encoding="utf-8") as f:
csv.writer(f).writerows(rows)
with open("people.csv", newline="", encoding="utf-8") as f:
for row in csv.reader(f):
print(row)
Вывод:
['id', 'name', 'comment']
['1', 'Анна', 'любит Python, SQL']
['2', 'Борис', 'сказал "привет"']
['3', 'Вера', 'две\nстроки']
Что здесь важно:
csv.writerсам поставил кавычки только там, где нужно (режимQUOTE_MINIMALпо умолчанию), удвоил кавычки и завершил строки\r\n, как в RFC.csv.readerвернул каждую запись списком строк. Числа тоже стали строками:'1', а не1. Типы CSV не хранит, приводить их нужно самому.- Файл открыт с
newline=""и явной кодировкой — оба параметра разберем ниже.
DictReader и DictWriter: доступ по именам столбцов
Если в файле есть заголовок, удобнее csv.DictReader: каждая запись — словарь, ключи берутся из первой строки.
import csv
with open("people.csv", newline="", encoding="utf-8") as f:
reader = csv.DictReader(f)
print(reader.fieldnames)
for row in reader:
print(int(row["id"]) * 10, row["name"])
['id', 'name', 'comment']
10 Анна
20 Борис
30 Вера
Если заголовка нет, имена передают явно: csv.DictReader(f, fieldnames=["id", "name", "comment"]). Для записи словарей есть csv.DictWriter: ему обязательно передают fieldnames, а заголовок пишет метод writeheader().
Зачем newline=»»
При открытии в текстовом режиме Python переводит концы строк. На Windows при записи каждый \n превращается в \r\n, а writer и так пишет \r\n. Итог — \r\r\n и пустые строки между записями. Поведение Windows можно воспроизвести на любой ОС параметром newline="\r\n":
import csv
rows = [
["id", "name"],
[1, "Анна"],
]
with open("bad.csv", "w", newline="\r\n", encoding="utf-8") as f:
csv.writer(f).writerows(rows)
print(open("bad.csv", "rb").read())
with open("bad.csv", newline="", encoding="utf-8") as f:
for row in csv.reader(f):
print(row)
b'id,name\r\r\n1,\xd0\x90\xd0\xbd\xd0\xbd\xd0\xb0\r\r\n'
['id', 'name']
[]
['1', 'Анна']
[]
Появились пустые записи — пустые списки после каждой строки. С newline="" в файле будет ровно \r\n. При чтении тот же параметр нужен, чтобы перевод строки внутри кавычек (как у Веры) не потерялся. Правило из документации модуля: файл для csv всегда открывать с newline="".
Разделитель и диалект
Файл с точкой с запятой читают так: csv.reader(f, delimiter=";"). Если разделитель заранее неизвестен, можно попросить csv.Sniffer угадать его по фрагменту:
import csv
sample = "name;price\nКофе;3,50\nЧай;2,00\n"
dialect = csv.Sniffer().sniff(sample, delimiters=";,\t")
print(repr(dialect.delimiter))
';'
Sniffer работает эвристически: на коротком или однородном фрагменте он может ошибиться или выбросить csv.Error. Для регулярного обмена лучше договориться о формате и указывать delimiter явно.
Кодировки: UTF-8, BOM и cp1251
CSV не сообщает свою кодировку. Если не указать encoding, Python возьмет кодировку по умолчанию для системы, и на разных машинах результат будет разным. Поэтому кодировку указывают всегда.
Excel на Windows без подсказки часто открывает UTF-8 как «кракозябры». Подсказка — метка порядка байтов (BOM) в начале файла, ее пишет кодировка utf-8-sig. Но при чтении такого файла как обычного utf-8 BOM прилипает к первому столбцу:
import csv
with open("report.csv", "w", newline="", encoding="utf-8-sig") as f:
w = csv.DictWriter(f, fieldnames=["name", "price"], delimiter=";")
w.writeheader()
w.writerow({"name": "Кофе", "price": "3,50"})
# Ошибка: читаем файл с BOM как обычный utf-8
with open("report.csv", newline="", encoding="utf-8") as f:
row = next(csv.DictReader(f, delimiter=";"))
print(list(row))
try:
print(row["name"])
except KeyError as e:
print("KeyError:", e)
# Исправление: utf-8-sig снимает BOM при чтении
with open("report.csv", newline="", encoding="utf-8-sig") as f:
row = next(csv.DictReader(f, delimiter=";"))
print(row["name"], row["price"])
['\ufeffname', 'price']
KeyError: 'name'
Кофе 3,50
Ключ называется не name, а \ufeffname. Для чтения неизвестных UTF-8 файлов utf-8-sig безопасен: он снимает BOM, если он есть, и работает как utf-8, если его нет.
Старые выгрузки из 1С и Excel часто в cp1251. Попытка прочитать их как UTF-8 дает ошибку:
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe8 in position 0: invalid continuation byte
Решение — encoding="cp1251". Угадывать кодировку по одному байту ненадежно: если источник известен, берите его кодировку, если нет — уточните у того, кто выгружал файл.
pandas.read_csv коротко
Когда CSV нужен для анализа, а не для построчной обработки, обычно берут pandas. Те же параметры там называются похоже:
import pandas as pd
df = pd.read_csv("report.csv", sep=";", decimal=",", encoding="utf-8-sig")
print(df)
print(df["price"].dtype)
name price
0 Кофе 3.5
float64
В отличие от модуля csv, pandas сам приводит типы: decimal="," превратил «3,50» в число. Проверено на pandas 2.3; в pandas 3.0 текстовые столбцы по умолчанию получают строковый тип str вместо object, это видно в df.dtypes. Работа с самими DataFrame — отдельная тема.
Безопасность: CSV-инъекция при экспорте
Если в CSV попадают данные пользователей (комментарии, имена), а файл потом открывают в Excel или LibreOffice, ячейка вида =HYPERLINK(...) станет формулой. Это CSV-инъекция (formula injection): табличный процессор выполнит формулу, покажет фишинговую ссылку или, в старых настройках, запустит внешнюю команду. Модуль csv тут не помогает: для него это обычная строка.
Базовая защита — экранировать апострофом текстовые ячейки, которые начинаются с =, +, -, @, табуляции, \r или \n (список OWASP; в некоторых локалях опасны и полноширинные =, +, -, @):
import csv
DANGEROUS = ("=", "+", "-", "@", "\t", "\r", "\n", "=", "+", "-", "@")
def safe_cell(value):
"""Экранирует текстовую ячейку от интерпретации как формулы."""
if isinstance(value, str) and value.startswith(DANGEROUS):
return "'" + value
return value
rows = [
["user", "comment", "balance"],
["mallory", '=HYPERLINK("http://example.com","жми")', -150],
["bob", "+7 900 000-00-00", 20],
]
with open("export.csv", "w", newline="", encoding="utf-8-sig") as f:
w = csv.writer(f, quoting=csv.QUOTE_MINIMAL)
for row in rows:
w.writerow([safe_cell(v) for v in row])
print(open("export.csv", encoding="utf-8-sig").read())
user,comment,balance
mallory,"'=HYPERLINK(""http://example.com"",""жми"")",-150
bob,'+7 900 000-00-00,20
Границы этого приема стоит понимать. Числовой баланс -150 остался числом, потому что функция трогает только строки: если число пришло строкой, его нужно привести к типу до экспорта. Телефон +7 ... тоже получил апостроф — для выгрузки в таблицу это приемлемо, но если файл читает программа, а не человек, апостроф станет частью данных. Поэтому экранирование делают на этапе экспорта для людей, а в данных, которые хранятся и передаются между системами, его не применяют. И это не гарантия: после открытия и повторного сохранения файла в Excel апостроф может пропасть, и формула снова станет активной.
Второй риск — при чтении чужих файлов. По умолчанию модуль ограничивает размер одного поля 131072 символами (csv.field_size_limit()), и длинное поле вызывает csv.Error. Поднимать лимит до максимума «чтобы не падало» для недоверенных файлов не стоит: лучше ограничить размер файла и обработать ошибку.
Частые ошибки: симптом -> причина -> что делать
| Симптом | Причина | Что делать |
|---|---|---|
| Пустые строки между записями | файл открыт без newline="" (Windows) |
открывать с newline="" |
Ключ \ufeffname, KeyError |
BOM прочитан как часть заголовка | encoding="utf-8-sig" |
UnicodeDecodeError |
файл в cp1251, читается как UTF-8 | указать encoding="cp1251" |
| Вся строка в одном поле | разделитель ; или табуляция |
delimiter=";" или "\t" |
csv.Error: field larger than field limit |
очень длинное поле | проверить файл, осознанно поднять лимит |
Выводы
- CSV — текстовый табличный формат: записи по строкам, поля через разделитель, спецсимволы внутри двойных кавычек с удвоением кавычки (RFC 4180).
- Разбирать CSV нужно модулем
csvили pandas, а неsplit(","): кавычки и переводы строк внутри полей ломают наивный разбор. - Файл для модуля
csvоткрывают сnewline=""и явной кодировкой;utf-8-sig— для обмена с Excel,cp1251— для старых выгрузок. - Все значения из
csv.readerиDictReader— строки, типы приводят вручную. - При экспорте пользовательских данных в таблицы экранируйте ячейки-формулы, это защита от CSV-инъекции.
Где применяется / связь с практикой
Освойте тему на практике
CSV — самый частый формат обмена данными между системами: выгрузки из CRM и 1С, отчеты рекламных кабинетов, открытые датасеты. Аналитик читает их в pandas, чистит кодировки и типы и строит отчет. Этот путь от сырого CSV до выводов системно разбирают на курсе «Python для аналитики». Попробовать формат занятий можно на открытых уроках Otus.
Как открыть и сохранить CSV в Windows и Excel без поломки кодировки — в статье «CSV в Windows от А до Я».
FAQ
Чем CSV отличается от XLSX?
CSV — простой текст без форматирования, формул, типов и нескольких листов. XLSX — zip-архив с XML, который хранит все это; для обмена между программами CSV проще, для отчетов с оформлением — XLSX.
Как прочитать огромный CSV, который не помещается в память?
csv.reader и так читает построчно и не держит файл целиком. В pandas для этого есть параметр chunksize, который отдает файл частями.
Можно ли хранить в CSV вложенные данные?
Формат плоский: вложенную структуру придется сериализовать в строку (например, в JSON внутри поля). Если вложенность постоянная, удобнее JSON Lines или Parquet.



