Время прочтения: 5 мин.

Передо мной стояла задача сравнения большого количества адресов из разных баз данных с целью поиска совпадений. И единственным рациональным, на мой взгляд, решением было привести адреса к единому виду. Для нормализации адресов использован единый российский государственный адресный реестр — ФИАС, базы которого находятся в открытом доступе.

Набор инструментов.

  Минимальный набор инструментов для реализации проекта:

— 60Гб – 100Гб свободного места

— Python 3+ и библиотеки elasticsearch,  simpledbf и pandas

— Еlasticsearch 7+

— Полная база данных ФИАС’а в dbf формате (https://fias.nalog.ru/Updates)

— Open Address Parser – проект для загрузки БД ФИАС’а и поиска в Elasticsearch  (https://github.com/shigabeev/address-segmenter#open-address-parser)

Подготовка к работе.

Скачиваем Open Address Parser. В репозитории находится подробная инструкция по установке необходимых компонентов.

В моем случае, ввиду некоторых ограничений, Elasticsearch был установлен напрямую, без Docker и Kibana. Запуск сервиса в такой ситуации происходит через «…\Elasticsearch\bin\elasticsearch.bat». На шаге «3. Загрузить ФИАС в elasticsearch» необходимо запустить скрипт upload_fias.py в режиме отладки(Debug) или построчно из Jupyter Notebook, как советует автор. Предварительно следует распаковать архив БД ФИАС и указать путь к нему:

parser = argparse.ArgumentParser()
parser.add_argument('--fiasdir', default=r'путь\к\файлам\*.dbf')

Запускаем Elasticsearch и проверяем его работоспособность строкой в браузере:

http://localhost:9200/

Теперь выполняем скрипт upload_fias.py в режиме отладки(Debug), предварительно расставив точки останова на местах преобразования таблиц в «csv» формат и загрузки их в Elasticsearch.

Для получения информации по квартирам/офисам и земельным участкам, следует раскомментировать строки, в которых содержаться аргументы ‘ROOM*’ и ‘STEAD*’ соответственно. В рамках моей задачи они не нужны. Также следует раскомментировать строку с функцией load_elastic():

# Загрузка самой главной таблицы
# На первых порах её нам хватит. Остальные загружаются при надобности
# Занимает 2 часа
# FIXME
load_elastic(os.path.join(fias_csv_dir, 'ADDROBJ.csv'), 'fias', 'address')

Если произошла критическая ошибка в процессе загрузки таблиц в Elasticsearch, следует удалить неполную таблицу из эластика по индексу (пример: index=»fias_full_text») и загрузить ее заново. Для повышения отказоустойчивости можно добавить следующие аргументы клиенту эластика:

es = Elasticsearch(timeout=30, max_retries=10, retry_on_timeout=True)

После успешного выполнения скрипта можно приступать к нормализации адресов.

Стандартизация адреса.

Пример нормализации адреса:

import api
addr = "РОССИЯ,197373,г. Санкт-Петербург,РАЙОН ПРИМОРСКИЙ,Город САНКТ-ПЕТЕРБУРГ,,Проспект ШУВАЛОВСКИЙ,д. 59,кор. 1,,кв. 9"
norm_addr = api.standardize(addr)

На вход подается необработанный адрес с лишними словами и пунктуацией. Для понимания процесса стандартизации предлагаю ознакомиться с тем, что происходит внутри функции api.standardize(addr):

Рисунок 1. Функция api.standardize().

Для подготовки адреса к поиску в эластике автор использует следующие методы NLP:

  • Регулярные выражения
  • Токенизация по словам

Регулярными выражениями очищаем адрес от ненужных символов и знаков препинания, а также извлекаем индекс, если он есть. После удаления всего лишнего адрес готов к токенизации:

tokens = [‘россия’, ‘г’, ‘санкт’, ‘петербург’, ‘район’, ‘приморский’, ‘город’, ‘санкт’, ‘петербург’, ‘проспект’, ‘шуваловский’, ‘д’, ’59’, ‘кор’, ‘1’, ‘кв’, ‘9’] Теперь происходит распознавание токенов для извлечения номера дома, строения и других элементов:

Рисунок 2. Функция parsing.extract_house_tokens(tokens).
house_tokens = ['д', '59', 'кор', '1', 'кв', '9']
house_types = ['дом', 'число', 'корпус', 'число', 'квартира', 'число']

Функция parsing.clarify_address(tokens, types) сопоставляет токены с их типом и возвращает словарь с информацией по дому:

house = {'дом': '59', 'корпус': '1', 'квартира': '9', 'Дом': '59', 'Корпус/строение': '1'}

И следом происходит отделение информации о доме от адреса:

address = 'РОССИЯ г Санкт Петербург РАЙОН ПРИМОРСКИЙ Город САНКТ ПЕТЕРБУРГ Проспект ШУВАЛОВСКИЙ '

С помощью запроса производим поиск в эластике по адресу. Найдя совпадение, мы получаем уникальный идентификатор адресного объекта(AOGUID) и по нему создаем поисковой запрос в эластик для конкретного дома/корпуса/строения.

В конечном итоге функция api.standardize() возвращает словарь с данными об  объекте: полный адрес, номер дома, корпус, строение, почтовый индекс и другую кадастровую информацию. В рамках задачи, функция api.standardize(string) была изменена и теперьвозвращает строку следующего вида:

return f"{dic['fullname']}|{dic.get('дом', ' ')}|{dic.get('корпус', ' ')}|{dic.get('строение', ' ')}"

Результат:

Вертикальная черта «|» разделяет полный адрес, номер дома, корпус и строение.

В таком виде будет происходить построчное сравнение адресов разных баз данных, с целью найти совпадения. На рисунке ниже представлен результат нормализации адресов автора проекта.

Рисунок 3. Пример нормализации.

Подробно ознакомиться с результатами автора можно в файле проекта «ref/references.xlsx».

Проблемы и недостатки.

Основная проблема заключается в «чистоте» исходных адресов. Вероятно, придется доработать функции токенизации и очистки адреса в «parsing.py» для конкретного случая.

Также в эластике могут отсутствовать некоторые адреса, номера домов, корпусов и строений, но они присутствуют на сайте ФИАС. В результате может быть получен неверный адрес. Это может быть связанно с ошибками при загрузке данных в эластик. Не исключены и ошибки в самой базе ФИАС.

Заключение.

В целом, данный проект подходит для нормализации адресов по стандарту ФИАС. С незначительной доработкой можно получить 90-95% точности нормализации. Дополнительно можно ознакомиться со статьей, где подробно разобрана структура БД ФИАС и показан пример импорта данных в реляционную базу PostgreSQL.