При загрузке данных из внешних источников, таких как файлы Excel, часто возникает задача не просто импортировать строки, но и выполнить их «умную» свертку. Проблема обостряется, когда объем данных велик (например, более 30 000 строк), а стандартный метод Свернуть() объекта ТаблицаЗначений не подходит из-за необходимости объединять текстовые поля (адреса, телефоны) через разделитель, исключая при этом повторы внутри самих строк. Разберем, как превратить медленную обработку, длящуюся сутками, в быстрый и надежный алгоритм.
Рассмотрим типичную ошибку начинающего разработчика. Для удаления дублей часто используется алгоритм с вложенным обходом таблицы. В первом цикле берется строка, а во втором — происходит поиск всех похожих строк в этой же таблице. С точки зрения математики, сложность такого алгоритма составляет O(n²). Если в файле 30 000 строк, программе придется выполнить около 900 миллионов операций сравнения. Именно поэтому обработка может занимать несколько суток.
Проанализируем ситуацию: нам нужно не просто сложить количество обращений, но и собрать все уникальные телефоны и адреса для каждого контрагента в одну строку. Стандартная функция ТЗ.Свернуть("Контрагент", "Количество") объединит только числа, а текстовые данные будут потеряны или разделены на разные строки, если они хоть немного отличаются.
Вместо того чтобы сравнивать каждую строку с каждой, мы будем использовать метод «одного прохода». Создадим результирующую таблицу значений и при обработке каждой строки из Excel будем проверять: есть ли уже такой контрагент в нашей новой таблице? Если есть — дополняем данные, если нет — создаем новую запись.
Рассмотрим пример кода, реализующий этот подход:
// Предположим, ТЗ_Исходная уже заполнена данными из Excel
ТЗ_Результат = ТЗ_Исходная.СкопироватьКолонки();
// Обязательно добавляем индекс для ускорения поиска!
ТЗ_Результат.Индексы.Добавить("Контрагент");
Для Каждого СтрИзФайла Из ТЗ_Исходная Цикл
// Ищем контрагента в результирующей таблице
ПараметрыПоиска = Новый Структура("Контрагент", СтрИзФайла.Контрагент);
НайденныеСтроки = ТЗ_Результат.НайтиСтроки(ПараметрыПоиска);
Если НайденныеСтроки.Количество() > 0 Тогда
ТекСтр = НайденныеСтроки[0];
// Суммируем количество
ТекСтр.КоличествоОбращений = ТекСтр.КоличествоОбращений + СтрИзФайла.КоличествоОбращений;
// Добавляем телефон, если его еще нет в строке
Если ЗначениеЗаполнено(СтрИзФайла.Телефон) И Найти(ТекСтр.Телефон, СтрИзФайла.Телефон) = 0 Тогда
ТекСтр.Телефон = ТекСтр.Телефон + ", " + СтрИзФайла.Телефон;
КонецЕсли;
// Добавляем адрес по тому же принципу
Если ЗначениеЗаполнено(СтрИзФайла.Адрес) И Найти(ТекСтр.Адрес, СтрИзФайла.Адрес) = 0 Тогда
ТекСтр.Адрес = ТекСтр.Адрес + ", " + СтрИзФайла.Адрес;
КонецЕсли;
Иначе
// Если не нашли — добавляем новую строку
НоваяСтр = ТЗ_Результат.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтр, СтрИзФайла);
КонецЕсли;
КонецЦикла;
Важный момент: использование метода ТЗ_Результат.Индексы.Добавить("Контрагент") критически важно. Без индекса поиск методом НайтиСтроки будет замедляться пропорционально росту результирующей таблицы.
Для максимально быстрой работы, особенно на платформе 8.2, рекомендуется использовать объект Соответствие. Поиск в нем работает быстрее, чем в индексированной таблице значений, так как используются механизмы хэширования.
Выясним причину эффективности этого метода: в качестве ключа соответствия мы будем использовать уникальный идентификатор контрагента (в данном случае ФИО), а в качестве значения — саму строку ТаблицыЗначений.
Разберем по шагам реализацию:
ТаблицуЗначений для результата.Соответствие.
ТаблицаРезультат = Новый ТаблицаЗначений;
ТаблицаРезультат.Колонки.Добавить("Контрагент");
ТаблицаРезультат.Колонки.Добавить("Телефон");
ТаблицаРезультат.Колонки.Добавить("Адрес");
ТаблицаРезультат.Колонки.Добавить("КоличествоОбращений");
КартаКонтрагентов = Новый Соответствие;
Для Каждого СтрокаДанных Из ТЗ_Из_Excel Цикл
Ключ = СокрЛП(СтрокаДанных.Контрагент);
СтрокаРезультата = КартаКонтрагентов.Получить(Ключ);
Если СтрокаРезультата = Неопределено Тогда
// Новый контрагент
СтрокаРезультата = ТаблицаРезультат.Добавить();
СтрокаРезультата.Контрагент = Ключ;
СтрокаРезультата.Телефон = СокрЛП(СтрокаДанных.Телефон);
СтрокаРезультата.Адрес = СокрЛП(СтрокаДанных.Адрес);
СтрокаРезультата.КоличествоОбращений = СтрокаДанных.КоличествоОбращений;
КартаКонтрагентов.Вставить(Ключ, СтрокаРезультата);
Иначе
// Контрагент уже был, дополняем данные
СтрокаРезультата.КоличествоОбращений = СтрокаРезультата.КоличествоОбращений + СтрокаДанных.КоличествоОбращений;
// Проверка на дублирование телефонов внутри строки
Если ЗначениеЗаполнено(СтрокаДанных.Телефон)
И Найти(СтрокаРезультата.Телефон, СокрЛП(СтрокаДанных.Телефон)) = 0 Тогда
СтрокаРезультата.Телефон = СтрокаРезультата.Телефон + "; " + СокрЛП(СтрокаДанных.Телефон);
КонецЕсли;
// Проверка на дублирование адресов
Если ЗначениеЗаполнено(СтрокаДанных.Адрес)
И Найти(СтрокаРезультата.Адрес, СокрЛП(СтрокаДанных.Адрес)) = 0 Тогда
СтрокаРезультата.Адрес = СтрокаРезультата.Адрес + " | " + СокрЛП(СтрокаДанных.Адрес);
КонецЕсли;
КонецЕсли;
КонецЦикла;
Зачастую данные в Excel «грязные»: телефоны могут быть записаны через «+7», «8» или со скобками. Чтобы свертка прошла успешно, проанализируем ситуацию и добавим предварительную очистку. Для этого удобно использовать функции работы со строками или Регулярные выражения через COM-объект VBScript.RegExp.
Рассмотрим простую функцию для нормализации телефона:
Функция ОчиститьТелефон(Знач ИсходныйТелефон)
Результат = "";
Для Поз = 1 По СтрДлина(ИсходныйТелефон) Цикл
Символ = Сред(ИсходныйТелефон, Поз, 1);
Если Найти("0123456789", Символ) > 0 Тогда
Результат = Результат + Символ;
КонецЕсли;
КонецЦикла;
Возврат Результат;
КонецФункции
Использование такой функции перед сравнением позволит избежать дублей вида «8900...» и «+7900...», так как они превратятся в одинаковую последовательность цифр.
После того как данные в таблице значений свернуты, их необходимо записать в справочник. Чтобы этот процесс не стал «бутылочным горлышком», используйте транзакции. Запись 15 000 уникальных элементов по отдельности может занять много времени из-за постоянного обращения к диску.
Применим пакетную запись:
НачатьТранзакцию();
Счетчик = 0;
Для Каждого Стр Из ТаблицаРезультат Цикл
НовыйЭлемент = Справочники.Контрагенты.СоздатьЭлемент();
НовыйЭлемент.Наименование = Стр.Контрагент;
// Заполнение остальных реквизитов...
Попытка
НовыйЭлемент.Записать();
Счетчик = Счетчик + 1;
Исключение
Сообщить("Ошибка при записи: " + ОписаниеОшибки());
КонецПопытки;
// Фиксируем транзакцию каждые 1000 элементов, чтобы не перегружать лог
Если Счетчик % 1000 = 0 Тогда
ЗафиксироватьТранзакцию();
НачатьТранзакцию();
КонецЕсли;
КонецЦикла;
ЗафиксироватьТранзакцию();
Резюме: Для быстрой обработки больших файлов Excel в 1С 8.2 следует отказаться от вложенных циклов в пользу объекта Соответствие или индексированных таблиц. Это позволяет сократить время выполнения задачи с нескольких суток до считанных минут.