
Думаю, многим интересно (хотя бы из любопытства), как именно они используют свой компьютер: самые нажимаемые кнопки, пройденное мышью расстояние, среднее время работы и другую информацию. В этой статье я расскажу один из вариантов того, как можно собрать такую информацию и затем представить её в виде интерактивных графиков. Все описанные действия производились на ноутбуке с ОС
Debian Wheezy
, Python 2.7.3
, R 2.15
.
Сбор данных
Всё началось с того, что этой осенью мне захотелось иметь как можно более полную статистику о моём использовании компьютера. Сказано — сделано: написал простейший кейлоггер, записывающий все нажатия и отпускания клавиш на клавиатуре, кнопок на мыши, а также все перемещения мыши. Также в голову пришла идея делать фото вебкамерой через некоторый промежуток времени, что также было реализовано.
Изначально все данные писались просто в текстовый файл, но потом решил всё-таки сделать «по хорошему» и перевёл запись всех событий в базу
SQLite
. Несмотря на то, что размер её по сравнению с сжатым gzip'ом текстовым файлов больше в несколько раз (сейчас база занимает немногим больше 0.5 Гб), это всё равно относительно немного, а делать выборки из базы всё-таки удобнее. Фото с камеры по-прежнему хранятся отдельно и их общий размер сейчас составляет 2.2 Гб (около 30000 файлов).Представление информации
Итак, с записью данных всё вроде бы решено, осталось всё это представить в виде удобных графиков и таблиц. Так как я уже достаточно давно собирался изучить язык
R
, но всё как-то не было повода начать, то решил его здесь и использовать. В общем-то после других языков начать пользоваться R
было достаточно несложно, хотя некоторые непривычные вещи всё-таки есть: например большинство стандартных операторов и функций векторизированы, принят стиль именования с разделением слов точкой (вместо _ или camel case в других языках). Сначала я просто осваивался с самим языком и входящими в стандартную поставку инструментами для обработки и отображения данных (надо сказать, достаточно богатыми), затем нашёл библиотеки ggplot2
для гибкого построения графиков и plyr
для операций вида split-apply-combine над массивами данных. Также существует возможность создавать файлы с Markdown
разметкой и внедрёнными в неё вычислениями на R (с помощью knitr
), что достаточно удобно. Однако, всё это статические графики и таблицы, и для любого изменения их внешнего вида, выбора некоторого подмножества данных требуется каждый раз переписывать код, а хотелось бы чего-то более динамичного, с возможностью ставить элементы управления вроде слайдеров, кнопок и другого.Как оказалось, недавно появился удобный способ достичь этого в
R
, с очень малым количеством дополнительного кода. Наткнулся я на этот инструмент можно сказать случайно, и не пожалел. Смотрите сами: Shiny
— «Easy web applications in R». Простейший пример есть прямо на той странице, и он действительно прост в написании. Надо сказать, что Shiny
— достаточно новый продукт, разработка (судя по репозиторию) началась в июне прошлого (2012) года и активно продвигается. Каких-либо багов в нём мне не встретилось, так что думаю можно считать проект стабильным. Кстати, на том же сайте можно найти RStudio — удобную IDE для разработки на R.Таким образом, отображение статистики я стал реализовывать с использованием
Shiny
. Некоторые скрины того, что получилось на текущий момент:


Также можно просмотреть эти страницы (правда в статическом виде, т.е. элементы управления не работают) на shiny-sample.aplavin.ru.
Видно, что возможности действительно богатые, причём внешний вид страницы можно полностью менять
HTML
, CSS
и JavaScript
кодом (у меня везде используется стандартный вариант). Удобно, что для работы Shiny
не требуется устанавливать никакой сервер, всё необходимое содержится в самом R-пакете.Вкратце о реализации
Весь код (и кейлоггер, и визуализация) доступен на BitBucket. Сейчас у меня вызывается по крону каждую минуту файл
capture
, который с вероятностью 50% делает снимок и сохраняет его (из-за того, что камера инициализируется не мгновенно, делается 20 снимков и сохраняется последний из них). Кейлоггер представлен исполняемым файлом keylogger.py
, запускаемым из inittab
'a (с использованием опции respawn
). В папке statistics
хранятся файлы keylogger.stats.R
и keylogger.stats.Rmd
, первый из которых генерирует графики просто в виде картинок, второй — в виде HTML страницы с использованием knitr
(оба, разумеется, статичные). Наконец, в папке shiny_page
содержатся файлы собственно страницы (ui.R
, server.R
) и файл compute.data.R
, который вычисляет все необходимые данные (сейчас это занимает от 30 секунд до 1 минуты, вынесено в отдельный файл чтобы не вычислять каждый раз при открытии страницы). Для удобства в той же папке есть Makefile
, который позволяет запускать приложение командой make run
.Вычисления статистик
Изначально все расчёты производились почти полностью запросами к базе
SQLite
, но затем, сравнив производительность GROUP BY
с функциями пакета plyr
, я увидел, что SQLite
выполняет аналогичные действия намного медленнее, даже с индексами. Единственная (но важная) проблема состоит в том, что для использования этих функций необходимо загружать весь набор данных в память. Сейчас при выполнении compute.data.R
используется около 1 Гб памяти, и через некоторое время 4 Гб на моём ноутбуке будет не хватать. В таком случае думаю нужно будет вернуться опять на вычисления средствами базы данных, это хотя и значительно медленнее, но хотя бы будет работать (хотя, конечно, предложения по этому поводу приветствуются). Для сравнения, аналогичный код на SQL и на R с использование plyr:SELECT field, COUNT(*) FROM Table GROUP BY field
ddply(dataset, ~field, nrow)
Также можно делать и более сложные, многоуровневые группировки. Пример из моего
compute.data.R
(без аналога на SQL, но думаю после предыдущего примера эта двухуровневость должна быть понятна):mouse.coords.by.win <- ddply(
coords,
~window,
function(df) {
res <- ddply(
df,
.(x=as.integer(x/binsize), y=as.integer(y/binsize)),
.fun=nrow,
.drop=F)
res$V1 <- res$V1 / max(res$V1)
res$cnt <- nrow(df)
res
})
Этот код считает для каждого окна (столбец
window
) распределение координат мыши по экрану по квадратам со стороной binsize
.Кейлоггер на Python
Для перехвата событий X сервера используются
Python
биндинги для xlib
, запись этих событий производится в базу данных SQLite
. Стоит заметить, что т.к. скрипты из inittab
запускаются от пользователя root
, перед обращением к xlib
нужно установить переменную окружения: os.environ['XAUTHORITY'] = '/home/USER/.Xauthority'
. Затем мы подключаемся к дисплею и создаём recording context для получения нужных нам событий (нажатий клавиш, кнопок и перемещений мыши):dpy = display.Display(':0')
ctx = dpy.record_create_context(
0,
[record.AllClients],
[{
'core_requests': (0, 0),
'core_replies': (0, 0),
'ext_requests': (0, 0, 0, 0),
'ext_replies': (0, 0, 0, 0),
'delivered_events': (0, 0),
'device_events': (2, 6), # в этой строке определяется, какие события мы получаем: в данном случае те, код которых от 2 до 6
'errors': (0, 0),
'client_started': False,
'client_died': False,
}])
dpy.record_enable_context(ctx, record_callback) # эта функция возвращает управление только при вызове record_disable_context в callback
Весь остальной код — получение, обработка и запись событий из callback-функции. В связи со сложной структурой полученных от X сервера данных, при обработке используется такой цикл:
def record_callback(reply):
data = reply.data
while len(data):
event, data = rq.EventField(None).parse_binary_value(data, record_dpy.display, None, None)
# здесь обработка event
В собственно обработке event'а нет ничего сложного: получаем его тип, дополнительные данные (поле detail) и записываем их. Однако, так как мы хотим записывать также окно (а точнее его класс), где произошло событие, нужно получить его. Это также делается с помощью функций из
xlib
:windowvar = dpy.get_input_focus().focus
wmclass = windowvar.get_wm_class()
Остальная часть кода — получение нормального названия клавиши из её кода, запись события в соответствующую таблицу в базе и обработка ошибок. С записью в базу связана ещё одна небольшая особенность: если при каждом событии flush'ить результат в файл, то при поступлении большого их количества запись не будет успевать проходить. Поэтому я записываю примерно через каждые 100 событий:
if randint(0, 100) <= 0:
dbconn.commit()
Конечно, в таком случае при аварийном завершении процесса небольшое количество последних событий не сохранятся, но в данном случае это не критично.
Схема базы данных:
CREATE TABLE KeyEvents(TimeStamp REAL, KeyName TEXT, EventType INTEGER, WindowClass TEXT);
CREATE TABLE MouseBtnEvents(TimeStamp REAL, KeyName TEXT, EventType INTEGER, WindowClass TEXT);
CREATE TABLE MouseMoves(TimeStamp REAL, MoveX INTEGER, MoveY INTEGER, WindowClass TEXT);
Заключение
В итоге можно сказать, что я разобрался с
R
— действительно удобным языком для подобных вычислений, с прозрачным захватом событий X сервера, ну и конечно получил красивую статистику своего использования компьютера. У меня есть ещё несколько идей по поводу того, какие графики и таблицы ещё стоит добавить в такой «отчёт», но интересно было бы также услышать ваши варианты (если кто-то дочитал до сюда).
P.S.: посоветуйте пожалуйста хорошую книгу или онлайн-курс по статистике и/или графической визуализации получаемых данных.
UPD1: значительно дополнена информация о реализации основных частей системы.