И так, продолжаем играться с питоном и попробуем написать статистику посещений для сайта. Для реализации воспользуемся связкой python и sqlite.
Я постараюсь рассказать про мой подход к написанию статистики для одного своего сайта написанного на питоне.

Создаем базу для статистики с одной единственной таблицей.
setup.py

1
2
3
4
5
6
7
8
9
10
11
# -*- coding: utf-8 -*-

import sqlite3

connection = sqlite3.connect('statistics.db')
cursor = connection.cursor()

cursor.execute('CREATE TABLE daily (id INTEGER PRIMARY KEY, time, uri, referer, user_agent, ip)')

cursor.close()
connection.close()

Для сбора статистики будем помещать в базу информацию о каждом запросе. У меня все запросы проходят через файл core.py так что в начале этого файла пишем код такого вида

1
2
3
4
5
6
7
8
9
10
11
12
13
14
core.py

# -*- coding: utf-8 -*-

import sqlite3
import time

connection = sqlite3.connect('statistics.db')
cursor = connection.cursor()

cursor.execute('INSERT INTO daily (time, uri, referer, user_agent, ip) VALUES (%s, %s, %s, %s, %s)' % (time.time(), uri, refer, user_agent, ip))

cursor.close()
connection.close()

Отвлечемся немного на sqlite. Модуль sqlite3 реализует работу с базой через DB-API 2.0. При таком подходе стандартная работа с базой выглядит так

connection = sqlite3.connect(‘файл БД’) — соединяемся с базой
cursor = connection.cursor() — получаем курсор, через него в дальнейшем выполняем все запросы

cursor.execute(запрос) — выполняем запрос

cursor.fetchone() — получить одну строку запроса
cursor.fetchall() — получить все строки запроса

connection.close() — закрываем соединение

В sqlite3 есть одна магическая команда, позволяющая обращаться к результатам выборки не только по индексу, но и по имени

1
connection.row_factory = sqlite3.Row

Теперь займемся самым легким, выбором из базы необходимых данных (мой подход возможно не самый оптимальный, так как вся нагрузка ложится на выборки, но все же)
Создадим класс для статистики.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# -*- coding: utf-8 -*-

import sqlite3
from parser import Parser

class Stats(object):

def __init__(self, db):
self.__connection = sqlite3.connect(db)
self.__connection.row_factory = sqlite3.Row
self.__cursor = self.__connection.cursor()

self.__parser = Parser()
self.__connection.create_function('truncate_time', 1, self.__parser.truncate_time)
self.__connection.create_function('parse_os_type', 1, self.__parser.parse_os_type)
self.__connection.create_function('parse_os_version', 1, self.__parser.parse_os_version)
self.__connection.create_function('parse_browser_type', 1, self.__parser.parse_browser_type)
self.__connection.create_function('parse_browser_version', 1, self.__parser.parse_browser_version)

def __del__(self):
self.__connection.close()

def get_rows(self, query):
self.__cursor.execute(query)
return self.__cursor.fetchall()

def get_row(self, query):
self.__cursor.execute(query)
return self.__cursor.fetchone()

def get_row_count(self, query):
self.__cursor.execute(query)
count = 0

for row in self.__cursor.fetchall():
count = count + 1

return count

def os_type(self):
return self.get_rows('SELECT COUNT(id) count, parse_os_type(user_agent) os FROM daily GROUP BY os ORDER BY count DESC')

def os_version(self):
return self.get_rows('SELECT COUNT(id) count, parse_os_type(user_agent) os, parse_os_version(user_agent) version FROM daily GROUP BY os, version ORDER BY count DESC')

def browser_type(self):
return self.get_rows('SELECT COUNT(id) count, parse_browser_type(user_agent) browser FROM daily GROUP BY browser ORDER BY count DESC')

def browser_version(self):
return self.get_rows('SELECT COUNT(id) count, parse_browser_type(user_agent) browser, parse_browser_version(user_agent) version FROM daily GROUP BY browser, version ORDER BY count DESC')

def daily_stats(self):
return self.get_rows('SELECT COUNT(id) count, truncate_time(time) date FROM daily GROUP BY date ORDER BY date DESC')

def page_visited(self):
return self.get_rows('SELECT COUNT(id) count, uri FROM daily GROUP BY uri ORDER BY uri')

def all_unique(self):
return self.get_row_count('SELECT ip, truncate_time(time) date FROM daily GROUP BY ip, date')

def unique_for_date(self, date):
return self.get_row_count('SELECT ip, truncate_time(time) date FROM daily WHERE date = «%s» GROUP BY ip, date' % date)

def unique_for_period(self, begin, end):
return self.get_row_count('SELECT ip, truncate_time(time) date FROM daily WHERE date BETWEEN «%s» AND «%s» GROUP BY ip, date' % (begin, end))

def count(self):
result = self.get_row('SELECT COUNT(id) count FROM daily')
return result[0]

def get_all(self):
return self.get_rows('SELECT * FROM daily')

Класс Parser мы рассмотрим поздней, в нем реализованы методы для обработки запросов.
В примере я реализовал следующие функции:

truncate_time — переводим тип timestamp к формату dd.mm.yyyy
parse_os_type — выбираем из поля user_agent типы ОС
parse_os_version — выбираем из поля user_agent версии ОС
parse_browser_type — выбираем из поля user_agent типы браузеров
parse_browser_version — выбираем из поля user_agent версии браузеров

Функция create_function делает доступными методы питона в запросах sqlite, для выборки статистики будем активно пользоваться этой возможностью.
Нам надо зарегистрировать методы класса как функции в sqlite

1
2
3
4
5
create_function('truncate_time', 1, self.__parser.truncate_time)
create_function('parse_os_type', 1, self.__parser.parse_os_type)
create_function('parse_os_version', 1, self.__parser.parse_os_version)
create_function('parse_browser_type', 1, self.__parser.parse_browser_type)
create_function('parse_browser_version', 1, self.__parser.parse_browser_version)

Методы класса

get_rows, get_row, get_row_count — спомогательные методы
os_type — получаем типы ОС
os_version — типы ОС вместе с версиями
browser_type — типы браузеров
browser_version — типы и версии браузеров
daily_stats — количество хитов по дням
page_visited — посещенные страницы и количество хитов
all_unique, unique_for_date, unique_for_period — все уникальные посетители, на дату и за период
count — общее количество хитов
get_all — все записи

Ну а теперь сам парсер. Приведу сразу полный код класса, думаю там ничего сложного нет. При вызове метода в него передается поле из базы и обрабатывается.
# -*- coding: utf-8 -*-

import datetime
import string

class Parser(object):

def parse_os_type(self, user_agent):
user_agent = user_agent.lower()

if 'windows' in user_agent:
return 'windows'
if 'linux' in user_agent:
return 'linux'
if 'macintosh' in user_agent:
return 'mac'
if 'freebsd' in user_agent:
return 'bsd'

return 'unknown'

def parse_os_version(self, user_agent):
user_agent = user_agent.lower()

if 'windows' in user_agent:
if 'windows nt 6.0' in user_agent:
return 'vista'
if 'windows nt 5.2' in user_agent:

return '2003'
if 'windows nt 5.1' in user_agent:
return 'xp'
if 'windows nt 5.0' in user_agent:
return '2000'
if 'windows nt 4.0' in user_agent:
return 'nt'
if 'windows 98' in user_agent:
return '98'
if 'windows 95' in user_agent:
return '95'

if 'macintosh' in user_agent:
start = user_agent.find('mac os x') + 8
return string.replace(user_agent[start: start + 7], '_', '.')

return 'unknown'

def parse_browser_type(self, user_agent):
user_agent = user_agent.lower()

if 'firefox' in user_agent:
return 'firefox'
if 'opera' in user_agent:
return 'opera'
if 'safari' in user_agent:
return 'safari'
if 'msie' in user_agent:
return 'ie'

return 'unknown'

def parse_browser_version(self, user_agent):
user_agent = user_agent.lower()

if 'firefox' in user_agent:
start = user_agent.find('firefox/') + 8
return user_agent[start: start + 3]

if 'opera' in user_agent:
start = user_agent.find('opera') + 5
return user_agent[start: start + 4]

if 'safari' in user_agent:
start = user_agent.find('version/') + 8
return user_agent[start: start + 4]

if 'msie' in user_agent:
start = user_agent.find('msie') + 4
return user_agent[start: start + 4]

return 'unknown'

def truncate_time(self, time):
date = datetime.datetime
return date.fromtimestamp(time).strftime('%d.%m.%Y')
Пример использования

1
statex = Stats(<span style="color: #a31515;">'statistics.db'</span>)

for row in statex.browser_type():
print '%s %s' % (row['browser'], row['count'])

for row in statex.os_type():
print '%s %s' % (row['os'], row['count'])
У меня в проекте это выглядит примерно так.
Выполняю запрос и передаю его в шаблон:
statex = Stats('statistics.db')
total = statex.count()

if (re.match(r'^(.*)/os(\/*)$', req.uri)):
os_types = []
for row in statex.os_type():
os_types.append({'name': row['os'], 'count': row['count'], 'percent': calc_percent(total, row['count'])})

os_versions = []
for row in statex.os_version():
os_versions.append({'name': row['os'], 'version': row['version'], 'count': row['count'], 'percent': calc_percent(total, row['count'])})

template = Template(filename=path + '/templates/os.tpl', format_exceptions=True)
req.content_type = 'text/html'
req.write(template.render(os_types=os_types, os_versions=os_versions, fetch_time=round(time.time() — start, 3)))
В шаблоне рисую табличку
<h1>OS</h1>

</p>

% for row in os_types:
${row['name']} ${row['count']} ${row['percent']}</br>
% endfor

<h1>OS Versions</h1>

% for row in os_versions:
${row['name']} ${row['version']} ${row['count']} ${row['percent']}</br>
% endfor

</p>

Fetch time: ${fetch_time}
Все. Дорабатывайте парсер и запросы под свои нужды.

P.S. Да бы меня не уличие в велосипедостроении, прошу считать данный пост примером к использованию sqlite3 и python :)