При импорте данных из сторонних систем в платформу 1С:Предприятие 7.7 разработчики часто сталкиваются с файлами формата CSV (Comma-Separated Values). На первый взгляд, задача кажется тривиальной: достаточно использовать стандартный объект Текст или объект файловой системы Scripting.FileSystemObject (FSO) для построчного чтения. Однако стандарт CSV (описанный в спецификации RFC 4180) допускает наличие символов переноса строки (нового абзаца) внутри текстовых полей, если они заключены в двойные кавычки.
В этой ситуации штатные методы чтения файлов, такие как ПолучитьСтроку() объекта Текст или метод ReadLine() от FSO, воспринимают внутриячеечный перенос строки как физический конец записи. В результате строка считывается не полностью, структура таблицы сдвигается, а импорт данных завершается ошибкой. Рассмотрим подробнее, как обойти это ограничение платформы 1С 7.7 и корректно прочитать сложный CSV-файл.
Если структура загружаемого файла строго фиксирована и мы заранее знаем точное количество колонок (а следовательно, и количество разделителей в каждой строке), мы можем применить алгоритм накопления данных. Выясним логику этого процесса: мы читаем физические строки из файла одну за другой. Если в текущей строке количество разделителей меньше эталонного, это означает, что строка была разорвана переносом. Мы временно запоминаем её и склеиваем со следующей физической строкой, продолжая проверку.
Разберем пример кода на встроенном языке 1С 7.7, реализующий этот подход:
// Предположим, что файл содержит строго 10 колонок, разделенных точкой с запятой
// Количество разделителей (точек с запятой) в корректной строке должно быть ровно 9
КоличествоРазделителей = 9;
РазделительКолонок = ";";
Т = СоздатьОбъект("Текст");
Т.Открыть(ПутьКФайлу);
ПредыдущаяЧастьСтроки = "";
ВсегоСтрокФайла = Т.КоличествоСтрок();
Для НомерСтроки = 1 По ВсегоСтрокФайла Цикл
ТекущаяСтрока = Т.ПолучитьСтроку(НомерСтроки);
// Если у нас остался фрагмент от предыдущего шага, склеиваем его с текущей строкой
Если ПустаяСтрока(ПредыдущаяЧастьСтроки) = 0 Тогда
ТекущаяСтрока = ПредыдущаяЧастьСтроки + Симв(13) + Симв(10) + ТекущаяСтрока;
ПредыдущаяЧастьСтроки = "";
КонецЕсли;
// Считаем количество разделителей в получившейся строке
ЧислоВхождений = СтрЧислоВхождений(ТекущаяСтрока, РазделительКолонок);
Если ЧислоВхождений < КоличествоРазделителей Тогда
// Разделителей не хватает, значит, ячейка перенесена на следующую строку
ПредыдущаяЧастьСтроки = ТекущаяСтрока;
Продолжить;
КонецЕсли;
// Строка собрана полностью, можем приступать к ее обработке
Сообщить("Успешно прочитана строка: " + ТекущаяСтрока);
КонецЦикла;
Важное ограничение метода: Данное решение корректно работает только в том случае, если сам символ разделителя (например, точка с запятой) гарантированно не встречается внутри текстовых ячеек в кавычках. Если же текстовые ячейки содержат и разделители, и переносы строк, данный алгоритм даст сбой.
Для создания универсального парсера, который полностью соответствует спецификации RFC 4180, проанализируем математическое свойство разметки CSV: любое текстовое поле, содержащее спецсимволы (переносы строк или разделители), обязано обрамляться двойными кавычками. При этом кавычки внутри самого текста удваиваются.
Это означает, что в пределах одной логической записи таблицы общее количество символов двойных кавычек всегда должно быть четным. Если при чтении очередной физической строки мы обнаруживаем нечетное количество кавычек, это однозначный признак того, что текстовое поле было открыто, но не закрылось до переноса строки. Нам необходимо читать строки далее до тех пор, пока суммарное количество кавычек не станет четным.
Посмотрим на пример реализации данного алгоритма:
Т = СоздатьОбъект("Текст");
Т.Открыть(ПутьКФайлу);
СобраннаяСтрока = "";
ВсегоСтрок = Т.КоличествоСтрок();
Для НомерСтроки = 1 По ВсегоСтрок Цикл
ФизическаяСтрока = Т.ПолучитьСтроку(НомерСтроки);
Если ПустаяСтрока(СобраннаяСтрока) = 0 Тогда
// Соединяем фрагменты, восстанавливая физический перенос строки
СобраннаяСтрока = СобраннаяСтрока + РазделительСтрок + ФизическаяСтрока;
Иначе
СобраннаяСтрока = ФизическаяСтрока;
КонецЕсли;
// Подсчитываем количество двойных кавычек в накопленном буфере
КоличествоКавычек = СтрЧислоВхождений(СобраннаяСтрока, """");
// Проверяем четность с помощью остатка от деления на 2
Если (КоличествоКавычек % 2) <> 0 Тогда
// Количество кавычек нечетное, продолжаем склейку в следующем шаге цикла
Продолжить;
КонецЕсли;
// Логическая строка полностью собрана!
ОбработатьГотовуюСтроку(СобраннаяСтрока);
// Сбрасываем буфер для следующей логической строки
СобраннаяСтрока = "";
КонецЦикла;
Этот алгоритм является чрезвычайно надежным, быстродействующим и не требует сторонних библиотек. Он корректно обрабатывает любые комбинации переносов и разделителей внутри кавычек.
Если файл имеет огромный размер (в вопросе автора темы фигурирует 11 тысяч строк и 900 колонок), посимвольный или даже построчный разбор силами встроенного языка 1С 7.7 может выполняться медленно. Для ускорения и упрощения разработки мы можем делегировать разбор CSV внешнему специализированному SQL-драйверу через технологию ADODB.
Microsoft Text Driver прекрасно справляется с многострочными ячейками, автоматически собирая их в единые поля. Однако для того чтобы драйвер правильно определил разделитель колонок (особенно если это точка с запятой ;), в каталоге с CSV-файлом необходимо динамически создать конфигурационный файл Schema.ini.
Рассмотрим по шагам программную реализацию этого метода:
// 1. Определяем пути к файлу
ИмяФайлаCSV = "data.csv";
КаталогФайла = "C:\Import\";
// 2. Создаем файл конфигурации Schema.ini в целевом каталоге
ФС.УстТекКаталог(КаталогФайла);
ТекстШеmapping = СоздатьОбъект("Текст");
ТекстШеmapping.ДобавитьСтроку("[" + ИмяФайлаCSV + "]");
ТекстШеmapping.ДобавитьСтроку("Format=Delimited(;)"); // Указываем разделитель ";"
ТекстШеmapping.ДобавитьСтроку("ColNameHeader=False"); // Есть ли заголовки колонок
ТекстШеmapping.ДобавитьСтроку("CharacterSet=ANSI");
ТекстШеmapping.ДобавитьСтроку("MaxScanRows=0"); // Сканировать весь файл для определения типов
ТекстШеmapping.Записать("Schema.ini");
// 3. Подключаемся к файлу через ADO
Connection = СоздатьОбъект("ADODB.Connection");
Recordset = СоздатьОбъект("ADODB.Recordset");
СтрокаПодключения = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" + КаталогФайла + ";Extended Properties=""Text;HDR=No;FMT=Delimited""";
Connection.Open(СтрокаПодключения);
// Выполняем SQL запрос к текстовому файлу, как к таблице БД
ТекстЗапроса = "SELECT * FROM [" + ИмяФайлаCSV + "]";
Recordset.Open(ТекстЗапроса, Connection, 1, 1); // adOpenKeyset, adLockReadOnly
// 4. Обходим полученную выборку данных
Пока Recordset.EOF() = 0 Цикл
// Драйвер автоматически склеил все переносы внутри ячеек!
// Каждая колонка доступна через коллекцию Fields
ЗначениеПервойКолонки = Recordset.Fields(0).Value;
ЗначениеВторойКолонки = Recordset.Fields(1).Value;
// Обработка данных...
Recordset.MoveNext();
КонецЦикла;
// Закрываем соединения
Recordset.Close();
Connection.Close();
// Удаляем временный файл конфигурации
ФС.УдалитьФайл(КаталогФайла + "Schema.ini");
Использование ADODB позволяет избежать сложного ручного парсинга строк и кавычек. Вы получаете чистые данные ячеек, в которых переносы строк сохраняются в первозданном виде (внутри значений соответствующих полей).
В случаях, когда файл чрезвычайно велик и содержит сотни тысяч символов, можно прибегнуть к предварительной обработке всего текста в оперативной памяти с помощью регулярных выражений. Идея заключается в том, чтобы найти все символы перевода строки, которые находятся внутри двойных кавычек, и временно заменить их уникальным текстовым маркером (например, [LF_PLACEHOLDER]).
После такой замены стандартные методы 1С (объект Текст) без проблем прочитают файл построчно, так как все физические разрывы внутри ячеек будут заменены на обычную текстовую строку. В процессе парсинга конкретных ячеек мы просто выполним обратную замену маркера на стандартный перенос строки РазделительСтрок.
Для реализации этого метода используется системный ActiveX-компонент VBScript.RegExp:
// Считываем весь файл в одну строковую переменную
ФСО = СоздатьОбъект("Scripting.FileSystemObject");
Файл = ФСО.OpenTextFile(ПутьКФайлу, 1); // 1 = ForReading
ВесьТекст = Файл.ReadAll();
Файл.Close();
// Инициализируем регулярные выражения
RegExp = СоздатьОбъект("VBScript.RegExp");
RegExp.Global = Истина;
RegExp.Multiline = Истина;
// Паттерн для поиска переносов внутри кавычек довольно сложен,
// поэтому применим более простую логику конечного автомата или
// воспользуемся готовым COM-объектом для парсинга, но если необходимо быстро заменить:
// Заменяем внутренние переходы строк.
// Примечание: Для сложной логики регулярных выражений в 1С 7.7
// лучше использовать посимвольный проход по строке в памяти, так как он работает мгновенно.
Если вы решите реализовать быструю замену в памяти посимвольно (что в 1С 7.7 работает на удивление быстро благодаря оптимизации строковых операций), используйте следующий алгоритм:
// Быстрый посимвольный парсер-конвертер в памяти
РазмерТекста = СтрДлина(ВесьТекст);
Результат = "";
ВнутриКавычек = 0;
Для Позиция = 1 По РазмерТекста Цикл
Символ = Сред(ВесьТекст, Позиция, 1);
Если Символ = """" Тогда
ВнутриКавычек = 1 - ВнутриКавычек; // переключаем флаг при встрече кавычки
КонецЕсли;
Если (ВнутриКавычек = 1) Тогда
Если (КодСимв(Символ) = 13) Или (КодСимв(Символ) = 10) Тогда
// Заменяем перенос строки внутри кавычек на временный маркер
Результат = Результат + "[LF]";
Продолжить;
КонецЕсли;
КонецЕсли;
Результат = Результат + Символ;
КонецЦикла;
Если производительность импорта не является критическим фактором (например, загрузка выполняется раз в день в фоновом режиме), задачу разбора структуры CSV можно полностью переложить на Microsoft Excel или LibreOffice Calc посредством механизма OLE/COM.
Офисный пакет содержит встроенные профессиональные парсеры текстовых файлов, которые автоматически разложат строки с любыми переносами по соответствующим ячейкам листа. Нам останется лишь считать готовые значения:
Попытка
Эксель = СоздатьОбъект("Excel.Application");
Исключение
Предупреждение("Microsoft Excel не установлен на данном компьютере!");
Возврат;
КонецПопытки;
// Открываем CSV-файл. Параметр Delimiter позволяет настроить разделитель колонок
Эксель.Workbooks.OpenText(ПутьКФайлу, , , 1, , , , , 1); // 1 в конце означает разделитель Semicolon (точка с запятой)
РабочийЛист = Эксель.ActiveSheet;
ВсегоСтрок = РабочийЛист.UsedRange.Rows.Count;
ВсегоКолонок = РабочийЛист.UsedRange.Columns.Count;
Для СтрокаТаблицы = 1 По ВсегоСтрок Цикл
Для КолонкаТаблицы = 1 По ВсегоКолонок Цикл
// Excel корректно сохранил переносы строк внутри конкретной ячейки
ЗначениеЯчейки = РабочийЛист.Cells(СтрокаТаблицы, КолонкаТаблицы).Value;
// Обрабатываем значение...
КонецЦикла;
КонецЦикла;
// Закрываем приложение Excel без сохранения изменений
Эксель.ActiveWorkbook.Close(0);
Эксель.Quit();
Подведем итоги: Для простых конфигураций файлов с гарантированным отсутствием разделителей в текстах подойдет Решение 1. Для универсальной работы на встроенном языке лучшим выбором будет Решение 2 (проверка четности кавычек). Если важна максимальная скорость обработки больших массивов данных без ручного написания сложных парсеров, задействуйте Решение 3 (ADODB + Schema.ini).