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-а?