isxam
Столкнулся с задачей мониторинга активности пользователей всем известной социальной сети. Передо мной стояла задача собирать данные о количестве пользователей, находящихся онлайн в определенной группе или сообществе.
Инструменты
Поскольку сам я занимаюсь веб-разработкой, то инструменты мной использовались такие
- 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;
}
Сейчас сделаем тоже самое, только через метод 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% переданные значения.
Вот как выглядит структура модуля
Получаем количество пользователей онлайн
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 со всеми его ограничениями.