При автоматизации учета затрат часто возникает необходимость импортировать данные из отчетов сторонних контрагентов. Типичный пример — ежемесячные отчеты операторов мобильной связи. Такие файлы часто поставляются в текстовом формате (TXT), но имеют сложную структуру: данные разбиты на информационные блоки, внутри которых используются различные разделители, текстовые «цепочки» из точек и пробелов, а также специфические форматы чисел.
В этой статье мы подробно разберем, как правильно реализовать импорт такого файла в 1С. Мы проанализируем типичные ошибки начинающих разработчиков, исправим популярное, но некорректное решение с форума, и рассмотрим два профессиональных метода разбора: с помощью конечного автомата (построчного чтения) и с использованием регулярных выражений (RegEx).
Посмотрим на структуру файла, которую предоставил автор темы. Файл состоит из повторяющихся блоков. Каждый блок начинается со строки с ключевыми данными контракта и телефона:
Контракт № 3954343434 Номер телефону: 380502423420
Далее следуют строки с тарифом и детализацией услуг, где название услуги отделено от ее стоимости длинной цепочкой из точек (например, . . . . . . . . . . . .). Нам необходимо импортировать общие данные (номер контракта, телефон, тариф) в шапку документа или в качестве строк основной таблицы, а детальную информацию по услугам и суммам — в табличную часть.
Попытка автора читать файл построчно через ТекстовыйДокумент внутри цикла с жестким смещением индексов строк (когда внутри цикла несколько раз вызывается ПолучитьСтроку()) — это классическая ошибка. Если структура файла немного изменится (например, у одного из контрактов будет на одну услугу больше или меньше), весь алгоритм «съедет», и данные запишутся некорректно.
В сообщении 6 на форуме было предложено использовать следующий код для разделения блоков:
мДанных = СтрРазделить(Блок, "Номер телефону:", Ложь);
Внимание: это грубейшая ошибка! В платформе 1С встроенная функция СтрРазделить работает не так, как аналогичные функции во многих других языках программирования. Она воспринимает второй параметр не как единую строку-разделитель, а как набор отдельных символов.
Если вы передадите в качестве разделителя строку "Номер телефону:", система разобьет ваш текст по каждому вхождению букв «Н», «о», «м», «е», «р», «т», «л», «ф», «у», знака двоеточия «:» и даже по пробелам! В результате вместо двух ожидаемых частей вы получите массив из десятков разорванных буквосочетаний.
Выясним, как обойти это ограничение платформы. У нас есть два надежных способа:
мДанных = СтроковыеФункцииКлиентСервер.РазложитьСтрокуВМассивПодстрок(Блок, "Номер телефону:", Истина);
"|" или тильду "~"), а затем применить стандартную функцию:
ВременнаяСтрока = СтрЗаменить(Блок, "Номер телефону:", "|");
мДанных = СтрРазделить(ВременнаяСтрока, "|", Ложь);
Загрузка всего текстового файла в память через объект ТекстовыйДокумент — ресурсоемкая операция, особенно если файл детализации весит десятки мегабайт. Гораздо эффективнее использовать объект ЧтениеТекста, который считывает файл последовательно, строка за строкой. Мы применим логику конечного автомата, отслеживая состояние текущего блока данных.
Рассмотрим по шагам реализацию этого алгоритма:
&НаСервере
Процедура ИмпортироватьФайлНаСервере(ПутьКФайлу)
// Инициализируем чтение файла в кодировке UTF-8
Чтение = Новый ЧтениеТекста(ПутьКФайлу, КодировкаТекста.UTF8);
ТекущаяСтрока = Чтение.ПрочитатьСтроку();
// Переменная для хранения ссылки на текущую обрабатываемую строку таблицы
ТекущаяСтрокаНомеров = Неопределено;
Пока ТекущаяСтрока <> Неопределено Цикл
// Очищаем строку от лишних пробелов по краям
ТекущаяСтрока = СокрЛП(ТекущаяСтрока);
Если ТекущаяСтрока = "" Тогда
ТекущаяСтрока = Чтение.ПрочитатьСтроку();
Продолжить;
КонецЕсли;
// 1. Обнаружили начало нового блока (Контракт)
Если СтрНаходит(ТекущаяСтрока, "Контракт №") > 0 Тогда
// Добавляем новую строку в табличную часть документа "Номера"
ТекущаяСтрокаНомеров = ЭтотОбъект.Номера.Добавить();
// Выделяем контракт и телефон методом позиционирования
ПозКонтракта = СтрНайти(ТекущаяСтрока, "Контракт №") + 10;
ПозТелефона = СтрНайти(ТекущаяСтрока, "Номер телефону:");
Если ПозТелефона > 0 Тогда
ТекущаяСтрокаНомеров.Контракт = СокрЛП(Сред(ТекущаяСтрока, ПозКонтракта, ПозТелефона - ПозКонтракта));
ТекущаяСтрокаНомеров.НомерТелефона = СокрЛП(Сред(ТекущаяСтрока, ПозТелефона + 15));
Иначе
ТекущаяСтрокаНомеров.Контракт = СокрЛП(Сред(ТекущаяСтрока, ПозКонтракта));
КонецЕсли;
// 2. Обнаружили строку тарифа
ИначеЕсли СтрНаходит(ТекущаяСтрока, "Тарифний Пакет:") > 0 И ТекущаяСтрокаНомеров <> Неопределено Тогда
ТекущаяСтрокаНомеров.Тариф = СокрЛП(СтрЗаменить(ТекущаяСтрока, "Тарифний Пакет:", ""));
// 3. Обрабатываем строки детализации услуг (содержат точки-разделители)
ИначеЕсли СтрНаходит(ТекущаяСтрока, ". . . .") > 0 И ТекущаяСтрокаНомеров <> Неопределено Тогда
// Разбираем строку услуги
РазобратьСтрокуУслуги(ТекущаяСтрока, ТекущаяСтрокаНомеров);
КонецЕсли;
// Переходим к следующей строке файла
ТекущаяСтрока = Чтение.ПрочитатьСтроку();
КонецЦикла;
Чтение.Закрыть();
КонецПроцедуры
Для разбора текстов со сложной или непостоянной структурой регулярные выражения являются индустриальным стандартом разработки. Они позволяют лаконично и быстро извлекать нужные фрагменты информации по шаблонам.
Проанализируем шаблоны (Patterns), которые подходят под нашу задачу:
Контракт\s*№\s*(\d+)Номер\s*телефону:\s*(\d+)^([^.:\n]+)(?:\s*\.\s*){3,}\s*(-?\d+\.\d+). Этот шаблон берет весь текст до точек как название услуги, пропускает цепочку точек и захватывает числовое значение в конце.Если вы работаете на современной платформе 1С (начиная с версии 8.3.23), вы можете использовать встроенный объект для работы с регулярными выражениями. Посмотрим на пример реализации с нативными механизмами:
&НаСервере
Процедура ПарсингЧерезВстроенныйRegEx(ТекстСтроки)
// Пример поиска номера телефона встроенными средствами 1С (платформа 8.3.23+)
ШаблонТелефона = "Номер\s*телефону:\s*(\d+)";
РезультатПоиска = СтрНайтиПоРегулярномуВыражению(ТекстСтроки, ШаблонТелефона);
Если РезультатПоиска.Количество() > 0 Тогда
// Извлекаем захваченную группу (сам номер телефона)
НомерТелефона = РезультатПоиска[0].Значение;
КонецЕсли;
КонецПроцедуры
Если ваша конфигурация работает на более старых версиях платформы, мы можем задействовать COM-объект операционной системы Windows — VBScript.RegExp. Разберем пример функции, извлекающей сумму и название услуги с помощью COM:
Функция РазобратьСтрокуЧерезCOM(Знач ВходнаяСтрока) Экспорт
Результат = Новый Структура("НазваниеУслуги, Сумма", "", 0);
RegExp = Новый COMОбъект("VBScript.RegExp");
RegExp.IgnoreCase = Истина;
RegExp.Global = Ложь;
// Шаблон: ищет текст, затем группу точек, затем число в конце
RegExp.Pattern = "^([^.]+)\s*(?:\.\s*){3,}\s*(-?\d+(?:\.\d+)?)";
Если RegExp.Test(ВходнаяСтрока) Тогда
Matches = RegExp.Execute(ВходнаяСтрока);
Если Matches.Count > 0 Тогда
Match = Matches.Item(0);
Результат.НазваниеУслуги = СокрЛП(Match.SubMatches.Item(0));
СтрокаСуммы = Match.SubMatches.Item(1);
Результат.Сумма = ПреобразоватьСтрокуВЧисло(СтрокаСуммы);
КонецЕсли;
КонецЕсли;
Возврат Результат;
КонецФункции
Еще один важный подводный камень, о котором часто забывают разработчики при импорте файлов — это формат дробных чисел. В файлах выгрузки операторов связи копейки обычно отделяются точкой (например, 78.4314 или -7.8431).
Если на сервере 1С установлена русская или украинская локаль ОС, стандартная функция 1С Число("78.4314") вызовет ошибку преобразования типа, так как система ожидает в качестве разделителя запятую, а точку считает недопустимым символом.
Кроме того, в строке могут присутствовать скрытые неразрывные пробелы (символ с кодом 160), которые используются для визуального форматирования чисел. Напишем надежную функцию очистки и конвертации строки в число:
Функция ПреобразоватьСтрокуВЧисло(Знач ИсходнаяСтрока)
// Убираем неразрывные пробелы (код 160) и обычные пробелы
ОчищеннаяСтрока = СтрЗаменить(ИсходнаяСтрока, Символы.НПП, "");
ОчищеннаяСтрока = СтрЗаменить(ОчищеннаяСтрока, " ", "");
// Приводим разделитель к системному (заменяем точку на запятую)
ОчищеннаяСтрока = СтрЗаменить(ОчищеннаяСтрока, ".", ",");
Попытка
Возврат Число(ОчищеннаяСтрока);
Исключение
// В случае ошибки возвращаем 0, чтобы не прерывать импорт всего файла
ЗаписьЖурналаРегистрации("ИмпортДанных.ОшибкаПреобразования",
УровеньЖурналаРегистрации.Предупреждение, , ,
"Не удалось преобразовать значение в число: " + ИсходнаяСтрока);
Возврат 0;
КонецПопытки;
КонецФункции
Применяя данные подходы, мы гарантируем высокую скорость чтения файлов любого объема, устойчивость алгоритма к незначительным изменениям структуры текста и корректное распознавание числовых значений независимо от региональных настроек сервера приложений.