
Привет, Хабр!
В этой статье я хотел бы поделиться опытом решения маленькой проблемы с большим количеством адресов. Если вы когда-либо работали с 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.
Что в итоге выбрать?
Проще всего сделать выбор составив небольшую таблицу:

В итоге мой выбор пал на 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)
Анализ результатов
В итоге я прошел путь от медленно работающих онлайн приложений до сервисов пакетного геокодирования. Выбор провайдера геосервисов полностью зависит от задач, которые стоят перед вами. У меня регулярно появляются запросы на обработку большого количества адресов и подход описанный в статье помог существенно сократить время.
Надеюсь, что данная статья будет полезна и конечно я открыт к комментариям и дополнениям!
