Мониторинг активности групп VK. Обрабатываем данные на VKScript

Источник: habrahabr
isxam

Столкнулся с задачей мониторинга активности пользователей всем известной социальной сети. Передо мной стояла задача собирать данные о количестве пользователей, находящихся онлайн в определенной группе или сообществе.

Инструменты

Поскольку сам я занимаюсь веб-разработкой, то инструменты мной использовались такие

  • PHP 5 (Zend Framework)
  • Vk API
  • Cron

Объясню свой выбор - Vk API - дело в том, что получить количество пользователей онлайн можно и без API, а спарсив страницу поиска по пользователям с фильтром сообщества и метки онлайн, однако я предпочел не возится с авторизацией и разбором тегов, а применить программный интерфейс.

Архитектура

Реализацию условно можно разделить на 2 части. Первая - скрипт, который на id группы находит количество пользователей онлайн и записывает его в БД. Вторая - админка, позволяющая добавлять новый группы для мониторинга и просматривать статистику по уже добавленным группам.
Что бы статистика была актуальна, необходимо как можно чаще мониторить состояние группы в текущий момент времени. Скрипт стоит повесить в Cron, пусть он у нас вызывается каждых 5 минут.

Обзор Vk API

Если с админкой все более-менее понятно, то вот со скриптом сбора статистики не совсем. Ознакомившись с методами, предоставляемыми API, прихожу к первому решению.

Первое решение (неверное)

С помощью методов groups.getMembersusers.get получаем список участников группы и их статус - онлайн или оффлайн. Далее считаем сколько пользователей онлайн. Все просто. Однако кажущаяся простота в результате приносит ряд проблем.
Все бы хорошо, если у Вас группы маленькой численностью (до 1000 человек). В противном случае упираемся в ограничения API - за один раз можно получить информацию только о 1000 пользователей. Что нам это ограничение - можно же вызывать метод в цикле, но нет. Производить вызовы API разрешено не чаще 3 запросов в секунду.
Посчитаем примерное количество запросов которое понадобится. Возьмем сообщество habrahabr Vk. Оно насчитывает более 40.000 пользователей, следовательно нам понадобится ~40 запросов чтобы получить членов сообщества и 40 запросов - их статус.
Отправляемся искать новое решение.

Второе решение (верное)

Обнаруживаем в документации метод execute 

Универсальный метод, который позволяет запускать последовательность других методов, сохраняя и фильтруя промежуточные результаты.

Принимает он на вход строку с кодом написанным на так называемом VKScript (похож на javascript). Проблема лишь в том, что вменяемая документация по этому методу и самому языку отсутствует. Вероятно решение найдено, так что можно углубится в изучение API Vk и VKScript в частности.

Работа с API

Скачиваем класс для работы с API, предлагаемый разработчиками. Я привел его только к более приемлемому виду, чтобы он вписался в coding style применяемый в Zend Framework.

Класс Api
<?php
class Vkapi_Model_Api {

    private $_accessToken = null;
    
    private $_apiUrl = 'https://api.vk.com/method/';
    
    public function __construct($accessToken) {
        
        $this->_accessToken = $accessToken;
        
    }
    
    public function api($method, $params = array())
    {
        $params['access_token'] = $this->_accessToken;
        $query = $this->_apiUrl. $method . '?' . $this->_params($params);
        $responseStr = file_get_contents($query);
        if(!is_string($responseStr)){
            return null;
        }
        
        $responseObj = json_decode($responseStr);
        return $responseObj;	
    }
    
    private function _params($params) {
        $pice = array();
        foreach($params as $k=>$v) {
            $pice[] = $k.'='.urlencode($v);
        }
        return implode('&',$pice);
    }
}

Аутентификацию и авторизацию я описывать не буду, так как она осуществляется через OAuth, много информации в рунете, да и на странице Vk API.

Осуществим пробный вызов к API - получим первые 20 постов в группе habrahabr

    public function wallsAction()
    {
        //.......
        $api = new Vkapi_Model_Api($accessToken);
        
        $response = $api->api('wall.get',array('owner_id' => '-20629724'));
        $this->view->walls = $response->response;
    }

image

Сейчас сделаем тоже самое, только через метод execute

    public function wallsAction()
    {
        //.......
        $api = new Vkapi_Model_Api($accessToken);
        $code = "
        	var walls = API.wall.get({ owner_id : -20629724 });
        	return walls;
        ";
        $response = $api->api('execute',array('code' => $code ));
        $this->view->walls = $response->response;
    }

В итоге получаем один и тот же результат.
Одно что плохо - это то, что мы смешали код VKScript и PHP. Выглядит это очень плохо. Займемся рефакторингом.
Было бы неплохо, чтобы каждый скрипт хранился в отдельном файле и вызвать его можно было бы одной функцией. Еще необходимо предусмотреть то, что в последствии нам еще понадобится передавать какие-то данные в этот скрипт ( сейчас например  owner_id  жестко забит в код).

Выносим VKScript в отдельные файлы

В корне нашего модуля создадим папку с названием "vkscripts", в нее будем складывать наши скрипты (например getWalls.vks). Пропишем путь к скриптам в config-файле application.ini

vkapi.scripts.path = APPLICATION_PATH "/modules/vkapi/vkscripts"

Нам нужен класс, который был бы удобен для вызова скриптов, расположенных в этой директории. Воспользуемся возможностями PHP5, а именно магическим методом __call. По названию вызываемого метода мы будем искать скрипт с таким названием.

Исходник класса
<?php

class Vkapi_Model_Executor
{
    private $_api;
    
    public function __construct($api)
    {
        $this->_api = $api;
    }
    
    public function __call( $methodName, $arguments )
    {
        $script = $this->_getScript($methodName);
        if(count($arguments)){
            $script = $this->_prepareParams($script, $arguments[0]);
        }		
        $response = $this->_api->api('execute', array('code' => $script));
        if( $error = $this->_getError($response) ){
        	throw new Exception($error->error_msg, $error->error_code);
        }
        
        return $response->response;	
    }
    
    private function _getError($response)
    {
        if( isset($response->error) ){
            $error = $response->error;
            return $error;			
        }
        return null;	
    }
    
    
    private function _getScript( $name )
    {
        $scriptsPath = Zend_Registry::get('vkapi_config')->scripts->path;
        
        $filePath = $scriptsPath . '/' . $name . '.vks';
        if(is_file($filePath)){
            $script = file_get_contents($filePath);
            return $script;
        }
        
        return null;
    }
}

Итак, давайте что-нибудь сделаем с этим классом.
В папку vkscripts кладем файл  getWalls.vks  с таким содержимым

var walls = API.wall.get({ owner_id : -20629724 });
return walls;

В контроллере:

    public function wallsAction()
    {
        //.......
        $api = new Vkapi_Model_Api($accessToken);
        $executor = new Vkapi_Model_Executor($api);
        $response = $executor->getWalls();
        $this->view->walls = $response->response;
    }

Мы получили тот же результат, только налицо существенные плюсы: мы разнесли код в отдельные файлы, сделали его более читабельным, упростили вызов execute.
Следующий шаг - добавление возможности передавать параметры в наш скрипт. Воспользуемся для этого неким представлением. В коде VKScript вначале при необходимости что-то получить на вход будем писать так:

var groupId = %GROUP_ID%;
var offset = %OFFSET%;

// .... здесь пошел наш код

А в нашем классе будем перед вызовом api с этим кодом заменять %VAR_NAME% на значение переменной.
Допишем наш класс Executor следующим образом

Исходник доработанного класса
<source lang="php">
<?php

class Vkapi_Model_Executor
{
    // ......
    
    public function __call( $methodName, $arguments )
    {
        $script = $this->_getScript($methodName);
        if(count($arguments)){
            $script = $this->_prepareParams($script, $arguments[0]);
        }		
        $response = $this->_api->api('execute', array('code' => $script));
        if( $error = $this->_getError($response) ){
        	throw new Exception($error->error_msg, $error->error_code);
        }
        
        return $response->response;	
    }

    // ......
    
    private function _prepareParams($script, $params)
    {
        
        foreach ($params as $key => $value){
            $script = str_replace('%' . strtoupper($key) . '%', $value, $script);
        }
        
        return $script;	
    
    }

}

В контроллере же при необходимости передачи параметром пишем следующее

    public function wallsAction()
    {
        //.......
        $api = new Vkapi_Model_Api($accessToken);
        $executor = new Vkapi_Model_Executor($api);
        $response = $executor->getWalls(array(
            'group_id'	=> -20629724,
            'offset'	=> 0
        ));
        $this->view->walls = $response->response;
    }

Что соответственно подставит в наш скрипт вместо %GROUP_ID% и %OFFSET% переданные значения.
Вот как выглядит структура модуля
image

Получаем количество пользователей онлайн

Cуществует ограничение на вызов методов API в execute. Лимит 22 вызова (найден практически). Так же в паутине я не нашел информации о том, что и на другие операторы (например сложение, вычитание ) тоже существуют ограничения, однако они есть. Поскольку если пробегать по массиву пользователей и считать количество онлайн я получал ошибку о превышенном числе операций, то было решено возвращать из execute полный список пользователей, после чего уже на стороне моего сервера считать их количество.
Из-за ограничения в числе запросов к API в методе execute нам все равно придется выполнить как минимум 1 запрос на 10.000 участников группы, потому что для обработки 1.000 требуется 2 запроса.
Вот скрипт который получился

var groupId = %GROUP_ID%;
var offset = %OFFSET%;

// API call limit
var _acl = 22;
var members = API.groups.getMembers({ gid : groupId }); _acl = _acl - 1;
var count = members.count;
var users = [];

while( _acl > 1 && offset < count){
    var _members = API.groups.getMembers({ gid : groupId, offset : offset }); _acl = _acl - 1;
    users = users + API.users.get({ uids : members.users, fields : "online" }); _acl = _acl - 1;
    offset = offset + 1000;
}
var result = {
    count	: count,
    offset	: offset,
    users	: users@.online
};
return result;

Немного прокомментирую свой код. Счетчик _acl - для предотвращения ошибки из-за превышения лимита операций с API. users@.online - возвращаем только список значений [0,1,1,0,0,0,1,0,1] онлайн-оффлайн. 
В контроллере вызываем этот скрипт, последовательно увеличивая offset, пока не пробежимся по всем участникам группы.

    $count = 1;
    $offset = 0;
    $nowOnline = 0;		
    while($count > $offset){
        $users = $executor->getOnline(array(
            'group_id'	=> $groupId,
            'offset'	=> $offset
        ));
        $count = $users->count;
        $offset = $users->offset;
        foreach ( $users->users as $online){
            if($online){
                $nowOnline++;
            }
        }
    }	

Итак протестим и увидим - данные полученные через API почти совпадают с данными с vk.com, возможно эта неточность из-за кешей, или по другой причине, не видной извне.

Замечания

VKScript не поддерживает функции, операторы инкремента, декремента. 

Итог

Мы разработали инструментарий для работы с API vk.com через метод execute. С помошью его можно разрабатывать приложения сбора статистики и т.д. причем выглядеть это будет очень даже приглядно. Прикрутить к этому всему интерфейс - это уже тривиальная задача. В конце замечу, что другая социальная сеть Facebook предоставляет доступ к исполнению кода, написанного на языке называемом FQL (Facebook Query Language, схож с SQL), у которого возможностей явно побольше чем у VKScript со всеми его ограничениями.


Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=31075