Координатный квест: как найти координаты и расстояния без регистраций и смс

от автора

Привет, Хабр!

С вами участник профессионального сообщества NTA Алексей Майка.

Хочу поделиться своим опытом решения одной интересной задачки и описать весь проделанный путь.

Был обычный денёк, сидел я на работе и занимался своими айтишными делами. Ко мне пришел руководитель и сказал: «Нужно рассчитать дистанцию до границы регионов для этих адресов». При этом без всяких платных сервисов и API онлайн карт, и своими усилиями. Айтишник понял, айтишник принял, айтишник получил свою заветную эксельку и пошёл работать.

План работы

Вступление

Из школьных уроков географии я помнил, что для определения километража требуется знать координаты (широту и долготу) двух точек. И исходя из этого, я разделил задачу на 4 части:

  • поиск координат границы;

  • предобработка данных;

  • поиск координат адресов;

  • непосредственный расчёт расстояний между координатами.

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

В качестве основного инструмента для парсинга, обработки и расчётов я использовал Python. Средой разработки выступали Jupyter Notebook (Anaconda), PyCharm и DataSpell от компании JetBrains (дело вкуса). При работе с данным проектом использовал библиотеки Numpy, Pandas, Plotly, Geopy, Selenium.

На этом прелюдия заканчивается, переходим к сути.

Начало начал

Для расчёта дистанции до границы нужны координаты, что неудивительно, самой границы. Вручную прокликивать точки на карте мне не очень хотелось, а попытка поиска готовых координат полностью провалилась. К счастью, удалось найти json‑файл с положением границ субъектов России, среди которых и находятся нужные точки.

Для начала достаю нужные области. Импортирую библиотеки для дальнейшей работы, сохраняю данные файла в словарь (dict) и смотрю на содержимое объекта:

#Библиотека для работы с ".json"-файлами import json  #Библиотека для обработки и анализа данных import pandas as pd #Библиотека для работы с многомерными массивами import numpy as np  # Считываем файл с координатами всех регионов with open('data//gadm41_RUS_1.json', encoding = 'utf-8') as js:     dict_coordin_border = json.load(js)
. Словарь с данными из файла

. Словарь с данными из файла

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

  • dict_coordin_border[‘features’] [‘properties’][‘NL_NAME_1’] — название субъекта федерации;

  • dict_coordin_border[‘features’] [‘geometry’][‘coordinates’] — координаты границ субъектов.

Выделяю из данного словаря только нужные пять областей, и записываю в pandas.DataFrame данные, где:

  • region — название региона;

  • lon — долгота точки границы;

  • lat — широта точки границы;

  • sequence_number — порядковый номер записи;

  • color — цвет региона.

Зачем цвет и порядковый номер? Расскажу далее, а сейчас предлагаю рассмотреть код:

Посмотреть код
df_coord_reg = pd.DataFrame() sequence_number = 0  for regions in dict_coordin_border['features']:     #Ставим условия для поля названия субъектов     if regions['properties']['NL_NAME_1'] in ['Воронежскаяобласть',                                               'Брянскаяобласть',                                               'Курскаяобласть',                                               'Ростовскаяобласть',                                               'Белгородскаяобласть']:          for list_coordin_lv_1 in regions['geometry']['coordinates']:             for list_coordin_lv_2 in list_coordin_lv_1:                 for list_coordin_finish_lvl in list_coordin_lv_2:                      #Заполняем df: Название региона, координаты точки границы, порядковый номер записи, цвет региона                     if regions['properties']['NL_NAME_1'] == 'Воронежскаяобласть': color = 'purple'                     elif regions['properties']['NL_NAME_1'] == 'Брянскаяобласть': color = 'white'                     elif regions['properties']['NL_NAME_1'] == 'Курскаяобласть': color = 'blue'                     elif regions['properties']['NL_NAME_1'] == 'Ростовскаяобласть': color = 'yellow'                     elif regions['properties']['NL_NAME_1'] == 'Белгородскаяобласть': color = 'red'                      df_coord_reg = df_coord_reg.append({'region': regions['properties']['NL_NAME_1'], #Название региона                                                         'lon': list_coordin_finish_lvl[0], #Долгота точки границы                                                         'lat': list_coordin_finish_lvl[1], #Широта точки границы                                                         'sequence_number': str(sequence_number), #Порядковый номер записи                                                         'color': color}, #Цвет региона                                                        ignore_index = True)                     sequence_number += 1

В итоге получается следующий dataframe:

Результат выполнения кода

Результат выполнения кода

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

О plotly

Plotly — это графическая библиотека для интерактивной визуализации данных. С её помощью можно создавать диаграммы, гистограммы, карты распределения, 2D‑диаграммы, 3D‑графики и многое другое. Эта библиотека — сильный «зверь» для визуала, и она поможет расположить полученные точки на карте. Подробнее ознакомиться можно по ссылке.

Код ниже отображает точки на географической карте Европы:

Код
#Импортируем библиотеки для визуализации данных import plotly.graph_objs as go  #Визуализируем на карте точки с координатами для проверки и дальнейшего анализа  fig = go.Figure(data=go.Scattergeo(                       #Scattergeo - данные, визуализируемые в виде точек географической карте                   lon = df_coord_reg['lon'],                      #Долгота точки                   lat = df_coord_reg['lat'],                        #Широта точки                   mode = 'markers',                                   #Вид точки                   marker_color  = df_coord_reg['color'],  #Цвет точки                   text = df_coord_reg['region'] + ' ' + df_coord_reg['sequence_number']  #Текст при наведении на точку                 ),             )  fig.update_layout(         title = 'Субъекты РФ ',      #Задаем название карты         geo = dict(             scope='europe',                                   #Шаблон карты              landcolor = "green",                           #Цвет для стран             countrycolor = "black",                      #Цвет границ между странами         ),         width=1500,                                            #Ширина карты         height=750                                              #Высота графика     )

Результат выполнения кода:

Субъекты РФ

Субъекты РФ

Как видно на рисунке, все точки находятся на своих местах. Осталось из них выбрать только точки, не являющиеся общими для регионов. Для этого я и задавал цвет областей и их порядковый номер.

Выбираю номера точек, которые находятся на границе, и перезаписываю данные в dataframe:

#Исходя из карты, выбираем следующие срезы df и записываем их в новую переменную df_coord_border = pd.concat([df_coord_reg[11:411],                              df_coord_reg[1278:1459],                              df_coord_reg[974:1226],                              df_coord_reg[3084:3157],                              df_coord_reg[2004:2413]])

Для проверки повторно визуализирую данные и сохраняю полученные координаты в json‑файл.

Код
#Визуально проверяем полученный dataframe  fig = go.Figure(data=go.Scattergeo(lon = df_coord_border['lon'],                                    lat = df_coord_border['lat'],                                    mode = 'markers',                                    marker_color  = df_coord_border['color'],                                    text = df_coord_border['region'] + ' ' + df_coord_border['sequence_number'])) fig.update_layout(         title = 'Субъекты РФ',         geo = dict(             scope='europe',             landcolor = "green",             countrycolor = "black",         ),         width=1500,                                            #Ширина карты         height=750                                              #Высота графика     )  #Сохраняем данные в json df_coord_border[['lon', 'lat']].to_json('data//border.json')

Результат работы кода:

Результат работы

Результат работы

Кто? А главное, зачем?

Помните об эксельке, которую я упоминал в начале? Вот теперь пришло и её время. Создаю новую тетрадку, импортирую библиотеки, читаю xlsx‑файл. Смотрю на данные.

import pandas as pd import numpy as np import json  #Возьмем данные из входного файла df_start_adress = pd.read_excel('data//starting_address.xlsx')  border_coord_df.head()
Считывание данных

Считывание данных

Что же получается? В файле хранятся 6 228 адресов, и, даже взглянув на эту выборку, закрадывается подозрение, что данные не имеют строгого формата. Необходимо удалить из dataframe дубликаты и проверить данные на пропуски:

#Удалим дубликаты df_start_adress = df_start_adress.drop_duplicates()  df_start_adress  #None отсутствуют df_start_adress.info()  #NaN,None и пустота могут быть в виде строки. Проверяем по длине строки df_start_adress[df_start_adress['полный адрес'].str.len() <= 5]
Вывод результатов кода

Вывод результатов кода

К счастью, пропусков не наблюдается, а после удаления дубликатов dataframe сократился на 2 000 строк.

Проанализировав можно выделить несколько проблем:

  1. Нет строгой типизации формата адресов. Это сильно ограничивает библиотеки и технологии, такие как запросы HTTP и бесплатные API сайтов.

Адреса без строгого формата

Адреса без строгого формата
  1. В адресах присутствует подстрока «Адрес из Росреестра:». При проверке таких адресов в «Яндекс.Картах», выдается адрес отдела Росреестра города исходного адреса или пустой результат поиска:

Адреса, в которых содержится подстрока "Адрес из Росреестра:"

Адреса, в которых содержится подстрока «Адрес из Росреестра:»
  1. Дублируются данные внутри ячеек. При проверке в онлайн картах данные не выдаются, или строится маршрут движения по этим адресам, точнее, путь от себя к себе:

Дублируемые данные

Дублируемые данные
  1. Присутствуют лишние данные, которые мешают поиску.

Обработать полученные данные и привести их к одному виду показалось очень трудозатратной задачей. Проверив несколько адресов, я решил использовать сервис «Яндекс Карты». Он показал, что может работать с различными адресами и выдавать корректный результат.

Но обработать адреса все равно необходимо и избавиться хотя бы от некоторых проблем: подстрока с Росреестром и повторяющиеся данные.

Для этого применяю функцию formating_text. В функции создаю список, разбиваю строку на слова и помещаю их в список поочередно. Если данное слово уже существует в списке, то его в список не добавляю. В конце удаляю из итоговой строки «Росреестр»:

def formating_text(text):     old_text = text.split()     new_text = []     for word in old_text:         if word not in new_text:             new_text.append(word)      return ' '.join(new_text).replace('Адрес из Росреестра: ', '')  df_start_adress['formating_adress'] = df_start_adress['полный адрес'].apply(lambda x: formating_text(x))
Результат работы функции

Результат работы функции

Сохраняю полученный результат и перехожу к следующему этапу.

Дёшево и сердито

Самые внимательные читатели могли заметить, что для данного проекта я использовал библиотеку Selenium. Почему именно она? Она обладает преимуществами на фоне остальных библиотек и методов парсинга. Её ближайшие аналоги:

  • API «Яндекс.Карт». Данная система хоть и очень удобна в использовании для этой задачи, но она не бесплатна. А мы договаривались в начале публикации, никаких дополнительных вложений.

Тарифы Яндекса
Тарифы на 15.05.2023 г. Источник

Тарифы на 15.05.2023 г. Источник
  • Http/Https‑запросы. В отличие от системы, описанной выше, для запроса требуется только возможность подключения к нужному сайту. Но с моими данными написание get/ post-запросов — довольно сложная задача. К примеру, запрос https://yandex.ru/maps/213/moscow/house/mokhovaya_ulitsa_11s1 состоит из названий города, улицы и номера дома на транслите. Преобразовать входящие данные в такой формат будет непосильной задачей для меня.

Методом исключения осталась только библиотека Selenium, которая будет симулировать действие человека в браузере на сайте. Это позволит обойти системы защиты Яндекса, воспользоваться их алгоритмами обработки данных и найти координаты объекта.

Сразу скажу, здесь я не буду обучать вас данной библиотеке. Лишь разберу основной алгоритм работы скрипта, покажу некоторые нюансы и возможные методы их решения.

Подключение драйвера. В данном проекте я использовал браузер Google Chrome, и примеры будут для Google. Библиотека Selenium уже имеет в себе драйвер для работы с Google, и для его подключения просто нужно прописать команду webdriver.Chrome(). Для перехода на сайт используйте функцию get.

from selenium import webdriver #Адрес сайта "Яндекс.Карты" url_adress = 'https://yandex.ru/maps'  #Подключение драйвера Google driver = webdriver.Chrome('chromedriver.exe') #Переход на сайт driver.get(url_adress) time.sleep(5)

Поиск адреса. Адрес вписывается в поле формы поиска, код данного элемента «<input class=”input__control_bold” >». Для поиска элемента использовал комбинацию функций WebDriverWait и ExpectedCondition. Selenium будет производить поиск элемента, пока он не будет найден или не кончится время ожидания.

Далее заполняю найденную форму адресом с помощью функции send_keys и запускаю поиск, имитируя нажатие клавиши Enter функцией send_keys(Keys.ENTER).

from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC # Время ожидания delay = 10  # Поиск формы ввода на сайте elem_search_string = WebDriverWait(driver, delay) \     .until(EC.presence_of_element_located(     (By.XPATH, "//input[@class='input__control _bold']")))  # Вписываем данные в форму elem_search_string.send_keys(adress) # Запускаем поиск elem_search_string.send_keys(Keys.ENTER)

Результат поиска. Если все отработало штатно, Яндекс должен выдать географические координаты адреса. С помощью уже знакомой комбинации WebDriverWait и ExpectedCondition записываю координаты в переменную:

# Поиск координат на сайте elem_search_2 = WebDriverWait(driver, delay) \     .until(EC.presence_of_element_located(     (By.XPATH, "//div[@class='toponym-card-title-view__coords-badge']"))) # Запись в переменную координат адреса coord = elem_search_2.text

Но Яндекс — не всемогущ, он не всегда находит однозначный ответ, поэтому на некоторые адреса он предлагает несколько вариантов. Как, например, здесь:

Результаты поиска неоднозначного адреса

Результаты поиска неоднозначного адреса

На случай таких ситуаций я брал первый предложенный вариант. Скорее всего он и будет являться нужным мне адресом:

elem_first_list = WebDriverWait(driver, delay) \     .until(EC.presence_of_element_located(     (By.XPATH, "//div[@class='search-snippet-view__body _type_toponym']"))) elem_first_list.click()

И напоследок очищаю форму записи:

try:     elem_clear = WebDriverWait(driver, 2) \         .until(EC.presence_of_element_located(         (By.XPATH, "//a[@class='small-search-form-view__pin']"))) except:     elem_clear = WebDriverWait(driver, 2) \         .until(EC.presence_of_element_located(         (By.XPATH, "//div[@class='small-search-form-view__icon _type_close']")))

Рекомендую объединить поиск координат на сайте и очистку формы записи на случай, если сервис не найдет никакого результата. Это позволит продолжить работу скрипта и не перезапускать программу.

Повторяю все вышеперечисленные действия ещё 3 000 раз и сохраняю
результат в файл.

Финишная прямая

Осталось дело за малым: рассчитать дистанцию между координатами адресов и кратчайших точек построенной границы. С этим поможет библиотека Geopy.

О Geopy 

Geopy — это сторонняя библиотека Python для определения географического местоположения. Она позволяет разработчикам Python легко находить координаты адресов, городов, стран и достопримечательностей по всему миру, используя сторонние геокодеры и другие источники данных. Ознакомиться с библиотекой можно по ссылке.

Для начала импортирую библиотеки и данные с файлов, которые получил ранее, координаты границы и адреса. Преобразую их в нужный формат для удобства в работе.

#Импортируем библиотеки import pandas as pd from geopy.distance import geodesic as GD  import json from tqdm import tqdm  #Ипортируем данные с координатами with open("data/adress_coord.json", 'r', encoding='utf-8-sig') as ad_cor:     adress_coord_dict = json.load(ad_cor)      border_coord_df = pd.read_json("data//border.json")  #Для удобства словарь с координатами адресов преобразуем в df atress_coord_df = pd.DataFrame(adress_coord_dict.items(), columns=['adress', 'coord'])  # Преобразуем координаты границы в список list_border_coord = list([(row['lat'], row['lon']) for index, row in border_coord_df.iterrows()])

Из библиотеки Geopy меня интересует только одна функция, которая как раз и рассчитает расстояние между двумя координатами — geodesic. Показываю, как она работает на примере:

Использование функции geodesic

Использование функции geodesic

Как видно из примера, в функцию нужно подавать широту и долготу в виде списка, множества, строки или кортежа. Главное, чтобы данные подавались попарно. В конце можно добавить единицу измерения расстояния: километры (km, kilometers), метры (m, meters), мили (mi, miles) и т. д.

Теперь пишу функцию, которая и будет считать минимальное расстояние между точками границы и адресом:

def distance_calculation(start_coord):     list_dist = []     for bord_coord in list_border_coord:          #Для расчета расстояния используем функцию GD([1 координаты точки],[2 координаты точки].[единица измерения расстояния])         dist = GD(start_coord, bord_coord).km          #Добовляем в список результат         list_dist.append(dist)          #Возвращаем минимальную дистанцию из списке     return min(list_dist)  tqdm.pandas() atress_coord_df['dist_to_bor'] = atress_coord_df['coord'].progress_apply(lambda x: distance_calculation(x))

Результат:

Итоговый результат

Итоговый результат

Оценка качества

Для сдачи итоговых результатов нужно их проверить, ведь плохой результат никто не любит. Открываю Google Maps и адреса из первоначальной эксельки, расстояние до границы и линейку. И, как видно из рисунков, результаты корректны, а погрешность — в допустимых нормах.

Проверка результатов

Проверка результатов

Итог

Что я могу сказать по итогу? Задача необычная, интересная и в меру сложная. Попрактиковался с библиотеками Pandas, Selenium, Plotly и посмотрел на новую библиотеку Geopy. Результат работы корректен, а погрешность — в допустимых рамках. Санные пошли дальше в работу.

В общем, задача мне понравилась. Я получил дополнительный опыт и даже некие новые знания, и на этом я заканчиваю пост. Желаю всем удачи!)

P. S. Кстати, чуть не забыл, с кодом вы можете ознакомиться на Github.


ссылка на оригинал статьи https://habr.com/ru/articles/735098/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *