Чтобы исследовать тексты, их нужно откуда-то брать, и в немалом количестве. Программы, добывающие ресурсы из Всемирной Паутины, называются краулерами (от английского crawl -- ползать) или пауками.
Бумажный кораблик
Существуют готовые продукты, способные выдерживать промышленные нагрузки и снабжать данными поисковые системы. Например, Nutch -- краулер, использующийся с открытой поисковой системой Lucene. Однако изучать такие системы -- все равно что получать новую специальность и использовать их для относительно скромных задач -- стрельба из пушки по воробьям.С другой стороны, современные языки программирования предоставляют готовые библиотеки, позволяющие свести задачу скачивания документа к одной строчке кода. Например, на Питоне:
import urllib
tmpfile, headers = urllib.urlretrieve('http://www.python.org/')
tmpfile, headers = urllib.urlretrieve('http://www.python.org/')
- многие документы обрываются в самом начале;
- неоправданно большое количество сообщений об ошибках, в том числе с ресурсов, которые прекрасно видны через браузер;
- диск заполнен мусором: вместо текстов пришли какие-то бинарные файлы из каких-то неприкаянных потоков;
- краулер всю ночь провисел в ожидании ответа от какого-то хоста;
- компьютер впал в ступор после того как процесс скушал все доступные ресурсы;
- ваш IP-адрес заблокирован и помещен в черные списки веб-мастеров, потому что краулер ходит куда не положено и делает запросы с частотой дятла;
Между тем, Python позволяет вырастить вполне жизнеспособного паучка -- пускай не промышленного уровня, но вполне пригодного для прототипов и решения частных задач, таких как исследование и обработка текстов. Достаточно порыться в стандартной документации и исходном коде библиотек -- первой, увы, не всегда хватает.
Вот предварительные требования к программе:
- Переносимость. Модуль должен работать одинаково из под разных операционных систем и по возможности ограничиваться стандартными библиотеками.
- Компактность. Очень не хочется городить очередной фреймворк, который к концу проекта будет провисать под собственным весом. Достаточно того, что urllib2 -- скорее не библиотека, а Java-образный фреймворк.
- Вежливость. Вежливыми принято называть краулеры, лояльные к 'robots.txt'. Так называется специальный файл, где веб-мастера объявляют правила поведения пауков на сайте. Скажем: таком-то краулеру не ходить в раздел '/news', никому не ходить в /weather/... Пример можно увидеть прямо через браузер: http://tv.yandex.ru/robots.txt .
Начнем, как водится, с конца
Простейший способ использования будет из командной строки:python crawler.py URL
Какого рода могут быть ошибки? Их можно свести к нескольким категориям:
- Соединение не состоялось. Например, кошка поиграла с сетевым кабелем и сети не стало. Или вместо http:// задано реез://. Или сервер долго не отвечает.
- Соединение состоялось, но посещение страницы запрещено robots.txt
- Соединение состоялось, но сервер вместо запрошенных данных вернул код ошибки (401 -- "требуется авторизация", 404 -- "документ не найден", 500 -- "внутренняя ошибка" и т.д ).
- Вместо целой страницы пришла только ее часть
- используется только HTTP-протокол
- интересны только html -документы и простой текст
#!/usr/bin/python
# -*- coding utf8 -*-
#########################################################################
# UserAgent tests
# author: Sergey Krushinsky
# created: 2008-12-28
#########################################################################
import sys, os
import urllib2
import unittest
import crawler
class TestUserAgent(unittest.TestCase):
def setUp(self):
self.crawler = crawler.UserAgent()
def tearDown(self):
pass
def test_default_agentname(self):
"""
Если имя не задано в конструкторе, он должно соответствовать
имени по умолчанию.
"""
msg = "Default agent name should be '%s', not '%s'" % \
(crawler.DEFAULT_AGENTNAME, self.crawler.agentname)
self.assertEqual(self.crawler.agentname, crawler.DEFAULT_AGENTNAME, msg)
def test_custom_agentname(self):
"""
Если имя задано в конструкторе, оно должно таким и быть.
"""
name = 'Other Test/2.0'
c = crawler.UserAgent(agentname=name)
self.assertEqual(
c.agentname,
name,
"Custom agent name should be '%s', not '%s'" % \
(name, c.agentname))
def test_htmlget(self):
"""
Краулер открывает заданный ресурс и в заголовке ответа возвращается
text/html.
"""
resp = self.crawler.open('http://spintongues.msk.ru/kafka2.html')
ctype = resp.info().get('Content-Type')
# В заголовке может быть что-нибудь вроде 'text/html; charset=windows-1251',
# поэтому обычное сравнение не подходит.
self.assert_(ctype.find('text/html') != -1, 'Not text/html')
def test_urlerror(self):
"""
Если задан неверный адрес, должны генерироваться ошибка IOError.
"""
self.assertRaises(IOError, self.crawler.open, 'http://foo/bar/buz/a765')
def test_robotrules(self):
"""
Если выяснилось, что robots.txt запрещает посещение адреса,
должно генерироваться исключение.
"""
# Яндекс, как известно, не любит пауков
self.assertRaises(
RuntimeError,
self.crawler.open,
'http://yandex.ru/')
if __name__ == '__main__':
suite = unittest.TestLoader().loadTestsFromTestCase(TestUserAgent)
unittest.TextTestRunner(verbosity=2).run(suite)
Пояснения к тестам:
- существует пакет по имени crawler и в нем -- класс UserAgent. Почему 'UserAgent', а не 'Crawler' или 'Spider'? Потому что последние два имени скорее ассоциируются с обходом сети. Позже мы решим и эту задачу.
- Экземпляр может быть создан без аргументов либо с аргументом 'agentname'. Это имя включается в заголовки HTTP-запроса, а кроме того, используется при анализе robots.txt.
- Основной рабочий метод -- open(адрес). Почему не 'get'?-- потому что сам UserAgent не читает содержание страницы, он открывает ее, возвращая 'file-like object', проверять и обрабатывать который будет уже кто-то другой.
- При попытке открыть страницу с несуществующего адреса, выбрасывается исключение IOError.
- Если пауку вход запрещен, генерируется RuntimeError.
(продолжение следует)
5 комментариев:
хм... странность какая-то, но у меня нет модуля crawler в репозиториях :(
подскажите как быть?
А модуля crawler на этом этапе еще не было. Его предстояло написать. Существует такая практика: вначале писать набор тестов к запланированному модулю, а потом уже сам модуль. То, что вы видите, и есть тест.
Спасибо за разъяснение, буду дальше разбираться =)
Спасибо за очень и очень интересный и позновательный цикл, надеюсь что будет продолжение или какой-либо новый цикл )))))
Спасибо огромное за интересный цикл публикаций, они очень помогают разобраться новичку,как я :)
Очень рад, что эти заметки оказались полезными. Не исключено, что напишу продолжение, за прошедшее время появились кое-какие наработки, надо только отвлечься от текучки и собраться с мыслями.
Отправить комментарий