Почему экспорт CSV из таблиц ломается при конвертации
Экспорт в CSV выглядит как простые, явно структурированные данные. Но стоит появиться настоящему адресу, ведущему нулю или встроенному переносу строки, как эта простота оказывается иллюзией.
· 3 мин чтения
Почему CSV кажется тривиальным, но не является таковым
Файл CSV выглядит настолько простым, насколько вообще может выглядеть формат данных — разбить каждую строку по запятым, первую строку считать заголовками столбцов, и всё, — и именно эта кажущаяся простота делает формат настолько лёгким для ошибки, как только в нём появляются настоящие данные. Наивный подход с разбиением по запятой прекрасно работает на игрушечном примере и ломается уже на первой же строке, где реальное значение данных содержит собственную запятую, что является рядовым явлением, а не крайним случаем: адрес вроде «ул. Главная, 123, кв. 4», название компании вроде «Иванов, Петров и Ко» или описание товара, включающее список, — всё это совершенно обычные значения, которые на законных основаниях содержат ровно тот символ, который наивный парсер использует как разделитель полей.
Как CSV на самом деле обрабатывает запятые внутри реального значения — и как наивные инструменты ошибаются
Спецификация CSV решает проблему встроенной запятой, разрешая заключать поле в двойные кавычки, внутри которых запятые и даже переносы строк являются просто буквальными данными, а не структурой — правильно взятое в кавычки поле вроде `"Иванов, Петров и Ко"` понимается как одно-единственное значение, содержащее запятую, а не как два отдельных значения, разделённых этой запятой. Парсер, наивно разбивающий текст по каждой запятой без отслеживания того, находится ли он в данный момент внутри поля в кавычках, не способен провести это различие и незаметно разбивает это единственное название компании на два ложных столбца — обычно вообще без выдачи какой-либо ошибки, и именно это делает данный класс ошибок опасным: конвертация выглядит успешной, а повреждение обнаруживается лишь позже, зачастую уже после того, как искажённые данные были для чего-то использованы.
Встроенные переносы строк внутри одной ячейки
Ячейка таблицы, содержащая многострочную заметку или многострочный адрес, — вещь совершенно обычная, и когда такая ячейка экспортируется в CSV, спецификация обрабатывает её так же, как встроенную запятую: всё поле заключается в кавычки, и перенос строки внутри него по-настоящему является частью данных поля, а не сигналом о начале новой строки. Парсер, который считает, что каждый перенос строки в файле отмечает начало новой записи, не отслеживая, находится ли он в данный момент внутри ещё не закрытого поля в кавычках, разбивает эту единственную многострочную ячейку на то, что выглядит как несколько отдельных строк — незаметно повреждая структуру строк всего, что следует за ней в файле, поскольку каждая последующая строка теперь смещена на то количество ложных лишних строк, которое внесла сломанная ячейка.
Неоднозначность типов: ведущие нули, номера телефонов и числа, которые на самом деле не числа
Каждое отдельное значение в необработанном файле CSV хранится как обычный текст, без какого-либо встроенного способа указать, является ли данное значение «на самом деле» числом, булевым значением или просто строкой, которая выглядит как число, — и это становится настоящей проблемой, как только конвертер пытается быть полезным, автоматически определяя типы. Почтовый индекс вроде «007» или идентификатор вроде «042» имеет ведущий ноль, который семантически значим — это идентификатор фиксированной ширины, и «7» и «007» являются разными идентификаторами, даже если представляют одно и то же число, — но конвертер, который охотно разбирает похожие на числа строки как настоящие числа, незаметно отбрасывает этот ведущий ноль, поскольку число 7 не имеет никакого представления о том, сколько нулей ему предшествовало. Номер телефона вроде «+1 555 0100» выглядит отчасти как число, но вовсе не является арифметическими данными, и превращение его в число даёт нечто совершенно бессмысленное. Безопасный, правильный вариант по умолчанию — оставлять неоднозначные значения текстом, если только значение однозначно и безопасно не является числовым, поскольку ошибка в эту сторону незаметно повреждает данные, вместо того чтобы выдать видимую, отслеживаемую ошибку.
Почему именно Excel добавляет второй, не связанный с этим слой проблем
Помимо собственных настоящих крайних случаев формата CSV, Microsoft Excel вносит дополнительное трение, вообще не имеющее отношения к самой спецификации CSV: многие европейские и другие региональные конфигурации Excel по умолчанию используют точку с запятой вместо запятой в качестве разделителя полей, поскольку запятая в этих локалях уже используется как десятичный разделитель, а значит файл CSV, экспортированный из одной из таких конфигураций Excel, вообще не будет корректно разобран парсером с разделением по запятой. У Excel также есть хорошо известная привычка автоматически переформатировать значения, которые он считает «узнанными», при открытии файла CSV для просмотра — например, превращать длинный числовой идентификатор в научную нотацию, — что представляет собой ошибочную интерпретацию со стороны Excel на этапе отображения, а не изъян самого файла CSV, но это регулярно принимают за настоящее повреждение данных в самом экспорте.