Мне понадобилось написать программку, которая разбирает access.log и раскладывает его поля по таблицам базы данных SQLite. Вполне тривиальная задачка, кто только ей ни занимался.
Сам парсер не представляет из себя ничего сложного. Вот пример строки лог-файла:
89.179.242.83 - - [06/Mar/2011:07:09:48 +0000] "GET /themes/_pebble/handheld.css HTTP/1.1" 200 1020 "http://www.lunarium.ru/2011/02/19/1298158440000.html" "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_6; ru-ru) AppleWebKit/533.19.4 (KHTML, like Gecko) Version/5.0.3 Safari/533.19.4"
А вот регулярное выражение, которое прекрасно с ней справляется (я позаимствовал его из Johen's blog :
parts = [
r'(?P\S+)', # host %h
r'\S+', # indent %l (unused)
r'(?P\S+)', # user %u
r'\[(?P
В одной из таблиц базы данных я решил хранить IP-адреса хостов, с которых приходят запросы. Хранить их удобнее в виде целых чисел, а не 4 значений, разделенных точками. Системные библиотеки прекрасно понимают и тот и другой формат.
Berkana:~ sergey$ ping 3232235777
PING 3232235777 (192.168.1.1): 56 data bytes
64 bytes from 192.168.1.1: icmp_seq=0 ttl=255 time=6.288 ms
В Python-овской бибиотеке urllib имеется функция inet_aton, возвращающая упакованное бинарное значение, которое может потребоваться при обмене данными с низкоуровневыми С-библиотеками. Из него можно получить целочисленное значение, для этого его надо распаковать, используя стандартный модуль struct. Беда в том, что документация к inet_aton немногословна и мне понадобилось немало времени, чтобы понять, с какими ключами вызывать функцию struct.unpack для получения нужного результата.
import socket
import struct
struct.unpack('!L', socket.inet_aton(ip))
, где ip представляет собой строку типа: '192.168.1.1'.
Обратная операция:
socket.inet_ntoa(struct.pack('!L', i))
, где i представляет собой целое число.
Я в курсе, что можно плюнуть на библиотеки и поступить по-простому:
a << 24 | b << 16 | c << 8 | d
, где a, b, c, d — части Ip-адреса. Но не стал этого делать из принципа, дабы не плодить сущности.
Следующая трудность возникла снова на пустом месте. В таблице referer я решил хранить адреса, с которых посетители заходят на мой сайт. URL-адреса могут быть закодированы (URL-encoded). Типичный пример — адрес страницы поисковой системы с результатами, в которую включена поисковая фраза:
http://go.mail.ru/search?q=%E4%EE%EB%E3%EE%F2%E0%20%E4%ED%FF%20%ED%E0%20%EC%E0%F0%F2%202011%E3%EE%E4%E0&rch=e&num=10&sf=90%22
Тарабарщина со знаками процентов в данном случае — ни что иное как фраза "долгота дня на март 2011года". Разумеется, хранить удобнее читаемый текст. В Perl-е, чтобы расшифровать закодированную таким методом строку, достаточно импортировать модуль URI::Escape и вызвать его функцию uri_unescape(). В Python-е для этой же цели служат urllib.unquote() и urllib.unquote_plus().
>>> import urllib
>>> url = "http://go.mail.ru/search?q=%E4%EE%EB%E3%EE%F2%E0%20%E4%ED%FF%20%ED%E0%20%EC%E0%F0%F2%202011%E3%EE%E4%E0&rch=e&num=10&sf=90"
>>> unquoted_url = urllib.unquote_plus(url)
>>> unquoted_url
'http://go.mail.ru/search?q=\xe4\xee\xeb\xe3\xee\xf2\xe0 \xe4\xed\xff \xed\xe0 \xec\xe0\xf0\xf2 2011\xe3\xee\xe4\xe0&rch=e&num=10&sf=90'
Поскольку SQLite работает с UTF-8, полученное значение необходимо перевести в unicode.
>>> unicode(unquoted_url)
Traceback (most recent call last):
File "", line 1, in
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 27: ordinal not in range(128)
Что случилось? А дело в том, что функция unicode не знает кодировку исходной строки. Раз запрос исходил из mail.ru, наверняка тут русская windows-кодировка. Так и есть!
>>> unicode(unquoted_url, encoding='cp1251')
u'http://go.mail.ru/search?q=\u0434\u043e\u043b\u0433\u043e\u0442\u0430 \u0434\u043d\u044f \u043d\u0430 \u043c\u0430\u0440\u0442 2011\u0433\u043e\u0434\u0430&rch=e&num=10&sf=90'
Ну, а как быть с другими кодировками? ...Вспоминаю, что для угадывания кодировки текста существует питоновский модуль chardet. Он считается достаточно надежным, однако для моей цели явно не подходит — уж слишком часто промахивается. Видимо потому, что исходный текст слишком короток. Пришлось написать вот такой костыль:
def decode_referer(url):
refurl = urllib.unquote_plus(url)
try:
refurl = unicode(refurl)
except UnicodeDecodeError:
logger.warn("Could not decode string: '%s'. Trying cp1251..." % refurl)
try:
refurl = unicode(refurl, encoding='cp1251')
logger.info('Success.')
except
UnicodeDecodeError: logger.warn("Could not decode string. Trying to guess encoding...")
enc = chardet.detect(refurl)['encoding']
logger.warn('Detected %s' % enc)
try:
refurl = unicode(refurl, encoding=enc)
logger.info('Success.')
except UnicodeDecodeError:
logger.error('Could not decode string. Will store it as is')
return refurl
...И кто придумал миф о продуктивности Python-а?