30 янв. 2009 г.

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

Как работает базовая аутентификация

Наряду с аутентификацией при помощи формы, существует и более простая -- так наз. "базовая аутентификация":

  • При первой попытке зайти на сайт сервер проверяет, если среди заголовков запроса поле 'Authorization'. Если нет или там содержится неверное значение, возвращается ошибка 401.
  • Браузер открывает всплывающее окошко для ввода имени пользователя логин и пароля
  • Пользователь вводит имя и пароль, нажимает OK и браузер делает повторный запрос, вставив в заголовок запроса: Authorization: Basic данные.
  • Сервер проверяет логин с паролем и если все в порядке, возвращает запрошенную страницу с кодом 200.

Пользователей заводит администратор веб-сервера. Их имена и пароли заносятся в конфигурацию. Они могут быть зашифрованы или храниться в виде текста -- зависит от требований к безопасности.

Эта защита считается не самой надежной и применяется чаще всего в технических целях. Например, чтобы закрыть доступ к порталу всем, кроме заказчиков, разработчиков и тестировщиков, пока не закончена работа.


В одной из заметок был продемонстрирован краулер, способный обходить сайт и проверять, все ли ссылки правильно работают. Поскольку использоваться он будет чаще всего на этапе тестирования сайтов, ему недостает умения заходить на защищенные таким способом страницы.

Базовая аутентификация и urllib2

Для базовой аутентификации средствами питоновской библиотеки urllib2 нужно использовать два встроенных класса:
  • HTTPPasswordMgr
  • HTTPBasicAuthHandler
Первый позволяет задавать имя пользователя и пароль, второй добавляется в набор handler-ов (обработчиков запроса) для специализированного экземпляра OpenDirector (см. подробности).

Простейший вариант HTTPPasswordMgr

class KnownPasswordMgr(HTTPPasswordMgr):
"""
Хранит заранее заданную пару логин/пароль
"""
def __init__(self, username, password):
HTTPPasswordMgr.__init__(self)
self.username = username
self.password = password

def find_user_password(self, realm, authuri):
"""
Возвращает заранее известную пару значений
"""
retval = HTTPPasswordMgr.find_user_password(self, realm, authuri)
if not (retval[0] or retval[1]):
return (self.username, self.password)

return retval
Этот класс рассчитан на использование одной единственной пары имя/пароль.

HTTPBasicAuthHandler

В конструктор класса UserAgent добавим необязательный параметр: credentials. Если он присутствует, к набору handler-ов будет добавлен HTTPBasicAuthHandler.

class UserAgent(object):
"""
Краулер.

Именованные аргументы конструктора и значения по умолчанию:
agentname -- имя ('Test/1.0')
email -- адрес разработчика (пустая строка)
hdrs -- словарь HTTP-заголовков (DEFAULT_HEADERS)
ignore_robots -- True, если следует игнорировать robots.txt (False)

credentials -- словарь с ключами 'логин' и 'пароль', если нужна
базовая аутентификация (None).
"""

def __init__(self,
agentname=DEFAULT_AGENTNAME,
email=DEFAULT_EMAIL,
hdrs=None,
ignore_robots=False,
credentials=None):

if not hdrs:
hdrs = {}
self.agentname = agentname
self.email = email

self.cookies_handler = SessionCookieHandler()
handlers = [ self.cookies_handler, ]
if not ignore_robots:
handlers.append(RobotsHTTPHandler(self.agentname))

if credentials:
handlers.append(
HTTPBasicAuthHandler(KnownPasswordMgr(**credentials))
)

self.opener = urllib2.build_opener(*handlers)

# переопределение заголовков по умолчанию
headers = copy(DEFAULT_HEADERS)
headers.update(hdrs)
op_headers = [ (k, v) for k, v in headers.iteritems() ]
op_headers.append(('User-Agent', self.agentname))
# если email не задан, HTTP-заголовок 'From' не нужен
if self.email:
op_headers.append(('From', self.email))

self.opener.addheaders = op_headers
Вот, собственно, и все. TestCase для новой функции может выглядеть так:

class TestAuth(unittest.TestCase):
def setUp(self):
self.crawler = crawler.UserAgent(
agentname='Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)',
ignore_robots=True,
credentials = dict(username='ЛОГИН', password='ПАРОЛЬ')
)

def _on_success(self, *args):
self.assertTrue(1)

def _on_failure(self, url, err):
self.fail('Authentication failed: %s' % err)

def test_authentication(self):
self.crawler.visit('АДРЕС САЙТА',
on_success=self._on_success,
on_failure=self._on_failure )
Здесь, в отличие от авторизации через форму, в случае успешного ответа не надо дополнительно проверять содержание страницы. В случае ошибки будет возвращаться все тот же код 401, т.е. функция обратного вызова _on_success не будет вызвана.

6 комментариев:

Nigma комментирует...

Прочитал все 12 статей, очень понравилось. Только не нашел собранных воедино исходников краулера.

Nigma комментирует...

Сам планирую написать краулер по ежедневному анализу форума.

Наувул-Наувул комментирует...

Спасибо за отзыв, рад, если что-то пригодится.

К сожалению, в блоггер неудобно помещать исходники. Как освобожусь немного, сделаю отдельный сайт, куда помещу и исходники и готовый к установке пакет.

Nigma комментирует...

Мне интересно Вы писали "В 2007 году, когда я писал краулера на Питоне для поискового проекта, именно отсутствие надежного HTML-парсера заставила меня пересесть на Perl. Ни SGMLParser ни HTMLParser из стандартных библиотек не в состоянии справиться со страницами, выходящими за рамки академического гипертекста."

Так какая библиотека способна понять все многообразие интернета ? Я думаю, что наверно из движка Mozill-ы если только собранная.

Nigma комментирует...

То есть я предполагаю, в инете найти подобное хотя сам не искал. Даже думал над тем, что бывают парсеры с векторной отладкой. Наподобие дай мне координаты Rect элемента на предполагаемой странице браузера. Это наверно мечты)))

Наувул-Наувул комментирует...

Кажется, html5lib вполне справляется c парсингом. Я знаю, что есть еще библиотека python-spidermonkey, которая понимает JavaScript на страницах. А вот что касается "векторной отладки" -- не уверен, что существует нечто подобное, задача-то явно нетривиальная.