В процессе интеграции 1С с внешними системами и веб-сервисами разработчики регулярно сталкиваются с необходимостью парсинга входящих текстовых строк. Задача выглядит тривиальной, когда строка имеет четкие разделители. Однако на практике часто приходят строки, в которых параметры расположены вплотную друг к другу и могут состоять из произвольного количества слов.
Рассмотрим классический пример такой строки:
Комиссия за реализацию товара от 21.05.2019 склад Центральный склад Москва по счету 152356
Шаблон (маска) этой строки выглядит следующим образом:
Комиссия за реализацию товара от %Дата% склад %НаименованиеСклада% %Город% по счету %НомерСчета%
Главная сложность заключается в том, что параметры %НаименованиеСклада% (например, "Центральный склад") и %Город% ("Москва") идут подряд без промежуточных статических разделителей. При этом и наименование склада, и название города могут состоять из нескольких слов (например, склад "Первый консигнационный склад", город "Нижний Новгород"). Дополнительно ситуация усложняется тем, что склады могут отсутствовать в базе данных 1С на момент загрузки, то есть их необходимо создавать «на лету».
В этой статье мы подробно разберем четыре практических метода решения этой задачи: от классического строкового разбора до использования современных регулярных выражений и внешних API стандартизации данных.
Если мы работаем на старых версиях платформы или не хотим использовать внешние библиотеки, задачу можно решить последовательным отсечением известных (статических) частей строки. Выясним алгоритм разбора по шагам:
"Центральный склад Москва" или "Склад-1 Подольск".Проанализируем реализацию данного алгоритма на встроенном языке 1С:
Функция РазобратьСтрокуВручную(Знач ВходнаяСтрока) Экспорт
Результат = Новый Структура("Дата, Склад, Город, НомерСчета", Неопределено, "", "", "");
// Шаг 1. Отсекаем статичное начало
Префикс = "Комиссия за реализацию товара от ";
ПозПрефикса = СтрНайти(ВходнаяСтрока, Префикс);
Если ПозПрефикса = 0 Тогда
Возврат Результат;
КонецЕсли;
РабочаяСтрока = Сред(ВходнаяСтрока, ПозПрефикса + СтрДлина(Префикс));
// Шаг 2. Выделяем дату (10 символов)
СтрокаДаты = Лев(РабочаяСтрока, 10);
Попытка
Результат.Дата = Дата(Сред(СтрокаДаты, 7, 4) + Сред(СтрокаДаты, 4, 2) + Лев(СтрокаДаты, 2));
Исключение
Результат.Дата = Неопределено;
КонецПопытки;
РабочаяСтрока = СокрЛП(Сред(РабочаяСтрока, 11));
// Удаляем слово "склад " из начала оставшейся строки, если оно присутствует
Если Лев(РабочаяСтрока, 6) = "склад " Тогда
РабочаяСтрока = СокрЛП(Сред(РабочаяСтрока, 7));
КонецЕсли;
// Шаг 3. Находим номер счета в конце строки
ПозСчета = СтрНайти(РабочаяСтрока, " по счету ");
Если ПозСчета > 0 Тогда
Результат.НомерСчета = СокрЛП(Сред(РабочаяСтрока, ПозСчета + 10));
РабочаяСтрока = СокрЛП(Лев(РабочаяСтрока, ПозСчета - 1));
КонецЕсли;
// Шаг 4. На данном этапе в РабочаяСтрока осталось только "Склад Город" (например, "Центральный склад Нижний Новгород")
РазделитьСкладИГород(РабочаяСтрока, Результат.Склад, Результат.Город);
Возврат Результат;
КонецФункции
Для надежного разделения склада и города внутри процедуры РазделитьСкладИГород мы воспользуемся перебором слов с конца строки и сопоставлением со списком известных городов (его можно подгрузить из КЛАДР/ФИАС или вести вручную в базе 1С):
Процедура РазделитьСкладИГород(Знач СтрокаСкладГород, Склад, Город)
МассивСлов = СтрРазделить(СтрокаСкладГород, " ", Ложь);
КоличествоСлов = МассивСлов.Количество();
Если КоличествоСлов = 0 Тогда
Возврат;
КонецЕсли;
// Сначала проверяем города, состоящие из двух слов (например, "Великий Новгород", "Ростов-на-Дону")
Если КоличествоСлов >= 2 Тогда
ПредполагаемыйГород = МассивСлов[КоличествоСлов - 2] + " " + МассивСлов[КоличествоСлов - 1];
Если ГородСуществуетВБазе(ПредполагаемыйГород) Тогда
Город = ПредполагаемыйГород;
Для Инд = 0 По КоличествоСлов - 3 Цикл
Склад = Склад + ?(ЗначениеЗаполнено(Склад), " ", "") + МассивСлов[Инд];
КонецЦикла;
Возврат;
КонецЕсли;
КонецЕсли;
// Проверяем классический односложный город (например, "Москва")
ПредполагаемыйГород = МассивСлов[КоличествоСлов - 1];
Если ГородСуществуетВБазе(ПредполагаемыйГород) Тогда
Город = ПредполагаемыйГород;
Для Инд = 0 По КоличествоСлов - 2 Цикл
Склад = Склад + ?(ЗначениеЗаполнено(Склад), " ", "") + МассивСлов[Инд];
КонецЦикла;
Возврат;
КонецЕсли;
// Дефолтный сценарий: если город не определен в базе, берем последнее слово как город
Город = ПредполагаемыйГород;
Для Инд = 0 По КоличествоСлов - 2 Цикл
Склад = Склад + ?(ЗначениеЗаполнено(Склад), " ", "") + МассивСлов[Инд];
КонецЦикла;
КонецПроцедуры
Если решение разворачивается на современной версии платформы «1С:Предприятие 8» (начиная с релиза 8.3.23), разработчикам доступен мощный нативный механизм регулярных выражений. Нам больше не нужно подключать медленный COM-объект VBScript.RegExp, который к тому же не работает на серверах под управлением ОС Linux.
Посмотрим на пример использования функции СтрНайтиПоРегулярномуВыражению. С помощью одного регулярного выражения мы можем извлечь дату, номер счета и изолировать блок «склад-город»:
Функция РазобратьСтрокуЧерезРегулярку(СтрокаИсточник) Экспорт
Результат = Новый Структура("Дата, СкладГород, НомерСчета", "", "", "");
// Шаблон регулярного выражения с тремя группами захвата (круглые скобки)
// 1 группа: дата в формате ДД.ММ.ГГГГ
// 2 группа: название склада и город (любые символы, ленивый поиск)
// 3 группа: номер счета в конце строки (только цифры)
Шаблон = "^Комиссия за реализацию товара от (\d{2}\.\d{2}\.\d{4}) склад (.+?) по счету (\d+)$";
МассивРезультатов = СтрНайтиПоРегулярномуВыражению(СтрокаИсточник, Шаблон);
Если МассивРезультатов.Количество() > 0 Тогда
РезультатПоиска = МассивРезультатов[0];
// Первая группа (индекс 0) - это вся найденная строка, последующие индексы - наши группы захвата
Если РезультатПоиска.Группы.Количество() >= 4 Тогда
Результат.Дата = РезультатПоиска.Группы[1].Значение;
Результат.СкладГород = РезультатПоиска.Группы[2].Значение;
Результат.НомерСчета = РезультатПоиска.Группы[3].Значение;
КонецЕсли;
КонецЕсли;
Возврат Результат;
EndФункции
После отработки регулярного выражения мы получаем очищенную подстроку Результат.СкладГород (например, "Центральный склад Москва"), которую далее разделяем алгоритмом поиска по списку городов, описанным в первом способе.
Для создания гибкой и расширяемой системы под несколько различных масок (например, если разные сервисы присылают строки в разном формате) мы можем реализовать универсальный парсер. Пользователь в пользовательском режиме 1С задает маску вида:
Комиссия за реализацию товара от %Дата% склад %Склад% %Город% по счету %НомерСчета%
Система при обработке строки автоматически конвертирует эту маску в регулярное выражение по правилам:
%Дата% заменяется на regexp-шаблон даты (\d{2}\.\d{2}\.\d{4}).%НомерСчета% заменяется на regexp-шаблон цифр (\d+).(.+?).Таким образом, мы получаем универсальный механизм разбора строк по шаблонам без необходимости переписывать программный код при изменении формата входящих данных.
Что делать, если название склада может содержать в себе название города (например, "Склад Подольск Сити") или названия городов абсолютно непредсказуемы, а держать в 1С огромный и постоянно обновляемый КЛАДР нецелесообразно? В современной практике для очистки и структурирования подобных текстовых данных используют внешние NLP-микросервисы распознавания адресов (например, DaData).
Разберем этот элегантный подход по шагам:
"Центральный склад Нижний Новгород".Реализуем интеграцию с внешним API на встроенном языке 1С:
Функция СтандартизироватьАдресЧерезСервис(Знач НеразобранныйТекст) Экспорт
Результат = Новый Структура("Город, НазваниеСклада", "", "");
HTTPСоединение = Новый HTTPСоединение("cleaner.dadata.ru", 443,,,,, Новый ЗащищенноеСоединениеOpenSSL());
HTTPЗапрос = Новый HTTPЗапрос("/api/v1/clean/address");
HTTPЗапрос.Заголовки.Вставить("Content-Type", "application/json");
HTTPЗапрос.Заголовки.Вставить("Authorization", "Token " + ПолучитьТокенDaData());
HTTPЗапрос.Заголовки.Вставить("X-Secret", "ID_Секретного_Ключа");
// Передаем текст в виде JSON-массива строк
ТелоЗапроса = "[""" + НеразобранныйТекст + """]";
HTTPЗапрос.УстановитьТелоИзСтроки(ТелоЗапроса, КодировкаТекста.UTF8, ИспользованиеByteOrderMark.НеИспользовать);
Попытка
HTTPОтвет = HTTPСоединение.ОтправитьДляОбработки(HTTPЗапрос);
Если HTTPОтвет.КодСостояния = 200 Тогда
ЧтениеJSON = Новый ЧтениеJSON;
ЧтениеJSON.УстановитьСтроку(HTTPОтвет.ПолучитьТелоКакСтроку(КодировкаТекста.UTF8));
МассивОтветов = ПрочитатьJSON(ЧтениеJSON);
ЧтениеJSON.Закрыть();
Если МассивОтветов.Количество() > 0 Тогда
ДанныеАдреса = МассивОтветов[0];
Результат.Город = ДанныеАдреса.city;
Результат.НазваниеСклада = ДанныеАдреса.unparsed_parts; // Всё, что сервис не определил как адрес
КонецЕсли;
КонецЕсли;
Исключение
ЗаписьЖурналаРегистрации("ИнтеграцияAPI.ОшибкаРазбораАдреса",
УровеньЖурналаРегистрации.Ошибка,,, ОписаниеОшибки());
КонецПопытки;
Возврат Результат;
КонецФункции
Этот подход гарантирует высочайшую точность разделения параметров без необходимости написания сложных эвристических алгоритмов внутри вашей конфигурации 1С.