Клуб API Карт

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

nolan23
29 августа 2012, 10:27

в одном проектике была поставлена задача: геокодировать более 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. Кстати, насиловать яндекс тысячами адресов в минуту тоже нехорошо)

 

 

6 комментариев
Подписаться на комментарии к посту
Евгений Белоусов
29 августа 2012, 10:58
function getAddressByPoint($lat, $lng, $format='full') {
    global $db;
   
    $lat = round($lat, 4);
    $lng = round($lng, 4);
   
    $addr = $db->get_single_value($sql="SELECT addr FROM coords WHERE lat='".$db->escape($lng)."' AND lng='".$db->escape($lat)."'");
    if($addr == '') {
        $query = 'http://geocode-maps.yandex.ru/1.x/?geocode='.urlencode($lng.','.$lat).'&format=json&results=1';
        $result = @file_get_contents($query);
        $result = ($result ? json_decode(@file_get_contents($query)) : '');
        if($result && isset($result->response)) {
            if(intval(@$result->response->GeoObjectCollection->metaDataProperty->GeocoderResponseMetaData->results) > 0) {
                $addr = $result->response->GeoObjectCollection->featureMember[0]->GeoObject->metaDataProperty->GeocoderMetaData->text;
                $db->query("INSERT INTO coords VALUES('".$db->escape($lng)."','".$db->escape($lat)."','".$db->escape($addr)."')", FALSE);
            }
        }
    }
    return (string)(@$addr);
}

при большом количестве скрипт отвалится по таймауту

вот тут @file_get_contents($query);

Евгений Белоусов
29 августа 2012, 11:14
знаю

Переносим всю логику на клиент.

Задачу о том что геокодируем и куда складываем делаем через REST( ajax в общем )

Получили ответ, запросили новую задачу, работаем. Повторить.

Запустили на ночь и все очень акуратно отработало.

При больших обьемах ставим этот скриптик и скрытый ифрейм на сайтике и за нас работают пользователи.

Настоящие облачные вычисления!

ботнетом попахивает)))

Здравствуйте!
А могли бы предоставить исходник всего этого проекта, а то особо не силен во всем этом, а задача 1 в 1