З досвіду створення системи аналізу відвідуваності комерційного сайту (частина 2), Сервери, Інтернет-технології, статті

Автор: Олег Артемов, системний адміністратор компанії Intersoft Lab

Підготовка даних для ClickStream Intelligence

Деякий час тому у нашій компанії з’явився новий комерційний веб-сайт www.contourcomponents.com. Природно, було цікаво проаналізувати статистику відвідувань веб-сайту, щоб зробити висновки про ефективність рекламної кампанії та визначити аудиторію сайту.

Існує багато способів аналізу логів веб-серверів, від “tail-f” до таких багатих можливостями програм, як, наприклад, Analog. Однак, жоден з них з якихось причин не задовольняв всіх запитів. Оскільки в нашої компанії є OLAP-клієнт власної розробки – “Контур Стандарт”, було вирішено використовувати для цього саме його.

“Контур Стандарт” легко налаштовується на будь-яку базу даних, для якої існує драйвер ODBC або лінк BDE. Проблема полягала в тому, що, по-перше, статистика відвідувань веб-сервера ведеться в текстовому файлі, а не в базі даних, а по-друге, наш веб-сервер розміщується у провайдера і працює під управлінням операційної системи Linux (хоча операційна система не має великого значення).

Перша проблема вирішується написанням скрипта, який обробляє лог і копіює його в СУБД. Для другої проблеми існує два способи вирішення: можна вести базу прямо на веб-сервері, використовуючи, наприклад MySQL, і ходити до неї по TCP / IP з якого місця. Але це генерує дуже великий трафік і небажано з міркувань безпеки. Тому ми зупинилися на другому способі: періодичному копіюванні даних на SQL сервер, що знаходиться всередині локальної мережі компанії.

У цій статті розповідається про один із способів обробки логів веб-сервера і подальшого копіювання в СУБД, що знаходиться на віддаленому сервері за допомогою скрипта, написаного на мові Python.

Налаштування журналізації веб-сервера

Для того, щоб мати можливість отримувати цікаву інформацію з логів веб-сервера, потрібно спочатку настроїти журналізацію так, щоб ця інформація туди потрапляла. Веб-сервер Apache надає можливість ведення будь-якої кількості журналів та визначення формату кожного.

Формат журналу визначається директивою LogFormat. Повний опис цієї директиви виходить за рамки цієї статті, тому скажу тільки, що в стандартному файлі конфігурації httpd.conf, що поставляються з Apache, під ім’ям combined вже визначений формат логу, що надає наступну інформацію: ip адресу клієнта, дату і час запиту, зміст запиту, код відповіді сервера, кількість переданих байт, адреса посилається сторінки, опис браузера клієнта і ще пару елементів, яких ми торкатися не будемо. Визначення формату виглядає наступним чином:


LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined

Тепер залишилося визначити файл логу:


CustomLog <Ім'я файлу> combined

Якщо ви використовуєте Internet Information Server, потрібно на сторінці Web Site властивостей сервера відзначити опцію Enable Logging і вибрати формат логу. У формату W3C Extended Log File Format можна вибирати записувані елементи запиту (Properties \ Extended Properties).

Починаємо обробку логу

У балці кожен клієнтський запит представлений окремим рядком, тому для читання файлу лога зручно скористатися конструкцією Python подібний до наступного:


log = open('/usr/local/apache/logs/access_log')
for request in log.readlines():
    ...

Природно, не обов’язково кожен раз обробляти весь файл, можна почати з першого рядка, що з’явилася з моменту останнього запуску скрипта. Для цього в кінці кожної сесії потрібно запам’ятовувати номер рядка, наприклад, в текстовому файлі. Тоді наступну сесію можна почати з цього рядка:


start = int(open('last').readline())
for request in log.readlines()[start:]:
    ...

Елементи в рядку запиту розділяються пробілами. Так як в деяких елементах можуть міститися пробіли (наприклад, типове зміст поля User-Agent таке: “Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0) “), вони беруться в лапки. Тому має сенс спочатку перетворити рядок в кортеж, використовуючи як роздільник”, а потім вже отримані елементи кортежу знову розбити на кортежі, визначивши роздільником пробіл. Це робиться методом split ():


>>> request=’62.110.7.82 – – [13/Sep/2002:14:19:58 +0400] “GET /demo.htm HTTP/1.1” 200 7297 “http://www.contourcomponents.com/ccactivex.htm” “Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)”‘
>>> req1 = request.split(‘”‘)
>>> req1
[‘62.110.7.82 – – [13/Sep/2002:14:19:58 +0400] ‘, ‘GET /demo.htm HTTP/1.1’, ‘ 200 7297 ‘, ‘http://www.contourcomponents.com/ccactivex.htm’, ‘ ‘, ‘Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)’, ”]

В отриманому кортежі нас цікавлять:

Тепер потрібно витягнути потрібні елементи запиту, знову застосувавши split (), але вже з роздільником-пробілом:

>>>> # Отримуємо ip і дату

>>>> req2 = req1[0].split()
>>>> req2
[‘62.110.7.82’, ‘-‘, ‘-‘, ‘[13/Sep/2002:14:19:58’, ‘+0400]’]
>>>> ip = req2[0]
>>>> date = req2[3][1:]
>>>> date
’13/Sep/2002:14:19:58′
>>>> # Отримуємо запит клієнта. Метод запиту і протокол нас не цікавлять, тому:

>>>> user_request = req1[1].split()[1]
>>>> user_request
‘/demo.htm’
>>>> # Отримуємо код стану і кількість байт

>>>> status, bytes = req1[2].split()
>>>> status
‘200’
>>>> bytes
‘7297’
>>>> # Отримуємо посилається адресу і браузер

>>>> referer = req1[3]
>>>> browser = req1[5]

До речі, browser можна також розділити, і отримати назву браузера і операційну систему клієнта, а з referer витягти, наприклад, пошукові слова.

Очищення даних

Легко помітити, що далеко не всі отримані дані підходять для завантаження на SQL сервер. Наприклад, дата повинна бути в форматі, “зрозумілою” серверу. Не кажучи вже про те, що bytes і status можуть і не бути числами, тоді як в базі даних для них має сенс завести поля типу integer (для подальшого підсумовування).

Розберемося спочатку з датою. Визначимо словник з порядковими номерами місяців. Він нам буде потрібен надалі:


month = {‘Jan’ : 1, ‘Feb’ : 2, ‘Mar’ : 3, ‘Apr’ : 4, ‘May’ : 5, ‘Jun’ : 6, ‘Jul’ : 7, ‘Aug’ : 8, ‘Sep’ : 9, ‘Oct’ : 10, ‘Nov’ : 11, ‘Dec’ : 12}

>>>> # Поділяємо на день, години, хвилини, секунди

>>>> datetime = date[1:].split(‘:’)
>>>> datetime
[’13/Sep/2002′, ’14’, ’19’, ’58’]

В принципі, день можна залишити як є. MS SQL Server розуміє такий формат. Тоді остаточна дата буде:


>>>> sql_date = ‘%s %s:%s:%s’ % (datetime[0], datetime[1], datetime[2], datetime[3])
>>>> sql_date
’13/Sep/2002 14:19:58′

Можна визначити інший формат, наприклад:

>>>> # Отримуємо день, місяць, рік

>>>> day = datetime[0].split(‘/’)
>>>> day
[’13’, ‘Sep’, ‘2002’]
>>>> # Тепер конструюємо дату

>>>> sql_date = ‘%s-%s-%s %s:%s:%s’ % (day[2], month[day[1]], day[0], datetime[1], datetime[2], datet
ime[3])
>>>> sql_date
‘2002-9-13 14:19:58’

Тепер перевіримо, чи справді bytes і status містять цілі числа. Це найкраще зробити за допомогою конструкції try / except:


try:
    bytes = int(bytes)
except:
    bytes = 0

Ще одна проблема може виникнути з вмістом запиту клієнта. Справа в тому, що рядок запиту може бути дуже довгою (наприклад, при спробі злому веб-сервера зловмисник може сформувати таку рядок, намагаючись скористатися уразливістю переповнення буфера). Має сенс обмежити її розумною величиною, наприклад 256 символів:


if len(user_request) > 255:
    user_request = user_request[:255]

Звичайно, якщо ви хочете аналізувати спроби злому і некоректні запити, потрібно залишити все як є, задавши відповідному полю таблиці потрібний формат.

Визначення імені хоста

Якщо в httpd.conf включено визначення імен хостів (HostNameLookups on), в лог записуються не ip адреси клієнтів, а імена хостов. Включати визначення імен хостів не рекомендується, так як погіршує продуктивність веб-сервера. Набагато краще визначати імена хостів клієнтів (якщо це потрібно для аналізу) при обробці логів.

Для визначення імені хоста по ip адресою модулі socket є функція gethostbyaddr (). Вона повертає кортеж, що складається з основного імені хоста, списку додаткових імен та списку додаткових ip адрес.


>>>> import socket
>>>> host = socket.gethostbyaddr(‘194.109.137.226’)
>>>> host
(‘fang.python.org’, [], [‘194.109.137.226’])

Якщо дізнатися ім’я хоста не вдалося, збуджується виключення:


>>>> socket.gethostbyaddr(‘172.16.0.1’)
Traceback (most recent call last):
..File “<stdin>”, line 1, in ?
socket.herror: (11004, ‘host not found’)
>>>> 

Так як в балці зазвичай присутній не один запит з даного ip адреси, немає сенсу щоразу визначати ім’я хоста за допомогою виклику функції gethostbyaddr (). Можна заповнювати словник певними відповідниками і спочатку пробувати шукати в ньому, а вже потім викликати функцію. А так як імена хостів змінюються рідко, можна завести текстовий файл (або базу даних) і зберігати інформацію в ньому. Тоді імена хостів, визначені під час сесії, використовуються в наступних сесіях, що сильно зменшує час обробки. Невдалі спроби визначення імені також має сенс записувати в словник, щоб під час поточної сесії більше не намагатися розпізнати цю адресу. Підсумовуючи все вище сказане, наведемо фрагмент скрипта для визначення імені хоста:


import socket

dns = {}
bad_ip = {}

# Відкриваємо файл з іменами хостів для додавання
names = open('names.txt', 'a+')

# Заповнюємо словник уже відомими іменами
for i in names.readlines():
    dns[i.split()[0]] = i.split()[1]

if dns.has_key(ip):
# Якщо адреса є у словнику, присвоюємо знайдене ім'я
    host = dns[ip]
elif bad_ip.has_key(ip):
# Якщо раніше не вдалося визначити ім'я - використовуємо ip
    host = bad_ip[ip]
else:
    try:
# Намагаємося визначити ім'я
        host = socket.gethostbyaddr(ip)[0]
# Записуємо його в словник і в файл
        dns[ip] = host
        names.write('%s %s\n' % (ip, host))
    except:
# Визначити ім'я не вдалося - використовуємо ip
# І записуємо його в словник невизначених ip адрес
        host = ip
        bad_ip[ip] = ip

Примітка: словник “невизначених” адрес ведеться тільки в перебігу сесії та в файл не записується (а раптом наступного разу пощастить?). Якщо ім’я визначити не вдалося, як ім’я береться ip адресу.

Визначення країни по ip адресою

Наступним кроком непогано було б визначити країну клієнта. Звичайно, це можна зробити по домену першого рівня, взятого з імені хоста, але, по-перше, фізичне розташування хоста не завжди відповідає країні, якої приписаний домен (наприклад, хост зони. com може знаходитися і в Росії), до того ж є такі інтернаціональні домени як. org,. net,. edu і т.д.

Проблему можна вирішити, отримавши статистику про розподіл адресного простору від однієї з організацій, відповідальної за це (APNIC (www.apnic.net/), RIPE NCC(www.ripe.net/), ARIN (www.arin.net/)). Ці організації з певною періодичністю розміщують таку статистику на своїх FTP серверах. Для отримання цієї інформації та формування бази розподілу ip адрес по країнах, було вирішено скористався кодом, описаним у статті Д.Откідача “Визначення країни по IP адресою” (python.ru/2002-06/69.html). Наведений у статті код розповсюджується під ліцензією в стилі Python і складається з визначення двох класів IPRangeDB і його спадкоємця CountryByIP, в якому визначено методи заповнення бази. Для зручності я помістив опису цих класів в окремий модуль country.py, в кінці якого додав пару рядків:


if __name__ == "__main__":
    db = CountryByIP('country.db', 'n')
    db.fetch()

Таким чином, клас IPRangeDB можна імпортувати з основного модуля і використовувати для визначення країни по ip адресою, а якщо запустити country.py, буде оновлюватися база відповідності ip адрес країнам. Хороший варіант – періодично автоматично запускати скрипт (через cron в Linux або Task Sheduler в Windows).

В основному скрипті для визначення країни пишемо наступний код:


db = IPRangeDB('country.db')
undefined_country = {}

if not undefined_country.has_key(ip):
    try:
        country = db[ip]
    except KeyError:
print 'Не вдалося визначити країну для:% s'% ip
        undefined_country[ip] = '00'
        country = '00'
else:
    country = '00'

Ip адреси, для яких не вдалося визначити країну, виводяться на екран і додаються до словника undefined_country. При цьому країні присвоюється код “00”. Висновок на екран можна продублювати висновком в окремий файл для подальшого аналізу.

Пересилання результатів обробки по FTP

Отже, ми обробили клієнтський запит і в результаті отримали кілька змінних з цікавої для нас інформацією.

Зручно записати ці змінні в якій-небудь файл, який потім легко можна буде обробити, наприклад в csv. Далі його потрібно заархівувати для зменшення трафіку і передати по FTP на наш локальний SQL сервер для подальшої обробки. Для запису рядка в csv файл додаємо:


csv.write('%s;%s;%s;%s;%s;%s;%s;%s;%s;%s\n' % (ip, host, date, time, request, status, bytes, referer, browser, country))

(Мається на увазі, що csv-файловий об’єкт, відкритий на запис)

Для копіювання файлу по протоколу FTP використовуємо модуль ftplib. Так як обсяг даних може бути дуже великим, заархівіруем файл:


import os

command = 'bzip2 -z log.csv'
os.system(command)

Потім створюємо з’єднання з FTP сервером і передаємо файл:


ftp = FTP('ftp.myhost.ru', 'oartemov', 'secret')
ftp.set_pasv(1)
ftp.cwd('/export')
ftp_command = 'STOR log.csv.bz2'
ftp.storbinary(ftp_command, open('log.csv.bz2'))
ftp.quit()

Видаляємо непотрібний архів:


os.unlink('log.csv.bz2')

У прикладі описується досить екзотичний випадок, коли FTP сервер знаходиться всередині локальної мережі, а веб-сервер, що стоїть у провайдера, виступає в ролі клієнта. Зазвичай же FTP сервер встановлений на тому ж комп’ютері, що і веб-сервер, тому скрипт необхідно запускати з комп’ютера, що знаходиться в локальній мережі і замість методу storbinary () використовувати retrbinary (). Можна код, відповідальний за отримання даних по FTP, розмістити спочатку скрипта, поміщає дані в СУБД.

Корисні посилання:

Визначення країни по IP адресою:

python.ru/2002-06/69.html

Текстова обробка в мові Python. Підказки для початківців:

www.iso.ru/cgi-bin/main/journal.cgi?do_what=details&id=27

Готуючи на Python. Сім вишуканих рецептів для програмістів:

www.iso.ru/cgi-bin/main/journal.cgi?do_what=details&id=199

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*