Блог API Яндекс.Карт

Серверная реализация мультигеокодирования

29 августа 2012, 15:03

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


Ранее мы уже публиковали класс множественного геокодирования на клиенте и наш вариант получения координат для списка адресов с использованием PHP и MySQL.


Спасибо автору за еще одну реализацию!

Серверная реализация мультигеокодирования

в одном проектике была поставлена задача: геокодировать более 1000 адресов из загружаемого excel-файла с адресами.

алгоритм такой:
1. загрузка excel-файла и его парсинг в csv
2. вывод окна с формой ajax, которая опрашивает сервер каждую минуту до окончания геокодирования
3. если юзер обновляет окно, сервер проверяет наличие "недокодированного" файла с данными,и, если такой есть, показывает форму из п.2
4. по окончании геокодирования выводится список ошибок и файл с данными удаляется.
вот функция собственно геокодирования:
$result = array('html'=>'');
// проверяем существовование файла с данными для геокодирования
if (file_exists($dataraw)) {
// инициализируем memcache
        $memcache = new Memcache;
        $memcache->pconnect('127.0.0.1', 11211);
// грузим данные для геокодирования
        $data = file($dataraw, FILE_IGNORE_NEW_LINES);
// устанавливаем таймаут работы скрипта
        $endtime = time() + 60;
        $storage = '';
        $count = 0;
        $geocoded = 0;
        $errordata = '';
// инициализируем curl.
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_HEADER, 0);
        curl_setopt($ch, CURLOPT_TIMEOUT, 10);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// цикл геокодирования заканчивается по таймаут либо когда массив с адресами кончится
        while ((time() < $endtime) && sizeof($data)) {
            $count ++;
            $coords = false;
            $string = array_shift($data);
            $row = str_getcsv($string);
            array_walk($row, create_function('&$val', '$val = trim($val);'));
// тут превращаем часть столбцов массива данных в строку адреса
            $addr = implode(',', array_slice($row, 4, 4));
// хэш для хранения и поиска в memcache
            $hash = md5($addr);
// проверяем наличие уже отгеокодированного адреса в memcache
            $coords =  $memcache->get($hash);
// обраращение к геокодеру
            if ($coords === false)
            {
                curl_setopt($ch, CURLOPT_URL, 'http://geocode-maps.yandex.ru/1.x/?kind=house'.
                    '&geocode='.urlencode('Россия,+'.implode(',+', array_slice($row, 4, 4))));
                $content = curl_exec($ch);
                if (curl_error($ch)) {
                    break;
                }
                $xml = simplexml_load_file($content);
// тут коряво, конечно, но проверяем точность геокодирования. нас вполне устроит точность exact, number, near и street
                if ($xml->GeoObjectCollection->metaDataProperty->GeocoderResponseMetaData->found !=0 &&
                    in_array($xml->GeoObjectCollection->featureMember[0]->GeoObject->metaDataProperty->GeocoderMetaData->precision, array('exact', 'number','near', 'street')))
                {
// сохраняем в memcache если геокодирование прошло удачно
                    $coords = sprintf($xml->GeoObjectCollection->featureMember[0]->GeoObject->Point->pos[0]);
                    $memcache->set($hash, $coords, MEMCACHE_COMPRESSED, strtotime('+30 days'));
                } else {
// сохраняем false, если геокодирование прошло неудачно.
                    $memcache->set($hash, NULL, MEMCACHE_COMPRESSED, strtotime('+1 days'));
                }
                $geocoded ++;
            };
            if ($coords) {
// сохраняем результат 
                list($lng, $lat) = explode(' ', $coords);
                $storage .= ($storage ? '|' : '') . implode(":", array(
                    $row[0],
                    $lat,
                    $lng,
                    isset($row[9]) ? intval($row[9]) : 0,
                    $row[0].". ".$row[1].' '.$row[2].' '.$row[3].'<br />'. implode(',', array_slice($row, 4, 5))));
            } else {
// или сохраняем ошибку
                $errordata .= "<tr><td>{$row[0]}</td><td>$addr</td></tr>";
            };
        };
// сохраняем ошибки в файл
        file_put_contents($error, $errordata."<tr colspan='2'>Обработано $count, геокодировано $geocoded<td></tr>");
// если отработан весь файл с данными - удаляем его и выводим ошибки
        if (sizeof($data) == 0) {
            @unlink($dataraw);
            $result['done'] = true;
            file_put_contents($error, "<tr><td colspan='2'>Обработка завершена<td></tr>" .
                "<tr><td colspan='2'><a href='/mapexcel/'>Нажмите для переходы к карте</a><td></tr>",  FILE_APPEND);
        } else {
            file_put_contents($error, "<tr><td colspan='2'>Обработано $count. Осталось ".sizeof($data)."<td></tr>",  FILE_APPEND);
        };
        $result['html'] = file_exists($error) ? file_get_contents($error) : '<tr><td colspan="2">Ошибок не найдено</td></tr>';
// сохраняем файл с обработанными данными
        file_put_contents($datafile, $storage);
// отправляем ответ json
        header('Content-Type: application/json');
        echo json_encode($result);
        exit;
    }

клиентская часть
if (file_exists($dataraw))
{
    echo '<h1>Обработка загруженного файла</h1>';
    echo '<div id="result">Обрабатывается...</div>'.PHP_EOL;
    echo '<script type="text/javascript">
    $().ready(function() {
        var load = function() {
                $.get("?action=work", function(response) {
                    $("#result").html(response.html);
                    if (!response.done) {
                        window.setTimeout(load, 1000);
                    }
                });
            };
        window.setTimeout(load, 1000);
    })
    </script>
    </body>
    </html>';
    exit;
};

пояснения:
1. cron не использую, так как проект мелкий, да и потом вспомнить, что там было в кроне - сложно
2. почему без mysql? нет смысла морочиться из-за такого мелкого объема. данные обновляются ежедневно, а вот адресов будет 20-50 новых в день
3. memcache? вместо него можно использовать что угодно - mysql, файловый кэш и т.д. (сложнее, медленнее)
4. почему curl? сначала исползовал file_get_contents, но после большого числа обращений к геокодеру, яндекс начинает отдавать результаты с большой задержкой и скрипт вылетает по таймауту. Можно поставить set_time_limit(0), но все расвно вылетает из-за Nginx. в общем, curl хорош тем, что легко настраивается timeout. Кстати, насиловать яндекс тысячами адресов в минуту тоже нехорошо)