Время прочтения: 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):
Для подготовки адреса к поиску в эластике автор использует следующие методы NLP:
- Регулярные выражения
- Токенизация по словам
Регулярными выражениями очищаем адрес от ненужных символов и знаков препинания, а также извлекаем индекс, если он есть. После удаления всего лишнего адрес готов к токенизации:
tokens = [‘россия’, ‘г’, ‘санкт’, ‘петербург’, ‘район’, ‘приморский’, ‘город’, ‘санкт’, ‘петербург’, ‘проспект’, ‘шуваловский’, ‘д’, ’59’, ‘кор’, ‘1’, ‘кв’, ‘9’] Теперь происходит распознавание токенов для извлечения номера дома, строения и других элементов:
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('строение', ' ')}"
Результат:
В таком виде будет происходить построчное сравнение адресов разных баз данных, с целью найти совпадения. На рисунке ниже представлен результат нормализации адресов автора проекта.
Подробно ознакомиться с результатами автора можно в файле проекта «ref/references.xlsx».
Проблемы и недостатки.
Основная проблема заключается в «чистоте» исходных адресов. Вероятно, придется доработать функции токенизации и очистки адреса в «parsing.py» для конкретного случая.
Также в эластике могут отсутствовать некоторые адреса, номера домов, корпусов и строений, но они присутствуют на сайте ФИАС. В результате может быть получен неверный адрес. Это может быть связанно с ошибками при загрузке данных в эластик. Не исключены и ошибки в самой базе ФИАС.
Заключение.
В целом, данный проект подходит для нормализации адресов по стандарту ФИАС. С незначительной доработкой можно получить 90-95% точности нормализации. Дополнительно можно ознакомиться со статьей, где подробно разобрана структура БД ФИАС и показан пример импорта данных в реляционную базу PostgreSQL.