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

в 17:30, , рубрики: php, php5, vk.com api, Песочница, метки: , ,

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

Инструменты

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

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

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

Архитектура

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

Обзор Vk API

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

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

С помощью методов groups.getMembers, users.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. Выглядит это очень плохо. Займемся рефакторингом.
Было бы неплохо, чтобы каждый скрипт хранился в отдельном файле и вызвать его можно было бы одной функцией. Еще необходимо предусмотреть то, что в последствии нам еще понадобится передавать какие-то данные в этот скрипт ( сейчас например group_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 со всеми его ограничениями.

Ссылки

VK API
Метод execute, краткое описание и пример VKScript
Facebook Query Language

Автор: isxam

Источник

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


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