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

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

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

Обо всем по порядку:


Предыстория


Поступила задача – “Привязать к координатам 24 тысячи адресов”. В голову пришло только два варианта решения проблемы:

  1. Веб-приложение для геокодирования, которым пользовался еще в университете;
  2. Написать скрипт на основе REST API геокодера.

В первом случае оказалось, что веб-приложение после обработки тысячи адресов падает. Распределять датасет между коллегами – идея, от которой сразу отказались.

Следовательно нужно использовать REST API геокодера для написания собственного скрипта, с сохранением результатов (это не совсем легальный способ и нужно читать условия использования сервиса). Возникает новая проблема – одно дело когда мы используем адресный поиск в приложении и сразу получаем результат, но когда стоит задача обработать более десяти тысяч адресов с сохранением, работа скрипта сильно затягивается. Можно подождать час или два, но миллион адресов придется геокодировать “необъятное время”, поэтому нужно искать другое решение и оно есть!

Крупные провайдеры геолокационных сервисов, помимо обычного сервиса геокодирования, предла��ают пакетный геокодер (Batch Geocoder), как раз для того чтобы за один запрос выполнить обработку большого количества адресов.

Пакетное геокодирование


Название сервиса говорит само за себя – у нас имеется пакет (например csv файл со списком адресов в виде таблицы), который мы загружаем на сервер, и он делает всю работу за нас.

Процесс пошагово выглядит так:

  1. Подготовка датасета, чтобы сервис принял его без ошибок;
  2. Настройка параметров результата работы (выбор колонок, разделитель ...);
  3. Загрузка файла в облако;
  4. Ожидание завершения обработки;
  5. Скачивание готового файла.

Благодаря облачным вычислительным мощностям, то что делается самописным скриптом за 1 час, выполняется за 1 минуту.

Следующий шаг – это выбор компании с наиболее лояльными условиями использования пакетного геокодера. Во-первых, далеко не у всех такой сервис есть, другие позволяют протестировать работу сервиса с серьезным ограничением. Также если у вас очень большие объемы, нужно обратить внимание на стоимость дополнительных транзакций, в случае превышения лимита бесплатного пакета.

Выбор провайдера сервиса пакетного геокодирования


На мировом рынке геолокационных сервисов лидирующие позиции занимают:

  • Google Maps;
  • HERE Technologies;
  • MapBox;
  • TomTom;
  • ESRI.

Конечно нельзя забывать о Yandex Technologies, у которого довольно сильные позиции в России.

Я взял за основу следующие параметры для выбора провайдера:

  • Количество запросов к сервису геокодирования в месяц бесплатно;
  • Ограничение на количество транзакций в день;
  • Наличие сервиса пакетного геокодирования;
  • Возможность использования пакетного геокодера в бесплатном плане.

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

Google Maps


Для того чтобы начать работу с геосервисами Google, первым делом требуется добавить в свой аккаунт информацию о банковской карте. Ежемесячный лимит 200 виртуальных долларов, далее идет оплата дополнительных транзакций с привязанной карты. В рамках этого лимита можно пользоваться различными сервисами, но транзакции у каждого считаются по-разному. Например одна тысяча запросов на геокодирование будет стоить 5$, но сервис построения маршрутов стоит в два раза дороже. Более детально можно ознакомиться на сайте, нас интересует лишь сервис геокодирования.

Если 200$ в месяц, то несложно посчитать бесплатное количество транзакций – 40 000 (сервис геокодирования). Пакетный геокодер среди сервисов отсутствует. Это значит, что придется писать собственный скрипт и результат будет примерно 1 адрес в секунду, а это шесть часов для 24 тысяч адресов. Чтобы ускорить процесс, можно попробовать запустить скрипт на платформе Google Cloud APIs, но я решил поискать альтернативные решения. Ограничений по количеству транзакций в день нет, поэтому все сорок тысяч можно потратить за один раз.

HERE Technologies


В прошлом Nokia Maps и в еще более глубоком прошлом Navteq, дает каждый месяц 250 тысяч транзакций бесплатно. Аналогично Google Maps это количество распространяется на все сервисы и каждый считается по-разному. При использовании бесплатного пакета, банковскую карту привязывать не надо. Если вы превысили лимит, то за каждую дополнительную тысячу транзакций необходимо заплатить 1$.

Важно наличие пакетного геокодера как отдельного сервиса, который входит в бесплатный план. Транзакции в нем учитываются по той же модели что и в обычном, то есть каждый адрес в файле, пакетный геокодер будет воспринимать за одну транзакцию.
По названию статьи понятно, что я использовал пакетный геокодер HERE, так как можно потратить все транзакции на геокодер и выполнить 250 000 операций геокодирования в месяц. Но это не единственная опция, поэтому смотрим, что есть у других компаний.

MapBox


При использовании геокодера MapBox, доступно 100 тысяч транзакций в месяц. Компания придерживается той же модели монетизации с оплатой дополнительных транзакций. Только есть интересная опция для “оптовиков” – чем больше у вас транзакций, тем меньше они стоят (конечно есть лимит снижения цены). Например от 100 тыс. до 500 тыс. дополнительная тысяча запросов будет стоить 0.75$, от 500 тыс. до 1 млн – 0.60$ и тд., подробнее можно ознакомиться на сайте. К сожалению пакетный геокодер доступен только в платном аккаунте.

TomTom


Платформа дает возможность выполнить 2500 транзакций в день, примерно 75 000 в месяц. При тестировании и разработке, ограничение по дням выглядит не очень привлекательно по сравнению с конкурентами, но оплата дополнительных транзакций наиболее гибкая. Имеется 8 вариантов оплаты дополнительной тысячи запросов и цена снижается с 0.5$ до 0.42$.

Среди сервисов есть пакетный геокодер с возможностью обработать до 10 тысяч адресов за один запрос (однако надо учитывать ограничение в день).

Yandex Technologies


Модель с ограничением транзакций по дням и у Yandex, но более лояльная 25 тысяч запросов. Если умножить это число на количество дней в месяце, то получиться внушительная цифра – 750 тысяч. На сайте представлены цены за дополнительную тысячу транзакций в рублях варьируются от 120 руб. до 11 руб.

Пакетный геокодер как сервис не представлен, поэтому добиться какой-то оптимизации не получится.

ESRI


Очень заманчивый бесплатный план с 1 миллионом транзакций в месяц. Также компания начисляет каждому аккаунту 50 кредитов (примерный эквивалент 5$). Стоит отметить, что это самый лояльный план по использованию геолокационных сервисов. Также имеется сервис пакетного геокодирования, но воспользоваться им можно только при наличии корпоративного аккаунта на платформе ArcGIS Online.

Что в итоге выбрать?


Проще всего сделать выбор составив небольшую таблицу:



В итоге мой выбор пал на HERE так как для решения моей задачи это оптимальный вариант. Конечно, я сделал далеко не полный анализ, в идеале нужно прогнать свой датасет через все геокодеры для оценки качества. Плюс если у вас несколько миллионов адресов, стоит задуматься о платном пакете и тогда нужно брать во внимание стоимость доп. транзакций.

Цель статьи не сравнение компаний, а решение проблемы оптимизации геокодирования большого объема адресов. Я лишь показал ход своих мыслей при выборе провайдера сервиса.

Гайд по работе с сервисом на Python


Для начала необходимо создать аккаунт на портале для разработчиков и сгенерировать в разделе проектов REST API KEY.

Теперь можно работать с платформой. Я опишу лишь часть функционала, которым располагает пакетный геокодер HERE: загрузка данных, проверка статуса, сохранение результатов.

Итак, начнем с импорта необходимых библиотек:

import requests
import json
import time
import zipfile
import io
from bs4 import BeautifulSoup

Далее если не возникло ошибок, создаем класс:

class Batch:

    SERVICE_URL = "https://batch.geocoder.ls.hereapi.com/6.2/jobs"
    jobId = None

    def __init__(self, apikey="your_api_key"):
        self.apikey = apikey

То есть при инициализации классу необходимо передать собственный ключ для REST API.
Переменная SERVICE_URL – это базовый URL для работы с сервисом пакетного геокодирования.
И в jobId будет храниться идентификатор текущей работы геокодера.

Важный пункт – правильная структура данных при запросе. Файл должен содержать две обязательные колонки: recId и searchText. В противном случае сервис вернет ответ с информацией об ошибке при загрузке.

Вот пример датасета:

   recId; searchText
   1; Санкт-Петербург, ул. Коллонтай, 6
   2; Москва, Алкон 1, Ленинградский пр-т., 72
   3; 425 W Randolph St Chicago IL 60606
   4; Румыния, DJ106 20-30, Sibiu 557260
   5; 200 S Mathilda Ave Sunnyvale CA 94086
  

Функция для загрузки файла в облако:

def start(self, filename, indelim=";", outdelim=";"):
        
        file = open(filename, 'rb')

        params = {
            "action": "run",
            "apiKey": self.apikey,
            "politicalview":"RUS",
            "gen": 9,
            "maxresults": "1",
            "header": "true",
            "indelim": indelim,
            "outdelim": outdelim,
            "outcols": "displayLatitude,displayLongitude,locationLabel,houseNumber,street,district,city,postalCode,county,state,country",
            "outputcombined": "true",
        }

        response = requests.post(self.SERVICE_URL, params=params, data=file)
        self.__stats (response)
        file.close()


Все достаточно просто, открываем на чтение файл со списоком адресов, формируем словарь параметров GET запроса. Некоторые параметры стоит объяснить:

  • “action”: “run” – старт обработки адресов;
  • “politicalView”: “RUS” – настройка геокодера на Россию. Без этой настройки спорные территории могут геокодироваться неправильно (например Крым или Курильские острова);
  • “gen”: 9 – версия геокодера (использовал рекомендованную в документации);
  • “maxresults”: 1 – для каждой операции геокодирования будем получать один наиболее релевантный результат;
  • “header”: true – наличие названий колонок в исходном файле для геокодирования;
  • “indelim”: “;” – тип разделителя файла, который загружаем;
  • “outdelim”: “;” – тип разделителя выходного файла;
  • “outcols”: “” – список колонок, которые должны быть в результирующем файле;
  • “outcombined”: true – ошибочные и успешные операции геокодирования буду объединены в один файл в соответствии с порядком геокодирования адресов.

Далее просто посылаем запрос с помощью библиотеки requests и выводим статистику. Конечно, нужно закрыть файл в конце функции. Функция __stats парсит ответ сервера, где содержится Id запущенной работы, а также выводит общую информацию об операции.

Следующий шаг – проверка статуса работы. Запрос формируется аналогичным образом, только необходимо передать Id операции. Параметр action должен содержать значение “status”. Функция __stats выводит в консоль полную статистику для оценки времени завершения работы геокодера.

    def status (self, jobId = None):

        if jobId is not None:
            self.jobId = jobId
        
        statusUrl = self.SERVICE_URL + "/" + self.jobId
        
        params = {
            "action": "status",
            "apiKey": self.apikey,
        }
        
        response = requests.get(statusUrl, params=params)
        self.__stats (response)

Одна из самых главных функций – сохранение результата. Для удобства работы лучше сразу разархивировать файл, который приходит с сервера. Запрос на сохранение файла идентичен проверке статуса просто в конце добавляем /result.

    def result (self, jobId = None):

        if jobId is not None:
            self.jobId = jobId
        
        print("Requesting result data ...")
        
        resultUrl = self.SERVICE_URL + "/" + self.jobId + "/result"
        
        params = {
            "apiKey": self.apikey
        }
        
        response = requests.get(resultUrl, params=params, stream=True)
        
        if (response.ok):    
            zipResult = zipfile.ZipFile(io.BytesIO(response.content))
            zipResult.extractall()
            print("File saved successfully")
        
        else:
            print("Error")
            print(response.text)

Финальная функция для парсинга ответа сервиса. Также ее задача состоит в том, чтобы сохранить идентификатор текущей задачи геокодирования:

    def __stats (self, response):
        if (response.ok):
            parsedXMLResponse = BeautifulSoup(response.text, "lxml")

            self.jobId = parsedXMLResponse.find('requestid').get_text()
            
            for stat in parsedXMLResponse.find('response').findChildren():
                if(len(stat.findChildren()) == 0):
                    print("{name}: {data}".format(name=stat.name, data=stat.get_text()))

        else:
            print(response.text)

Для тестирования работы достаточно запустить Python интерпретатор в папке со скриптом. Класс Batch находится в файле geocoder.py:

>>> from geocoder import Batch
>>> service = Batch(apikey="Ваш ключ для REST API")
>>> service.start("big_data_addresses.csv", indelim=";", outdelim=";")

requestid: "Будет указан Id работы"
status: accepted
totalcount: 0
validcount: 0
invalidcount: 0
processedcount: 0
pendingcount: 0
successcount: 0
errorcount: 0


Отлично работа началась. Проверим статус:

>>> service.status()

requestid: "Будет указан Id работы"
status: completed
jobstarted: 2020-04-27T10:09:58.000Z
jobfinished: 2020-04-27T10:17:18.000Z
totalcount: 249999
validcount: 249999
invalidcount: 0
processedcount: 249999
pendingcount: 0
successcount: 249978
errorcount: 21

Мы видим что обработка датасета завершена. Всего за семь минут удалось прогеокодировать 250 тысяч адресов (без учета ошибок — errorcount). Осталось сохранить результаты:

>>> service.result()
Requesting result data ...
File saved successfully

Полное описание класса Batch


Я думаю, что не помешает добавить скрипт полностью:

import requests
import json
import time
import zipfile
import io
from bs4 import BeautifulSoup

class Batch:

    SERVICE_URL = "https://batch.geocoder.ls.hereapi.com/6.2/jobs"
    jobId = None

    def __init__(self, apikey="Ваш ключ для REST API "):
        self.apikey = apikey
        
            
    def start(self, filename, indelim=";", outdelim=";"):
        
        file = open(filename, 'rb')

        params = {
            "action": "run",
            "apiKey": self.apikey,
            "politicalview":"RUS",
            "gen": 9,
            "maxresults": "1",
            "header": "true",
            "indelim": indelim,
            "outdelim": outdelim,
            "outcols": "displayLatitude,displayLongitude,locationLabel,houseNumber,street,district,city,postalCode,county,state,country",
            "outputcombined": "true",
        }

        response = requests.post(self.SERVICE_URL, params=params, data=file)
        self.__stats (response)
        file.close()
    

    def status (self, jobId = None):

        if jobId is not None:
            self.jobId = jobId
        
        statusUrl = self.SERVICE_URL + "/" + self.jobId
        
        params = {
            "action": "status",
            "apiKey": self.apikey,
        }
        
        response = requests.get(statusUrl, params=params)
        self.__stats (response)
        

    def result (self, jobId = None):

        if jobId is not None:
            self.jobId = jobId
        
        print("Requesting result data ...")
        
        resultUrl = self.SERVICE_URL + "/" + self.jobId + "/result"
        
        params = {
            "apiKey": self.apikey
        }
        
        response = requests.get(resultUrl, params=params, stream=True)
        
        if (response.ok):    
            zipResult = zipfile.ZipFile(io.BytesIO(response.content))
            zipResult.extractall()
            print("File saved successfully")
        
        else:
            print("Error")
            print(response.text)
    

    
    def __stats (self, response):
        if (response.ok):
            parsedXMLResponse = BeautifulSoup(response.text, "lxml")

            self.jobId = parsedXMLResponse.find('requestid').get_text()
            
            for stat in parsedXMLResponse.find('response').findChildren():
                if(len(stat.findChildren()) == 0):
                    print("{name}: {data}".format(name=stat.name, data=stat.get_text()))

        else:
            print(response.text)

Анализ результатов


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

Надеюсь, что данная статья будет полезна и конечно я открыт к комментариям и дополнениям!