Работа с неструктурированными строками адресов — одна из классических и наиболее трудоемких задач в практике разработчика 1С. Часто адрес хранится в базе в виде единой строки, например: "Россия, Респ. Башкортостан, Белорецкий р-н., с. Ассы, ул. Мубарякова, д. 8". Когда возникает необходимость сопоставить эти данные, отсечь город от улицы или выполнить точечный поиск, стандартные механизмы запросов начинают давать сбои. Конструкция ПОДОБНО "%...%" работает медленно и неэффективно на больших объемах данных, а встроенный язык СУБД не предоставляет простых функций для работы со строками.
В этой статье мы подробно разберем, как решить проблему разделения адреса на составляющие. Мы рассмотрим как традиционные подходы (выгрузка в таблицу значений, использование временных таблиц), так и современные нативные инструменты платформы 1С, включая регулярные выражения и возможности адресного классификатора.
Если задача позволяет обработать адреса во встроенном языке перед выводом или записью, мы можем отказаться от медленных функций БСП и использовать быстрые нативные функции платформы 1С. Начиная с версии 8.3.6, в платформе появились функции СтрРазделить и СтрСоединить, написанные на C++ и работающие значительно быстрее любых аналогов на встроенном языке.
Рассмотрим по шагам, как разложить строку адреса в массив по запятым, отсечь ненужные части и собрать адрес обратно:
АдресСтрока = "Россия, Респ. Башкортостан, Белорецкий р-н., с. Ассы, ул. Мубарякова, д. 8";
// Шаг 1. Разбиваем строку по запятым
МассивЧастей = СтрРазделить(АдресСтрока, ",");
// Шаг 2. Анализируем полученные части
МассивДляНовогоАдреса = Новый Массив;
УлицаИДом = "";
Для Каждого ЧастьАдреса Из МассивЧастей Цикл
ОчищеннаяЧасть = СокрЛП(ЧастьАдреса);
// Ищем ключевые сокращения улиц и домов
Если Лев(ОчищеннаяЧасть, 3) = "ул." Или Лев(ОчищеннаяЧасть, 2) = "д." Тогда
УлицаИДом = УлицаИДом + ?(ЗначениеЗаполнено(УлицаИДом), ", ", "") + ОчищеннаяЧасть;
Иначе
МассивДляНовогоАдреса.Добавить(ОчищеннаяЧасть);
EndIf;
КонецЦикла;
// Шаг 3. Собираем очищенную адресную часть (без улицы и дома) обратно
АдресБезУлицы = СтрСоединить(МассивДляНовогоАдреса, ", ");
Сообщить("Регион/Город: " + АдресБезУлицы);
Сообщить("Улица/Дом: " + УлицаИДом);
Этот метод прост, надежен и отлично подходит для постобработки результатов запроса.
Если в вашем распоряжении современная версия платформы 1С (начиная с 8.3.23), вы можете использовать встроенную поддержку регулярных выражений (RegEx). Это полностью исключает необходимость ручного поиска символов, подсчета запятых или использования нестабильных COM-объектов на Linux-серверах.
Выясним, как с помощью регулярного выражения быстро вычленить из строки только информацию об улице и доме:
АдресСтрока = "Россия, Респ. Башкортостан, Белорецкий р-н., с. Ассы, ул. Мубарякова, д. 8";
ШаблонУлицы = "ул\.\s?[А-Яа-яЁё\s]+,\s?д\.\s?\d+";
// Ищем совпадение шаблона в строке адреса
РезультатПоиска = СтрНайтиПоРегулярномуВыважению(АдресСтрока, ШаблонУлицы);
Если РезультатПоиска.Длина > 0 Тогда
УлицаИДом = Сред(АдресСтрока, РезультатПоиска.Начало, РезультатПоиска.Длина);
// Для получения адреса без улицы удаляем найденную подстроку
АдресБезУлицы = СтрЗаменитьПоРегулярномуВыражению(АдресСтрока, ШаблонУлицы, "");
Сообщить("Найдено: " + УлицаИДом);
Сообщить("Остаток адреса: " + СокрЛП(АдресБезУлицы));
КонецЕсли;
Регулярные выражения позволяют обрабатывать даже самые нестандартные написания адресов, гибко подстраивая шаблоны под особенности ввода пользователей.
Если результат парсинга критически важно использовать в дальнейшем сложном запросе (например, для соединения с другими таблицами БД), наилучшим архитектурным решением будет предварительная обработка данных. Прямой парсинг строк внутри SQL-запроса снижает производительность СУБД.
Проанализируем эффективный алгоритм реализации этой схемы:
ТаблицаЗначений.СтрРазделить или регулярными выражениями, разделяя адрес на нужные колонки.ТаблицуЗначений обратно в новый запрос через менеджер временных таблиц для финального сопоставления.Рассмотрим пример кода, реализующий данный подход:
// Шаг 1. Получаем сырые данные
ЗапросПервичный = Новый Запрос;
ЗапросПервичный.Текст =
"ВЫБРАТЬ
| Контрагенты.Ссылка КАК Контрагент,
| Контрагенты.АдресСтрокой КАК АдресСтрокой
|ИЗ
| Справочник.Контрагенты КАК Контрагенты";
ТаблицаАдресов = ЗапросПервичный.Выполнить().Выгрузить();
// Добавляем новые типизированные колонки для разделенного адреса
ТаблицаАдресов.Колонки.Добавить("ГородскойОкруг", Новый ОписаниеТипов("Строка",,,, Новый КвалификаторыСтроки(150)));
ТаблицаАдресов.Колонки.Добавить("УлицаИДом", Новый ОписаниеТипов("Строка",,,, Новый КвалификаторыСтроки(150)));
// Шаг 2. Парсим строки адреса во встроенном языке
Для Каждого СтрокаАдреса Из ТаблицаАдресов Цикл
МассивЧастей = СтрРазделить(СтрокаАдреса.АдресСтрокой, ",");
// Простая логика: считаем, что первые 3 части - это город/район, остальное - улица
Если МассивЧастей.Количество() >= 3 Тогда
СтрокаАдреса.ГородскойОкруг = СокрЛП(МассивЧастей[0]) + ", " + СокрЛП(МассивЧастей[1]) + ", " + СокрЛП(МассивЧастей[2]);
// Собираем оставшуюся часть адреса
МассивУлицы = Новый Массив;
Для Индекс = 3 По МассивЧастей.В Граница() Цикл
МассивУлицы.Добавить(СокрЛП(МассивЧастей[Индекс]));
КонецЦикла;
СтрокаАдреса.УлицаИДом = СтрСоединить(МассивУлицы, ", ");
Иначе
СтрокаАдреса.ГородскойОкруг = СтрокаАдреса.АдресСтрокой;
КонецЕсли;
КонецЦикла;
// Шаг 3. Передаем обработанную таблицу в новый запрос через МВТ
МенеджерВТ = Новый МенеджерВременныхТаблиц;
ЗапросФинальный = Новый Запрос;
ЗапросФинальный.МенеджерВременныхТаблиц = МенеджерВТ;
// Помещаем таблицу значений во временную таблицу в запросе
ЗапросФинальный.УстановитьПараметр("ТаблицаАдресов", ТаблицаАдресов);
ЗапросФинальный.Текст =
"ВЫБРАТЬ *
|ПОМЕСТИТЬ ВТ_Адреса
|ИЗ
| &ТаблицаАдресов КАК ТаблицаАдресов
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ВТ_Адреса.Контрагент,
| ВТ_Адреса.ГородскойОкруг,
| ВТ_Адреса.УлицаИДом
|ИЗ
| ВТ_Адреса КАК ВТ_Адреса";
Результат = ЗапросФинальный.Выполнить().Выбрать();
Пока Результат.Следующий() Цикл
// Выполняем дальнейшую бизнес-логику
КонецЦикла;
Существует продвинутый и нестандартный прием, позволяющий обойти ограничения встроенного языка запросов 1С и разбить строку по запятым прямо на уровне СУБД. Этот метод крайне полезен, когда выгрузка миллионов записей в оперативную память сервера приложений невозможна из-за жестких требований к производительности.
Суть метода заключается в генерации последовательности чисел (индексов символов) и посимвольном анализе строки через функцию ПОДСТРОКА. Сначала мы создаем виртуальную таблицу чисел от 1 до 100, затем соединяем ее с нашей таблицей адресов и находим позиции запятых. На основе позиций запятых с помощью функций ВЫБОР мы вычленяем нужные подстроки.
Посмотрим на пример построения такого запроса:
ВЫБРАТЬ 1 КАК Число ПОМЕСТИТЬ Цифры
ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 2 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 3 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 4
ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 5 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 6 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 7
ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 8 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 9 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 0
;
// Генерируем таблицу чисел от 1 до 100 через кросс-соединение
ВЫБРАТЬ
(Т1.Число * 10 + Т2.Число) + 1 КАК НомерСимвола
ПОМЕСТИТЬ ТаблицаЧисел
ИЗ
Цифры КАК Т1
ПОЛНОЕ СОЕДИНЕНИЕ Цифры КАК Т2 ПО ИСТИНЕ
;
// Находим позиции всех запятых в строке адреса
ВЫБРАТЬ
Адреса.Ссылка КАК Ссылка,
Адреса.Адрес КАК Адрес,
ТаблицаЧисел.НомерСимвола КАК ПозицияЗапятой
ПОМЕСТИТЬ ПозицииЗапятых
ИЗ
Справочник.Адреса КАК Адреса
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ТаблицаЧисел КАК ТаблицаЧисел
ПО ТаблицаЧисел.НомерСимвола <= ПОДХОДЯЩАЯ_ДЛИНА_СТРОКИ // Ограничиваем длину
И ПОДСТРОКА(Адреса.Адрес, ТаблицаЧисел.НомерСимвола, 1) = ","
;
// Вычленяем части адреса на основе найденных позиций
ВЫБРАТЬ
ПозицииЗапятых.Ссылка,
ПОДСТРОКА(ПозицииЗапятых.Адрес, 1, МИНИМУМ(ПозицииЗапятых.ПозицияЗапятой) - 1) КАК СтранаИРегион
ИЗ
ПозицииЗапятых КАК ПозицииЗапятых
СГРУППИРОВАТЬ ПО
ПозицииЗапятых.Ссылка,
ПозицииЗапятых.Адрес
Важное примечание: Этот метод сложен в отладке и поддержке. Используйте его только в критических с точки зрения оптимизации сценариях.
Если ваша конфигурация разработана с использованием Библиотеки стандартных подсистем (БСП), вам нет необходимости писать собственные алгоритмы разбора строк. В БСП встроены мощные механизмы взаимодействия с Государственным адресным реестром (ГАР/ФИАС). Дабы не "изобретать велосипед", задействуем функционал модуля адресного классификатора.
Для разбора произвольной строки адреса в структурированное представление используйте метод:
// Локальный разбор строки по загруженному адресному классификатору ГАР/ФИАС
РезультатРаспознавания = АдресныйКлассификаторСлужебный.РаспознатьАдрес(СтрокаАдреса);
Если локальный классификатор не загружен в базу (поскольку он занимает терабайты дискового пространства), можно воспользоваться официальным облачным веб-сервисом 1С (в рамках поддержки 1С:Контрагент):
// Запрос к веб-сервису 1С для парсинга адреса любой сложности
РезультатВеб = АдресныйКлассификаторСлужебный.РаспознатьАдресВебСервис(СтрокаАдреса);
В результате работы этих функций система вернет готовую структуру, содержащую отдельно регион, район, город, населенный пункт, улицу, дом и квартиру. Этот метод является наиболее надежным и современным стандартом в экосистеме 1С.
Если база данных полностью самописная и интеграция БСП невозможна или нецелесообразна, отличной альтернативой выступает использование внешних специализированных API, таких как DaData. Сервис позволяет бесплатно или по подписке отправлять неструктурированные строки адресов и получать назад полностью валидированный, разобранный по полям JSON.
Реализуем процедуру разбора адреса через HTTP-соединение:
Функция РазобратьАдресЧерезДадату(СыройАдрес, ТокенAPI, СекретныйКлюч) Экспорт
HTTPСоединение = Новый HTTPСоединение("clean.dadata.ru", 443,,,,, Новый ЗащищенноеСоединениеOpenSSL);
HTTPЗапрос = Новый HTTPЗапрос("/api/v1/clean/address");
HTTPЗапрос.Заголовки.Вставить("Content-Type", "application/json");
HTTPЗапрос.Заголовки.Вставить("Authorization", "Token " + ТокенAPI);
HTTPЗапрос.Заголовки.Вставить("X-Secret", СекретныйКлюч);
// Передаем адрес массивом в теле запроса
ТелоЗапроса = "[""" + СыройАдрес + """]";
HTTPЗапрос.УстановитьТелоШаблона(ТелоЗапроса);
Попытка
HTTPОтвет = HTTPСоединение.ОтправитьДляОбработки(HTTPЗапрос);
Если HTTPОтвет.КодСостояния = 200 Тогда
ЧтениеJSON = Новый ЧтениеJSON;
ЧтениеJSON.УстановитьСтроку(HTTPОтвет.ПолучитьТелоКакСтроку());
РезультатАнализа = ПрочитатьJSON(ЧтениеJSON);
ЧтениеJSON.Закрыть();
// Возвращаем структуру с разделенными полями адреса
Если РезультатАнализа.Количество() > 0 Тогда
Возврат РезультатАнализа[0];
КонецЕсли;
КонецЕсли;
Исключение
ЗаписьЖурналаРегистрации("ИнтеграцияDaData", УровеньЖурналаРегистрации.Ошибка,,, ОписаниеОшибки());
КонецПопытки;
Возврат Неопределено;
КонецФункции
Полученный от DaData объект будет содержать такие ключи, как city (город), street (улица), house (дом) и даже географические координаты, что полностью закрывает потребность в ручном парсинге.