При работе с большими объемами текстовых данных (например, логами, XML-выгрузками, базами данных в формате JSON или HTML-страницами) разработчики в 1С часто сталкиваются с задачей поиска определенных вхождений. Когда текстовый файл содержит более 600 тысяч строк, стандартные и интуитивно понятные методы поиска могут привести к критическому падению производительности и зависанию сеанса работы пользователя.
В этой статье мы подробно проанализируем проблему поиска строк, содержащих целевую подстроку (например, product__image), разберем, почему стандартные подходы вроде функции СтрРазделить или циклов с чтением по индексу работают некорректно или слишком медленно, а также рассмотрим несколько высокопроизводительных решений — от классических «хаков» платформы до современных возможностей регулярных выражений.
На первый взгляд, логичным решением кажется разбиение текста на массив по разделителю — искомому слову. Однако, попытавшись применить код:
Массив = СтрРазделить(ТвояСтрока, "product__image", Ложь);
мы получим совершенно не тот результат, на который рассчитывали. Вместо разделения текста на крупные блоки по слову product__image, платформа вернет массив, состоящий из отдельных букв («p», «r», «o», «d», «u», «c», «t», «_», «i», «m», «a», «g», «e») и их мелких обрывков.
Выясним причину такого поведения системы: встроенная функция 1С СтрРазделить во втором параметре принимает не единую строку-разделитель, а набор одиночных символов-разделителей. Платформа воспринимает строку "product__image" как указание разделять исходный текст по любому из символов, входящих в это слово. В итоге ваш текст буквально «шинкуется» посимвольно.
Чтобы обойти это ограничение и все-таки использовать СтрРазделить, можно предварительно заменить искомую фразу на уникальный одиночный символ, который гарантированно отсутствует в тексте (например, на перевод строки Символы.ПС или редкий символ вроде тильды ~), а затем выполнить разделение:
ВременныйТекст = СтрЗаменить(ИсходныйТекст, "product__image", Символы.ПС);
МассивРезультатов = СтрРазделить(ВременныйТекст, Символы.ПС, Ложь);
Однако этот способ требует полного дублирования строки в оперативной памяти при замене, что на объемах в 600 000 строк может вызвать серьезную нагрузку на ОЗУ.
У многих разработчиков возникает вполне обоснованная «счетчикофобия» при работе с большими текстами. Они справедливо опасаются использовать циклы. Но дело тут не в самих циклах, а в катастрофически неоптимальной функции СтрПолучитьСтроку.
Давайте рассмотрим типичную ошибку начинающих разработчиков:
// Крайне медленный код! Опасность зависания!
ВсегоСтрок = СтрЧислоСтрок(БольшойТекст);
Для Счетчик = 1 По ВсегоСтрок Цикл
ТекущаяСтрока = СтрПолучитьСтроку(БольшойТекст, Счетчик);
Если СтрНайти(ТекущаяСтрока, "product__image") > 0 Тогда
// Обработка строки
КонецЕсли;
КонецЦикла;
Этот код имеет квадратичную сложность алгоритма $O(N^2)$. Платформа 1С не хранит текстовую строку как массив строк в памяти. При каждом вызове функции СтрПолучитьСтроку(БольшойТекст, Счетчик) система вынуждена заново читать весь текст с самого первого символа, отсчитывая символы перевода строк, пока не дойдет до нужного номера. Для текста в 600 тысяч строк этот процесс займет долгие часы, полностью заблокировав рабочий процесс.
До появления полноценных инструментов работы со строками в платформе 1С разработчики использовали оригинальный и невероятно быстрый системный «хак». Этот метод основан на преобразовании текста в формат внутренней сериализации 1С с последующим вызовом функции ЗначениеИзСтрокиВнутр(). Данный метод превращает текстовый разделитель в элемент системного массива практически мгновенно, так как парсинг происходит на уровне скомпилированного C++ кода платформы.
Рассмотрим готовую функцию реализации этого алгоритма:
Функция БыстрыйСплит(ПереданнаяСтрока, Разделитель) Экспорт
СтрокаДанных = ПереданнаяСтрока;
// Экранируем кавычки для корректного системного разбора
СтрокаДанных = СтрЗаменить(СтрокаДанных, """", """""");
// Подсчитываем число вхождений для формирования заголовка массива
ЧислоВхождений = СтрЧислоВхождений(СтрокаДанных, Разделитель);
// Заменяем наш разделитель на структуру элементов внутреннего массива 1С
СтрокаДанных = СтрЗаменить(СтрокаДанных, Разделитель, """},{""S"",""" );
// Формируем служебную обертку внутреннего формата массива 1С
СтрокаДанных = "{""#"",51e7a0d2-530b-11d4-b98a-008048da3034,{" + СтрЗаменить(ЧислоВхождений + 1, Символы.НПП, "" ) + ",{""S"",""" + СтрокаДанных + """}}}";
// Быстро преобразуем сформированную строку в реальный Массив 1С
Возврат ЗначениеИзСтрокиВнутр(СтрокаДанных);
КонецФункции
Преимущества метода:
Важные недостатки и ограничения:
ЗначениеИзСтрокиВнутр может аварийно завершить работу с ошибкой десериализации.Символы.ПС в каждом полученном элементе массива, что нивелирует выигрыш в скорости.Если ваш текст изначально загружается из внешнего файла (или его можно туда временно сохранить), то самым правильным, стабильным и быстрым решением будет последовательное чтение файла через объект ЧтениеТекста.
При таком подходе платформа не загружает в оперативную память весь гигантский объем текста целиком, а считывает его небольшими буферизованными порциями. Мы получаем линейную сложность алгоритма $O(N)$ и минимальное потребление ресурсов.
Разберем по шагам реализацию этого метода на практике:
// Создаем массив для хранения найденных номеров строк
МассивНомеровСтрок = Новый Массив;
ПутьКФайлу = "C:\temp\large_data.txt"; // Укажите ваш путь к файлу
ИскомоеСлово = "product__image";
// Инициализируем объект последовательного чтения с указанием кодировки
Чтение = Новый ЧтениеТекста(ПутьКФайлу, КодировкаТекста.UTF8);
ТекущаяСтрока = Чтение.ПрочитатьСтроку();
НомерСтроки = 1;
// Запускаем быстрый однопроходной цикл по всему файлу
Пока ТекущаяСтрока <> Неопределено Цикл
// СтрНайти работает чрезвычайно быстро, так как ищет в пределах одной небольшой строки
Если СтрНайти(ТекущаяСтрока, ИскомоеСлово) > 0 Тогда
МассивНомеровСтрок.Добавить(НомерСтроки);
КонецЕсли;
// Переходим к следующей строке и увеличиваем счетчик
ТекущаяСтрока = Чтение.ПрочитатьСтроку();
НомерСтроки = НомерСтроки + 1;
КонецЦикла;
// Закрываем файл после завершения работы
Чтение.Закрыть();
// Теперь в МассивНомеровСтрок хранятся все точные номера строк, где встретилось слово
Почему этот метод идеален для 600k+ строк?
Если вы работаете на современных релизах платформы «1С:Предприятие» (начиная с версии 8.3.23.1865), вам доступен мощный встроенный инструмент — работа с регулярными выражениями без использования медленных COM-объектов Windows.
С помощью функции СтрНайтиВсеПоРегулярномуВыражению() мы можем моментально получить позиции всех вхождений нужного текста в строке.
Проанализируем пример реализации:
// Получаем массив объектов РезультатПоискаПоРегулярномуВыражению
РезультатыПоиска = СтрНайтиВсеПоРегулярномуВыражению(ИсходныйТекст, "product__image");
МассивПозиций = Новый Массив;
Для Каждого ТекущийРезультат Из РезультатыПоиска Цикл
// Свойство НачальнаяПозиция возвращает точный индекс символа вхождения во всем тексте
МассивПозиций.Добавить(ТекущийРезультат.НачальнаяПозиция);
КонецЦикла;
Как быстро сопоставить позицию символа с номером строки?
Зная абсолютные индексы символов вхождения, мы можем определить номера строк с помощью быстрого бинарного поиска. Для этого сначала один раз собираем позиции всех символов перевода строки Символы.ПС в исходном тексте в отдельный индексированный массив чисел. Затем для каждого найденного вхождения через быстрый двоичный поиск определяем, между какими индексами переноса строк находится наше вхождение. Это работает значительно быстрее, чем повторный посимвольный обход текста.
Для эффективного решения задачи поиска в больших объемах текста придерживайтесь следующих правил:
СтрПолучитьСтроку() внутри циклов при обработке файлов или текстов объемом более 1000 строк.ЧтениеТекста. Это самый надежный, предсказуемый и не требовательный к памяти способ.СтрНайтиВсеПоРегулярномуВыражению.СтрРазделить — она делит строку по набору отдельных символов, а не по целым словам. Для разделения по словам используйте предварительную замену на уникальный символ-разделитель.