Решаем квартирный вопрос при помощи API Яндекс.Карт

в 12:43, , рубрики: javascript, Maps API, php, xpath, геокодирование, яндекс.карты, метки: , , , ,

В жизни даже самого «махрового» IT-шника порой наступает момент, когда нужно не только вылезти из своей берлоги на улицу, но целиком перенести себя на новое место жительства. Обычный человек в таких случаях вооружается Интернетом и прочёсывает сайты недвижимости в поисках подходящих вариантов, которые отмечаются на карте, выписываются или распечатываются, а затем планомерно прозваниваются. Если наступает конец цикла, а задача ещё не выполнена — goto line 1… А на каком-то этапе человеку это надоедает и он идёт в агенство.

Вот и в моей жизни пришло время для переезда, но проведя несколько дней за такой рутинной деятельностью я вспомнил, что незря ношу бороду есть такой чудесный сервис, как Яндекс.Карты, и у них есть не менее чудесное API. Посидев одно утро и скомбинировав всё с простейшим граббером на PHP и XPath я получил такую вот красочную карту, где разными маркерами можно отмечать объекты (квартиры) по любому из критериев, или просто одним взглядом оценить, какие из них ближе к желаемому месту дислокации (в моём случае это было метро):

Снимок экрана

Сразу скажу, что для меня это первый опыт поиска квартиры и потому я могу не знать о сервисе, делающим что-то подобное — однако после нескольких дней поиска ничего такого на вторичном рынке недвижимости я не нашёл. А нашёл бы — не было бы повода писать этот пост, так что поскорее продолжим, пока кто-нибудь из читательов не вставил своё веское «ня»…

Граббер

Информацию будем брать с сайта «Бюллетеня недвижимости»: у них удобный поиск с кучей фильтров, но без карты, что весьма проблематично для людей вроде меня, которые не знают каждую улицу на память.

На сайте есть возможность просмотреть все результаты поисковой выдачи (не более 300) на одной странице в режиме печати — этим мы и воспользуемся. URL имеет такую форму:

http://www.bn.ru/zap_fl.phtml?print=printall&параметры_поиска

Допустим, мы получили страницу с нужными результатами по фильтру. Как вытащить из неё список квартир? Обычно я эту задачу решаю регулярками, но у этого сайта какая-то особая неструктура HTML, поэтому проще оказалось использовать XPath (W3Schools). С его помощью легко получить список строк в таблице (<tr>), а из них — ячейки с адресом, ценой и прочим, просто перебрав коллекцию DOMNode.

Всё вместе это выглядит так:

// Все объявления.
$all = array();
$baseURL = 'http://www.bn.ru/zap_fl.phtml?print=printall&';
// Текст, который выводит сайт при отсутствии результатов поиска.
$empty = iconv('utf-8', 'cp1251', 'Количество найденных вариантов 0');

// В моей форме есть возможность выбора нескольких районов.
foreach ((array) $_REQUEST['region'] as $region) {
  $url = $baseURL."region$region=$region&";
  // Другие критерии...

  // Этот цикл нужен, чтобы результатов не было больше 300 - таким образом мы 
  // получаем все объявления по нашим критериям, даже если их больше 300.
  foreach (range(0, 10000, 1000) as $price0) {
    $reqURL = $url."price1=$price0&price2=".($price0 + 999);
    // Скачиваем страницу - см. примечание после кода.
    $data = dl($reqURL);

    if (!strpos($data, $empty)) {
      // Результаты есть.
      $offers = parse($data);
      $all = array_merge($all, $offers);
    }

    // Не бомбим сервер запросами.
    usleep(200000);
  }
}

Функция dl() может быть заменена на вызов cURL или простой file_get_contents() — правда, последний у меня не сработал и я использовал свой класс для скачки ресурсов, имитирующий браузер.

Функция parse() для разбора HTML в массив объявлений.

function parse($html) {
  // Сообщаем DOMDocument, что у нас документ в UTF-8 (единственный способ,
  // который сработал у меня). 
  $html = '<?xml encoding="UTF-8">'.iconv('cp1251', 'utf-8', $html);

  $doc = new DOMDocument('1.0', 'utf-8');
  @$doc->loadHTML($html) or die('loadHTML: '.$html);

  // Делаем выборку строк таблицы с объявлениями.
  $xpath = new DOMXPath($doc);
  // На странице несколько таблиц, плюс разные типы строк и ячеек - добавляем
  // предикаты (условия) для выбора только тех, что содержат объявления.
  $nodes = $xpath->query('//table[@class="results"]/tr[th[@class="head_kvart"] or td[@width or @class="tooltip"]]');

  // Готовый массив с квартирами.
  $results = array();
  // В таблице нет колонки с числом комнат (вернее, она не всегда есть) - считаем
  // их сами.
  $roomCount = 1;

  // $nodes - коллекция элементов-строк (tr).
  foreach ($nodes as $row) {
    // Собираем данные из ячеек - площадь, адрес и т.п.
    $cells = array();
    $cell = $row->firstChild;

    while ($cell) {
      $cell->nodeType == XML_ELEMENT_NODE and $cells[] = trim($cell->nodeValue);
      $cell = $cell->nextSibling;
    }

    if (count($cells) == 1) {
      // Строка всего с одной ячейкой - числом комнат для результатов в строках
      // ниже. Запоминаем его.
      $roomCount = (int) reset($cells);
    } else {
      $cells[0] = $roomCount;
      
      // Некоторые объявления имеют colspan на полях с ценой - заполняем остальные
      // данные нулевыми значениями.
      if (count($cells) == 10) {
        array_splice($cells, 6, 1, array(0, '', $cells[6], ''));
      }
      
      // Получаем адрес объявления, чтобы мы могли просмотреть подробности с карты.
      $html = $row->ownerDocument->saveXML($row);
      if (preg_match('~<a href="([^"]+)~u', $html, $match)) {
        array_unshift($cells, 'http://www.bn.ru'.$match[1]);
      } else {
        array_unshift($cells, '');
      }

      // Наконец, присваиваем цепочке ячеек осознанные имена.
      $keys = array('url', 'rooms', 'address', 'floors', 'houseType', 'area', 'areaLiving', 'areaKitchen', 'toilet', 'price', 'conditions', 'seller', 'phone', 'notes');

      $result[] = array_combine($keys, $cells);
    }
  }

  return $result;
}

Карта

Отлично, половина задачи выполнена — массив $all содержит все объявления в удобном для работы формате. Дело за малым — расположить их на карте. Для удобства приведу образец элемента массива:

array(
  'url' => 'http://www.bn.ru/detail/flats/xxxxxx.html?from=search', 
  'rooms' => 1, 
  'address' => '7 Советская ул., xxx', 
  'floors' => '1\5', 
  'houseType' => 'СФ', 
  'area' => '30', 
  'areaLiving' => '18.3', 
  'areaKitchen' => '6', 
  'toilet' => ' ', 
  'price' => '3100', 
  'conditions' => ' ', 
  'seller' => 'xxxxx Недвижимость', 
  'phone' => '(965) xxxxxxx', 
  'notes' => 'ПП свободна ХС торг',
)

Базовый HTML для минимально работающей Яндекс.Карты:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Map grabber</title>

    <script src="//api-maps.yandex.ru/2.0/?load=package.standard&lang=ru-RU" type="text/javascript"></script>
    <script src="//cdnjs.cloudflare.com/ajax/libs/jquery/1.9.0/jquery.min.js"></script>

    <style>
      html, body, .#map { margin: 0; padding: 0; }
      #map { width: 100%; height: 800px; }
    </style>
  </head>
  <body>
    <div id="map"></div>
  </body>
</html>

jQuery для работы Карт не нужен, но он пригодится для раскрашивания маркеров.

Допустим, у нас уже есть полученный список квартир. Его мы сбросим в массивы на JavaScript и всё остальное уже будем делать на нём:

// Массив адресов для расстановки маркеров.
var coords = []
// Массив информации об этих адресах.
var info = []

<?foreach ($all as $offer) {?>
    coords.push('Санкт-Петербург, <?=$offer['address']?>')
    info.push(<?=json_encode($offer, JSON_UNESCAPED_UNICODE)?>)
<?}?>

ymaps.ready(init);

Теперь самое интересное — расставляем маркеры в функции init(). Для этого используется геокодирование и объект MultiGeocoder из примера API Карт; ему мы передаём список адресов (coords), он получает по ним список координат (широта и долгота), по которым мы выставляем на карте маркеры.

function init() {
  // Создаём карту с центром в в Санкт-Петербурге:
  var map = new ymaps.Map('map', {
    center: [59.932666, 30.329596],
    zoom: 13,
    behaviors: ['default', 'scrollZoom'],
  })

  // Добавляем кнопки масштаба и линейку:
  map.controls
    .add('zoomControl', {left: 5, top: 5})
    .add('mapTools', {left: 35, top: 5})

  // Преобразуем адреса в координаты с помощью геокодера Карт.
  (new MultiGeocoder({boundedBy: map.getBounds()}))
    .geocode(coords)
    .then(
      function (res) {
        // Сервер вернул нам результат для всех запрошенных адресов.
        for (var i = 0; i < res.geoObjects.getLength(); i++) {
          var cells = info[i]
          
          // Создаём текст для показа при клике на каркере - код для таблицы 
          // я пропустил, так как там ничего сложного.
          var text = '<p>' + $('<b>').append(
            $('<a>')
              .attr({href: cells.url, target: '_blank'})
              .text(cells.address)
          )[0].outerHTML + '</p>'

          // Геообъект - уже готовый маркер, полученный от сервера.
          var geo = res.geoObjects.get(i)
          // Сохраняем объект маркера для простого поиска позже.
          info[i].geo = geo
          // Заменяем стандартный текст в окошке при клике.
          geo.properties.set('balloonContentBody', text)
        }

        // Ставим все маркеры на нашей карте.
        map.geoObjects.add(res.geoObjects)
      },
      function (err) { alert(err) }
    )
    
  return map
}

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

А как же?..

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

Я решил не изобретать велосипед и сделал поля для ввода кусков кода на JavaScript для проверки условий, которые применяются к каждому маркеру. Если одно из условий совпадает, маркер окрашивается в другой цвет и другие условия не проверяются. Таким образом можно подсветить, например, все однокомнатные квартиры, затем — все остальные, имеющие большую кухню, а затем — те из оставшихся, которые находятся на втором этаже.

Полный код приводить не буду — он тривиален. Приведу только часть для подсветки (типы маркера описаны в API):

// Типы маркеров и их условия вида [ [тип_маркера, код_условия], ... ].
var markers = []

// Поле ввода маркера находится внутри <p data-marker="twirl#redDotIcon">.
// twirl#redDotIcon - тип маркера (preset) для передачи в карту.
$('p[data-marker] input').val(function (i, value) {
  var marker = $(this).parent().attr('data-marker')
  value = $.trim(value)
  value && markers.push([marker, value])
  return value
})

// Перебираем имеющиеся маркеры (см. init()). cells - {price: 123, rooms: 2, ...}.
$.each(info, function (i, cells) {
  var colored = false

  for (var i = 0; i < markers.length && !colored; i++) {
    var item = markers[i]
    var func = new Function('cells', 'return ' + item[1]);

    // Выполняем код для проверки условия - если оно выполняется, красим маркер.
    if (func(cells)) {
      cells.geo.options.set('preset', item[0])
      // Пропускаем оставшиеся типы маркеров.
      colored = true
    }
  }

  // Не одно условие не совпало - красим маркер в умолчательный цвет.
  colored || cells.geo.options.set('preset', 'twirl#blueIcon')
})

Поработав над этим кодом ещё немного можно сделать куда более оптимизированную и удобную систему, но для одного утра, по-моему, совсем неплохо!

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

Автор: ProgerXP

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js