При выгрузке отчетов и таблиц из Microsoft Excel в формат CSV часто возникает ситуация, когда в тексте ячеек присутствуют переносы строк (созданные пользователями через сочетание клавиш Alt+Enter). При попытке загрузить такой файл типовыми средствами платформы 1С строки таблицы разрываются, и структура колонок нарушается.
Разберем природу этой проблемы, выясним, почему привычный объект ТекстовыйДокумент разделяет строки не так, как ожидается, и рассмотрим эффективные варианты решения задачи непосредственно в коде 1С.
Проанализируем структуру файла, создаваемого Excel при экспорте в CSV. Реальное окончание строки таблицы разделяется последовательностью символов возврата каретки и перевода строки — CRLF (в терминах встроенного языка: Символы.ВК + Символы.ПС, байты 13 и 10). Однако внутри ячеек принудительный перенос сохраняется только как одиночный перевод строки — LF (Символы.ПС, байт 10).
Разработчики часто пытаются указать явный разделитель при чтении файла через метод Прочитать() объекта ТекстовыйДокумент:
ТекстДок = Новый ТекстовыйДокумент;
ТекстДок.Прочитать(ИмяФайла, "UTF-8", Символы.ВК + Символы.ПС);
Однако такой подход не решает проблему. Выясним причину такого поведения платформы: внутри объекта ТекстовыйДокумент единственным системным разделителем строк всегда является Символы.ПС. Параметр разделителя в методе Прочитать() лишь указывает, какую именно последовательность байтов из входящего файла требуется преобразовать в системный перевод строки. Любой одиночный Символы.ПС платформа безусловно считает окончанием строки документа и разбивает текст на отдельные элементы коллекции. Кроме того, загрузка больших CSV-файлов целиком через ТекстовыйДокумент нерационально расходует оперативную память сервера.
Рассмотрим изящный способ обхода системной конвертации, предложенный участниками обсуждения. Если внутри многострочных ячеек гарантированно присутствует только одиночный символ Символы.ПС, а строки файла разделяются парой Символы.ВК + Символы.ПС, мы можем указать в качестве разделителя строк только возврат каретки Символы.ВК.
Разберем реализацию этого алгоритма с помощью потокового объекта ЧтениеТекста:
ФайлЧтения = Новый ЧтениеТекста(ИмяФайла, КодировкаТекста.UTF8, Символы.ВК, "");
Пока Истина Цикл
СтрокаДанных = ФайлЧтения.ПрочитатьСтроку();
Если СтрокаДанных = Неопределено Тогда
Прервать;
КонецЕсли;
// В начале строки может оставаться символ ПС от предыдущего разделителя
СтрокаДанных = СокрЛП(СтрокаДанных);
Если ПустаяСтрока(СтрокаДанных) Тогда
Продолжить;
КонецЕсли;
// Передаем строку на синтаксический разбор по колонкам
ОбработатьСтрокуCSV(СтрокаДанных);
КонецЦикла;
ФайлЧтения.Закрыть();
Обратим внимание на четвертый параметр конструктора ЧтениеТекста. По умолчанию параметр КонвертируемыйРазделительСтрок содержит последовательность Символы.ВК + Символы.ПС. Передавая туда пустую строку "", мы отключаем стандартное преобразование и получаем возможность разделить файл строго по границам записей.
Посмотрим на общепринятый стандарт разбора CSV-файлов. Согласно спецификации RFC 4180, ячейка, содержащая символы перевода строки или разделители колонок, обязательно обрамляется двойными кавычками, а внутренние кавычки дублируются. Это дает возможность отслеживать логическую целостность строки: если общее количество кавычек в считанной строке нечетное, значит, поле не закрыто и внутри ячейки встретился перенос строки.
Разберем по шагам реализацию потокового чтения с объединением многострочных строк:
ЧтениеТекста.ПрочитатьСтроку().Символы.ПС до тех пор, пока количество кавычек не станет четным.Посмотрим на практический пример кода:
Функция КоличествоСимволовВСтроке(ИсходнаяСтрока, ИскомыйСимвол)
Возврат СтрДлина(ИсходнаяСтрока) - СтрДлина(СтрЗаменить(ИсходнаяСтрока, ИскомыйСимвол, ""));
КонецФункции
Процедура ПрочитатьФайлCSV(ИмяФайла)
ФайлЧтения = Новый ЧтениеТекста;
ФайлЧтения.Открыть(ИмяФайла, КодировкаТекста.UTF8);
ТекущаяСтрокаCSV = "";
Кавычка = """";
Пока Истина Цикл
СтрокаЧасть = ФайлЧтения.ПрочитатьСтроку();
Если СтрокаЧасть = Неопределено Тогда
Прервать;
КонецЕсли;
Если ПустаяСтрока(ТекущаяСтрокаCSV) Тогда
ТекущаяСтрокаCSV = СтрокаЧасть;
Иначе
ТекущаяСтрокаCSV = ТекущаяСтрокаCSV + Символы.ПС + СтрокаЧасть;
КонецЕсли;
// Проверяем четность кавычек
ЧислоКавычек = КоличествоСимволовВСтроке(ТекущаяСтрокаCSV, Кавычка);
Если ЧислоКавычек % 2 = 0 Тогда
// Строка завершена, выполняем разбор полей
РазобратьСтрокуПоКолонкам(ТекущаяСтрокаCSV);
ТекущаяСтрокаCSV = "";
КонецЕсли;
КонецЦикла;
ФайлЧтения.Закрыть();
КонецПроцедуры
Данный вариант является наиболее универсальным, так как корректно обрабатывает любые комбинации переносов строк внутри экранированных кавычками полей, не зависит от настроек операционной системы и потребляет минимальное количество оперативной памяти даже на файлах размером в сотни мегабайт.
Если разбор выполняется на сервере под управлением ОС Windows, мы можем проанализировать альтернативный вариант: чтение CSV-файла как таблицы реляционной базы данных с помощью драйверов Microsoft Text Driver (*.txt; *.csv) или Microsoft.ACE.OLEDB.
Драйвер OLE DB самостоятельно интерпретирует спецификацию CSV, корректно считывает многострочные ячейки внутри кавычек и возвращает готовый набор записей ADODB.Recordset. Это исключает необходимость вручную собирать строки и обрабатывать экранирование спецсимволов.