13 янв. 2009 г.

Краулер своими руками. Часть 9

Пора заняться усовершенствованием краулера.

Сжатие контента

Большинство серверов умеют сжимать передаваемый контент, а браузеры, соответственно,-- разжимать. Делается это ради экономии трафика. Только вначале клиент и сервер должны договориться об этом между собой.
  • Клиент должен отправить в запросе заголовок Accept-encoding: алгоритм сжатия(например, gzip).
  • Если сервер умеет сжимать страницу указанным алгоритмом, он это сделает и вернет заголовок Content-Encoding: алгоритм сжатия.
  • Клиент проверяет заголовок Content-Encoding и если там указано сжатие, распаковывает данные.
До недавних пор распаковывать налету сжатый контент средствами питоновских библиотек было непросто. Xah Lee даже использовал официальную документацию к модулю gzip как пример неудачной документации. У меня год-два назад не получалось распаковывать сжатый web-контент, как ни старался. Но, видимо, в текущей версии недоработки были исправлены. Сейчас простой рецепт приведен в Dive Into Python.

Метод visit, добавленный в класс UserAgent, представляет собой обертку вокруг open:
...
from StringIO import StringIO
import gzip

class UserAgent(object):
...

def open(self, url, add_headers=None):
"""
Возвращает file-like object, полученный с заданного адреса.
В случае ошибки возвращает HTTPError, URLError или IOError.
"""
logging.info('Opening %s...' % url)
req = urllib2.Request(url, None)
if add_headers:
for k, v in add_headers.iteritems():
req.add_header(k, v)
handle = self.opener.open(req, None, TIMEOUT)

return handle

def gunzip(self, stream):
gz = gzip.GzipFile(fileobj=stream)
return StringIO(gz.read())

def visit(self, url, add_headers={}, on_success=None, on_failure=None):
"""
Возвращает последний пройденный URL, объект StringIO и info, полученные
с заданного адреса через callback-метод on_success.
В случае ошибки вызывает метод on_failure, передавая туда исклчение.

Фактически, это обертка вокруг open. Важное отличие в том, что
этот метод автоматически разворачивает сжатый контент.
"""
add_headers.update({'Accept-encoding': 'gzip'})
try:
r = self.open(url, add_headers)
s = StringIO(r.read())
info = r.info()
if info.get('Content-Encoding') == 'gzip':
logging.debug('Gzipped content received')
stream = self.gunzip(s)
else:
stream = s
stream.seek(0)
on_success(r.geturl(), stream, info)
except Exception, e:
on_failure(url, e)

  • старый метод open принимает дополнительные заголовки.
  • новый метод visit читает данные и "заворачивает" их в StringIO
  • результат возвращается в callback-функции on_success и on_failure.

Почему используются callback-функции?

В случае успешного запроса, могут понадобиться три результата:
  1. содержание страницы
  2. заголовки ответа
  3. адрес, с которого были получены результаты (в случае редиректов он не совпадает с исходным адресом)
Можно, конечно, вернуть список результатов, но по-моему, это не очень красиво и удобно с точки зрения поддержки. Нехорошо когда функция возвращает больше одного значения. И не всегда будут нужны сразу три результата. В последнем случае callback-функция может быть объявлена без указания лишних аргументов:
def handle_success(*args):
ctype = args[2].get('Content-Type')
...

Мне это больше нравится, чем:
results = visit(аргументы)
ctype = results[2].get('Content-Type')
Впрочем, дело вкуса...

Старый метод open теперь снаружи практически не нужен.

Комментариев нет: