![](https://habrastorage.org/webt/6b/g3/oo/6bg3ooc_jcrhty63bg_33x8inta.jpeg)
Привет, Хабр!
В этой статье я хотел бы поделиться опытом решения маленькой проблемы с большим количеством адресов. Если вы когда-либо работали с API геокодирования или пользовались онлайн инструментами, то думаю вы разделяете мою боль ожидания результата в течение нескольких часов, а то и больше.
Речь идет не о сложных алгоритмах оптимизации, а об использовании сервиса пакетного геокодирования, который принимает на вход список адресов и возвращает файл с результатами. Тем самым можно сократить время обработки с нескольких часов до минут.
Обо всем по порядку:
Предыстория
Пакетное геокодирование
Выбор провайдера пакетного геокодера
Гайд по работе с сервисом на Python
Полное описание класса Batch
Анализ результатов
Предыстория
Поступила задача – “Привязать к координатам 24 тысячи адресов”. В голову пришло только два варианта решения проблемы:
- Веб-приложение для геокодирования, которым пользовался еще в университете;
- Написать скрипт на основе REST API геокодера.
В первом случае оказалось, что веб-приложение после обработки тысячи адресов падает. Распределять датасет между коллегами – идея, от которой сразу отказались.
Следовательно нужно использовать REST API геокодера для написания собственного скрипта, с сохранением результатов (это не совсем легальный способ и нужно читать условия использования сервиса). Возникает новая проблема – одно дело когда мы используем адресный поиск в приложении и сразу получаем результат, но когда стоит задача обработать более десяти тысяч адресов с сохранением, работа скрипта сильно затягивается. Можно подождать час или два, но миллион адресов придется геокодировать “необъятное время”, поэтому нужно искать другое решение и оно есть!
Крупные провайдеры геолокационных сервисов, помимо обычного сервиса геокодирования, предлагают пакетный геокодер (Batch Geocoder), как раз для того чтобы за один запрос выполнить обработку большого количества адресов.
Пакетное геокодирование
Название сервиса говорит само за себя – у нас имеется пакет (например csv файл со списком адресов в виде таблицы), который мы загружаем на сервер, и он делает всю работу за нас.
Процесс пошагово выглядит так:
- Подготовка датасета, чтобы сервис принял его без ошибок;
- Настройка параметров результата работы (выбор колонок, разделитель ...);
- Загрузка файла в облако;
- Ожидание завершения обработки;
- Скачивание готового файла.
Благодаря облачным вычислительным мощностям, то что делается самописным скриптом за 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.
Что в итоге выбрать?
Проще всего сделать выбор составив небольшую таблицу:
![](https://habrastorage.org/webt/kg/xy/b1/kgxyb1kmzemqxqicwajfi7hggva.jpeg)
В итоге мой выбор пал на 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)
Анализ результатов
В итоге я прошел путь от медленно работающих онлайн приложений до сервисов пакетного геокодирования. Выбор провайдера геосервисов полностью зависит от задач, которые стоят перед вами. У меня регулярно появляются запросы на обработку большого количества адресов и подход описанный в статье помог существенно сократить время.
Надеюсь, что данная статья будет полезна и конечно я открыт к комментариям и дополнениям!