13 мар. 2011 г.

Миф о продуктивности Python-а

Последние 2-3 года моя основная работа связана с Perl-ом и каждая попытка написать на Python-е что-нибудь "для души" заканчивается раздражением. Спотыкаешься там, где, казалось бы, никаких "коряг" быть не должно. Так произошло и в эти выходные.

Мне понадобилось написать программку, которая разбирает 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-а?