[] Парсер 2GIS в семь строчек кода, или почему важно контролировать лимиты запросов на сервер

Наверное любому из тех, кто хоть как-то причастен к области анализа данных хотя-бы раз приходилось сталкиваться с поиском сторонних источников получения этих самых данных. Сегодня я хотел бы поделиться с Вами одним из самых неожиданных для меня мест, где эти данные лежат почти что на поверхности, да еще и в огромных количествах. Знакомьтесь — это 2GIS.

Image


Как ты это сделал?

Итак, первым делом заходим на сайт 2GIS, вводим случайный адрес и открываем режим разработчика, работа с сетью. Нас интересует вкладка XHR(Он же XMLHttpRequest). Данный запрос предоставляет клиенту функциональность для обмена данными между клиентом и сервером. Более подробна его работа описана здесь.

image

Видим, что есть запросы нескольких типов:


  • get — Запрос на получение информации об объекте по его id;
  • items — Запрос на получение списка объектов по строке поиска;
  • markers — Запрос на получение информации о значках и их расположении на карте;
  • count — Запрос на получение ссылок на фотографии с данного места (могу ошибаться);
  • bss — Запрос на построение отдельных полигонов карты (могу ошибаться);
  • poi — Запрос на получение информации об отдельных полигонах на карте.

Нас интересует первые два запроса, а именно — запрос items, и запрос get. Недолго думая, полностью копируем первый, вставляем его в браузерную строку, и получаем тот самый JSON ответ, в котором хранится вся информация по запросу «офис компании 2gis». Делаем однозначный вывод: Если можно напрямую отправлять запросы на сервер и получать от него ответ, то это действие можно автоматизировать. Но, давайте для начала разберем, из чего состоит сам запрос:


items запрос
https://catalog.api.2gis.ru/3.0/items?
viewpoint1=37.28485099218749%2C55.77155201664903
&viewpoint2=37.95501700781249%2C55.73561570631377
&type=street%2Cadm_div.city%2Ccrossroad%2Cadm_div.settlement%2Cstation%2Cbuilding%2Cadm_div.district%2Croad%2Cadm_div.division%2Cadm_div.region%2Cadm_div.living_area%2Cattraction%2Cadm_div.place%2Cadm_div.district_area%2Cbranch%2Cparking%2Cgate%2Croute
&page=1
&page_size=12
&q=офис%20компании%202gis
&locale=ru_RU
&fields=request_type%2Citems.adm_div%2Citems.context%2Citems.attribute_groups%2Citems.contact_groups%2Citems.flags%2Citems.address%2Citems.rubrics%2Citems.name_ex%2Citems.point%2Citems.geometry.centroid%2Citems.region_id%2Citems.segment_id%2Citems.external_content%2Citems.org%2Citems.group%2Citems.schedule%2Citems.timezone_offset%2Citems.ads.options%2Citems.stat%2Citems.reviews%2Citems.purpose%2Csearch_type%2Ccontext_rubrics%2Csearch_attributes%2Cwidgets%2Cfilters
&stat%5Bsid%5D=91e1c495-9e55-4ca9-8712-e15073071f6e
&stat%5Buser%5D=a8e546d0-291f-4778-bc72-1f84d55dcdfc
&key=ruoedw9225
&r=1831242903

Перед нами самый обычный GET запрос. Для удобства я предварительно разделил его на части. Взглянем на него и разберемся в деталях:


  • viewpoint1, viewpoint2 — это непосредственные координаты нашего окна карты;
  • type — тип запроса. Изменяя этот параметр можно осуществлять поиск, к примеру, только только по городам, либо только по «жилым зонам», либо же устроить поиск везде, как в нашем примере.
  • page, page_size — номер страницы и количество отображаемых запросов на странице. Бывает так, что по одному запросу может быть несколько ответов. К примеру, на запрос: «банкоматы». Здесь данный параметр очень пригодится.
  • locale — Выбранная локаль для запроса.
  • q — поле нашего запроса. Как видим, пробелы заменены знаками %20, запятые — на знак %2С. При составлении запроса необходимо будет это учитывать.
  • fields — поля возвращаемых значений. В данном поле, по сути, хранится вся информация, которую мы хотим получить в нашем запросе.
  • stat, key, r — поля идентификации пользователя.

Попробуем скорректировать наш запрос и посмотреть, какие поля имеют значения, а какие — нет. Забегая вперед скажу, что запрос прекрасно будет работать и без viewpoint, page и прочих подобных. А вот если изменить поля идентификации — непременно получим error 400. Значит по этим ключам и id любая информация должна быть нам доступна.

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

Напишем наш Python скрипт, который будет получать JSON ответ с информацией об адресе. Для этого импортируем модуль requests, добавим в headers заголовки браузера ноутбука, предобработаем адрес, и просто отправим запрос на сервер.


код Python для получения информации по адресу
import requests 

headers = {'user-agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:41.0) Gecko/20100101 Firefox/41.0'}

#Создаем функцию получения данных по адресу
def getDataFromAddress(address):
    address = '%20'.join('%2C'.join(address.split(',')).split(' ')) #Получаем адрес, заменяем запятые на %2C, а пробелы на %20
    link = 'https://catalog.api.2gis.ru/3.0/items?type=street%2Cadm_div.city%2Ccrossroad%2Cadm_div.settlement%2Cstation%2Cbuilding%2Cadm_div.district%2Croad%2Cadm_div.division%2Cadm_div.region%2Cadm_div.living_area%2Cattraction%2Cadm_div.place%2Cadm_div.district_area%2Cbranch%2Cparking%2Cgate%2Croute&page=1&page_size=12&q='+address+'&locale=ru_RU&fields=request_type%2Citems.adm_div%2Citems.context%2Citems.attribute_groups%2Citems.contact_groups%2Citems.flags%2Citems.address%2Citems.rubrics%2Citems.name_ex%2Citems.point%2Citems.geometry.centroid%2Citems.region_id%2Citems.segment_id%2Citems.external_content%2Citems.org%2Citems.group%2Citems.schedule%2Citems.timezone_offset%2Citems.ads.options%2Citems.stat%2Citems.reviews%2Citems.purpose%2Csearch_type%2Ccontext_rubrics%2Csearch_attributes%2Cwidgets%2Cfilters&stat%5Bsid%5D=91e1c495-9e55-4ca9-8712-e15073071f6e&stat%5Buser%5D=a8e546d0-291f-4778-bc72-1f84d55dcdfc&key=ruoedw9225&r=1831242903' # передаем предобработанный адрес в запрос
    answer = requests.get(link, headers=headers) # делаем запрос
    return json.loads(answer.content.decode('utf-8')) # возвращаем ответ в виде json

Вот и все! 7 строчек кода, и поиск по адресу готов. Введя город, улицу, и дом, наша функция вернет JSON с достаточно неплохой информацией об объекте: его id, широту, долготу, тип, район города, и так далее. И это уже впечатляет!


Больше, больше данных!

Еще больше информации можно получить по запросу get. Правда вместо адреса он использует id постройки, но мы без труда получаем его из предыдущего запроса:


код Python для получения информации по строению
def getDataFromBuildings(building_id):
    link = 'https://catalog.api.2gis.ru/2.0/catalog/branch/list?building_id='+str(building_id)+'&locale=ru_RU&fields=items.region_id%2Citems.segment_id%2Citems.reviews%2Citems.adm_div%2Citems.contact_groups%2Citems.flags%2Citems.address%2Citems.rubrics%2Citems.name_ex%2Citems.point%2Citems.external_content%2Citems.schedule%2Citems.timezone_offset%2Citems.org%2Citems.stat%2Citems.ads.options%2Citems.attribute_groups%2Crequest_type%2Csearch_attributes&stat%5Bsid%5D=91e1c495-9e55-4ca9-8712-e15073071f6e&stat%5Buser%5D=c8109e98-e546-455d-b6ed-fcfd7cb4ffe0&key=ruoedw9225&r=3862084826' #передаем id постройки в запрос
    answer = requests.get(link ,headers=headers) #делаем запрос
    return json.loads(answer.content.decode('utf-8'))# возвращаем ответ в виде json

building_id = getDataFromAddress('Арма,Нижний Сусальный переулок, 5 ст16,Басманный район, Москва')['result']['items'][0]['address']['building_id'] #получаем id постройки по данному адресу
getDataFromBuildings(building_id) #получаем json ответ с организациями в здании

Еще 7 строчек кода, и теперь мы имеем доступ не только к данным о строении, но также и об организациях в этом здании. А именно — время работы, способы оплаты, тип организации, и даже номера телефонов.


А теперь распаралеллить!

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


код Python для получения информации по адресу, многопоточный
from multiprocessing import Pool 
from multiprocessing.dummy import Pool as ThreadPool

pool = ThreadPool(32) #создаем пул, указываем количество потоков

def getFullData(address):
    addressData = getDataFromAddress(address) #получаем данные об адресе
    building_id = addressData['result']['items'][0]['address']['building_id'] #получаем id здания
    buildingData = getDataFromBuildings(building_id) #получаем данные об организациях в здании
    return buildingData, addressData #возвращаем кортеж с данными об адресе и организациях по этому адресу

addressAr = ['Арма,Нижний Сусальный переулок, 5 ст16, Басманный район, Москва', 'Щербанёва, 25, Омск'] #массив адресов
fullData = pool.map(getFullData, addressAr) #Применяем нашу функцию к массиву с адресами

#Закрываем пул
pool.close()
pool.join()

Таким образом 2GIS позволяет получать любые данные о любых организациях достаточно быстро и просто. При этом не нужно регистрироваться, оставлять заявку или же изучать API.


Итог

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

Решается это вроде бы тоже не так сложно — необходимо лишь наладить лимит запросов на сервер (наврядли человек с одним уникальным stat user & key сможет отправлять больше чем 10 запросов в секунду) и никакой, даже самый хитрый охотник за данными, не сможет их украсть.

P.S — такие возможности были открыты в конце декабря, после чего я сразу отписался в техподдержку 2GIS (при чем ни один раз). На дворе 15 января, ответа до сих пор не поступило, из чего можно сделать вывод что «Это не баг, а фича!». Надеюсь, так оно и задумано. Спасибо!

© Habrahabr.ru