Пользователь
0,0
рейтинг
18 марта 2011 в 23:13

Разработка → Собираем данные с помощью Scrapy

Здесь уже проскакивали вскользь упоминания об этом фреймворке для сбора данных. Инструмент действительно мощный и заслуживает большего внимания. В этом обзоре я расскажу, как

scrapy

  • создать паука, выполняющего GET запросы,
  • извлекать данные из HTML документа,
  • обрабатывать и экспортировать данные.






Установка



Требования: Python 2.5+ (3-я ветка не поддерживается), Twisted, lxml или libxml2, simplejson, pyopenssl (для поддержки HTTPS)

Без проблем установил из репозитариев Ubuntu. На странице Installation guide описывается установка в других дистрибутивах Linux, а так же в Mac OS X и Windows.

Задача



Наверное, кому-то захочется распарсить интернет-магазин и стянуть оттуда весь каталог с описаниями товара и фотографиями, но я намеренно не стану этого делать. Возьмем лучше какие-нибудь открытые данные, к примеру, список учебных заведений. Сайт является достаточно типовым и на нем можно показать несколько приемов.

Прежде чем писать паука, надо осмотреть сайт-источник. Заметим, сайт построен на фреймах (?!), во фреймсете ищем фрейм со стартовой страницей. Здесь присутствует форма поиска. Пусть нам нужны только вузы Москвы, поэтому заполняем соответствующее поле, жмем «Найти».

Анализируем. У нас есть страница с ссылками пагинации, 15 вузов на страницу. Параметры фильтра передаются через GET, меняются лишь значение page.

Итак, сформулируем задачу:

  1. Перейти на страницу abitur.nica.ru/new/www/search.php?region=77&town=0&opf=0&type=0&spec=0&ed_level=0&ed_form=0&qualif=&substr=&page=1
  2. Пройтись по каждой странице с результатами, меняя значение page
  3. Перейти в описание вуза abitur.nica.ru/new/www/vuz_detail.php?code=486®ion=77&town=0&opf=0&type=0&spec=0&ed_level=0&ed_form=0&qualif=&substr=&page=1
  4. Сохранить детальное описание вуза в CSV-файле


Создание проекта



Переходим в папку, где будет располагаться наш проект, создаем его:

scrapy startproject abitur
cd abitur


В папке abitur нашего проекта находятся файлы:

  • items.py содержит классы, которые перечисляют поля собираемых данных,
  • pipelines.py позволяет задать определенные действия при открытии/закрытии паука, сохранения данных,
  • settings.py содержит пользовательские настройки паука,
  • spiders — папка, в которой хранятся файлы с классами пауков. Каждого паука принято писать в отдельном файле с именем name_spider.py.


Паук



В созданном файле spiders/abitur_spider.py описываем нашего паука

class AbiturSpider(CrawlSpider):

    name = "abitur"
    allowed_domains = ["abitur.nica.ru"]
    start_urls = ["http://abitur.nica.ru/new/www/search.php?region=77&town=0&opf=0&type=0&spec=0&ed_level=0&ed_form=0&qualif=&substr=&page=1"]

    rules = (
             Rule(SgmlLinkExtractor(allow=('search\.php\?.+')), follow=True),
             Rule(SgmlLinkExtractor(allow=('vuz_detail\.php\?.+')), callback='parse_item'),
             )

    "..."


Наш класс наследуется от CrawlSpider, что позволит нам прописать шаблоны ссылок, которые паук будет сам извлекать и переходить по ним.

По порядку:

  • name — имя паука, используется для запуска,
  • allowed_domains — домены сайта, за пределами которого пауку искать ничего не следует,
  • start_urls — список начальных адресов,
  • rules — список правил для извлечения ссылок.


Как вы заметили, среди правил параметром передается callback функция. Мы к ней скоро вернемся.

Элементы



Как я уже говорил, в items.py содержится классы, которые перечисляют поля собираемых данных.
Это можно сделать так:

class AbiturItem(Item):

    name = Field()
    state = Field()

    "..."


Распарсенные данные можно обработать перед экспортом. К примеру, учебное заведение может быть «государственное» и «негосударственное», а мы хотим хранить это значение в булевом формате или дату «1 января 2011» записать как «01.01.2011».

Для этого существуют входные и выходные обработчики, поэтому поле state запишем по-другому:

class AbiturItem(Item):

    name = Field()
    state = Field(input_processor=MapCompose(lambda s: not re.match(u'\s*не', s)))
    
    "...."


MapCompose применяется к каждому элементу списка state.

Поиск элементов на странице



Возвращаемся к нашему методу parse_item.

Для каждого элемента Item можно использовать свой загрузчик. Его назначение тоже связано с обработкой данных.

class AbiturLoader(XPathItemLoader):
    default_input_processor = MapCompose(lambda s: re.sub('\s+', ' ', s.strip()))
    default_output_processor = TakeFirst()

class AbiturSpider(CrawlSpider):

    "..."

    def parse_item(self, response):
        hxs = HtmlXPathSelector(response)

        l = AbiturLoader(AbiturItem(), hxs)
        l.add_xpath('name', '//td[@id="content"]/h1/text()')
        l.add_xpath('state', '//td[@id="content"]/div/span[@class="gray"]/text()')	
	"..."
        return l.load_item()


В нашем случае из каждого поля удаляются крайние и дублирующиеся пробелы. В загрузчик также можно добавить индивидуальные правила, что мы делали в классе AbiturItem:

class AbiturLoader(XPathItemLoader):
    "..."
    state_in = MapCompose(lambda s: not re.match(u'\s*не', s))


Так что, поступайте как вам удобнее.

Функция parse_item() возвращает объект Item, который передается в Pipeline (описываются в pipelines.py). Там можно написать свои классы для сохранения данных в форматах, не предусмотренных стандартным функционалом Scrapy. Например, экспортировать в mongodb.

Поля этого элемента задаются с помощью XPath, о котором можно прочитать здесь или здесь. Если вы используйте FirePath, обратите внимание, что он добавляет тег tbody внутрь таблицы. Для проверки путей XPath используйте встроенную консоль.

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

Запуск



Исходный код можно взять тут, для запуска перейдите в папку с проектом и наберите в консоли

scrapy crawl abitur --set FEED_URI=scraped_data.csv --set FEED_FORMAT=csv


Вкратце, я все описал, но это лишь малая часть возможностей Scrapy:
  • поиск и извлечение данных их HTML и XML
  • преобразование данных перед экспортом
  • экспорт в форматы JSON, CSV, XML
  • скачивание файлов
  • расширение фреймворка собственными middlewares, pipelines
  • выполнение POST запросов, поддержка куков и сессий, аутентификации
  • подмена user-agent
  • shell консоль для отладки
  • система логирования
  • мониторинг через Web-интерфейс
  • управление через Telnet-консоль


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

Рабочий пример выложил на GitHub.
Александр @bekbulatov
карма
43,2
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (49)

  • +8
    Ну вот, а я только сегодня реализовывал всё на BS и urllib2 =)
    Даешь еще статей по питоновским/джанговским либам на Хабре!

    Спасибо за пост =)
  • +6
    Какая штука классная, моим велосипедам очень стыдно теперь.
  • 0
    Что делать если js на странице возвращает контент?
    • 0
      Наследуете паука от BaseSpider и пишете реквесты вручную
      yield Request(url, callback=self.parse_page)
      • 0
        а веб-движок не проще прицепить?
        • НЛО прилетело и опубликовало эту надпись здесь
          • 0
            «офисный» ботнет решает :)
          • 0
            Есть, используем spynner. Тормоз, конечно, но работает.
    • 0
      Разбираешь что делает JS и реализуешь этот алгоритм в пауке. Вряд-ли там что-то сверхсложное будет.
    • 0
      Если там AJAX-запрос, возвращающий структурированные в JSON/XML/whatever данные — просто пишем Request на адрес хэндлера, входные параметры придётся упаковывать вручную, с результатом делаем что хотим. Если запрос возвращает кусок HTML, предназначенный для встраивания в разметку — можно использовать прямо XPathItemLoader (правда, если у этого куска несколько элементов верхнего уровня, придётся обернуть в фиктивный элемент, иначе парсер расстроится).
      С частичными постбэками ASP.NET, возможно, будет сложнее.
  • 0
    списибо, было интересно почитать. раньше для этих целей ковырял mechanize и Beautiful Soup, хотел еще посмотреть lxml/leaf чтобы выбрать что будет удобней, но руки так и не дошли.
  • –6
    Так же для этих целей можно использовать nodejs. правда он работает под GNU Linux но если напильником поработать то и на винде запустится. Очень удобно пауков на нём писать! Большой плюс, то что он может превращать html код в DOM модель и работать с ним через jquery. Тоесть вытащить любую часть страницы не занимает много труда! Так же к нему написано много дополнений, с помощью них можно работать через прокси и сохранять сессии с куками! Вообщем советую глянуть и в этом направлении тоже! С удовольствием выслушаю конструктивную критику или поддержку!
    • 0
      А еше гвозди микроскопом забивать удобно.
    • 0
      Из конструктивной критики — насколько мне известно, NodeJS это больше все-таки для создания веб-приложений. А тут описывается готовый специальный очень мощный, работающий в продакшне фреймворк конкретно для скачивания страниц и их разбора.
      Могли-бы хоть список библиотек дать с которыми можно так просто в NodeJS странички качать, обрабатывать, сохранять данные. Превращать html в DOM и выполнять xpath/CSS запросы все умеют. Куки и прокси Scrapy из коробки поддерживает.
      • 0
        NodeJS это в первую очередь надстройка надо V8, а уж что вы с ним будете делать это зависит от вашей фантазии и знания JS. Г-н kurokikaze несколько раз демонстрировал примеры своих «игр» с хабром и во всех случаях в качестве движка для пауков выступало что-то на базе nodeю

        Лично мне понравилась связка: node-htmlparser + node-soupselect. Во всяком случае вменяемый парсинг аукционов с vworker.com, без regexp и прочих «не тру» методов (для интереса рекомендую взглянуть на исходный код их аукционов и проникнуться даже не индийским, а каким-то нечеловеческим кодом), удалось написать только на ней. Под питоном честно пытался сделать это с помощью BeautifulSoup, pyquery и недавно рекламировавшегося на хабре leaf. Все потерпели неудачу. Возможно, стоило написать собственную обертку над lxml, где учесть все недостатки целевого сайта, но мне показалось, что возни будет больше, чем просто написать под node.
        • +1
          Не, ну если знаете Node лучше, чем Twisted/Scrapy то пишите на нем. Хотя XPath запросы везде должны быть одинаковые, не вижу здесь проблемы)))
          Просто Sсrapy это не «еще одна библиотека для ПАРСИНГА страничек». Это фреймворк для написания веб-пауков. В Scrapy при желании можно использовать любую библиотеку для парсинга. Я использую lxml, хотя по-умолчанию там libxml2.

          Фишка Scrapy в том, что он не говорит, что «Twisted это в первую очередь надстройка над Python. А то, какого вы паука на нем напишете зависит от вашей фантазии и знания Python». Он говорит — скачиванием страничек я занимаюсь сам, диспетчеризацией данных, очередью запросов, приоритетами, обработкой исключений, поиском ссылок, защитой от зацикливания, защитой от того, что странички "/catalog.php?p1=1&p2=2" и "/catalog.php?p2=2&p1=1" это одно и то же, удаленная консоль, отладка памяти, REST интерфейс для управления, логгирование, генератор кода, отличная документация и куча других штуковин. Причем все это можно легко отключить или доработать. Есть готовая инфраструктура, вам остается только написать стартовую страницу, XPath запросы и (опционально) механизм для сохранения результатов (только если нужно что-то сложнее CSV/XML — для этих есть уже готовые экспортирующие конвейеры).
  • 0
    Очень интересно хотелось бы больше информации

    По вот этой части:
    >>выполнение POST запросов, поддержка куков и сессий, аутентификации

    Раньше я для этого использовал mechanize — конкретно авторизация, заполнение и отправка форм, получение данных из форм редактирования.

    Scrapy позволяет это?
    • 0
      да, вконтакте им ок парсится, делал бота раньше
    • 0
      Да, все это можно. Делается так:
      FormRequest.from_response(response,
                          formdata={'username': 'john', 'password': 'secret'},
                          callback=self.after_login)


      Scrapy также следует редиректам, хранит куки, устанавливаемые с сервера (иногда нужно отключать).
      JavaScript код не выполняет, поэтому если сайт на Ajax, запросы необходимо прописывать вручную.
      И все это из коробки.
    • 0
      По сравнению с Mechanize есть одно очень важное преимущество — производительность. Scrapy работает поверх Twisted — асинхронного фреймворка. Поэтому держать по тысячи соединений одновременно вполне реально.
      • 0
        Памяти дофига нужно.
        • 0
          Ну, гораздо меньше, чем понадобилось бы при использовании потоков.
  • 0
    Мне понравилось, как раз искал подобные темы
  • 0
    Эта штука умеет запускать паука и парсер многопоточно? Это true-многопоточность (noGIL)?
    • +1
      Там Twisted внутри.
    • НЛО прилетело и опубликовало эту надпись здесь
    • 0
      Оно НЕ умеет многопоточность. Оно на асинхронных сокетах. (На всякий случай уточню — это плюс). Я им парсил 20 форумов разных, на каждый форум по 4 одновременных соединения. Полет нормальный.
      • 0
        А в чем плюс? Помимо самого скачивания есть же еще и парсинг кода для поиска ссылок с целью их обхода, отномающий не мало процессорного времени. В итоге вы упретесь в процессор, вместо того чтобы упереться в пропускную способность сети.
        • +1
          Ну так при одинаковом количестве страничек в секунду расход процессора будет один и тот же на многопоточном и асинхронном спайдере. Но первый будет жрать заметно больше памяти (плюс оверхед на переключения контекста потоков) ну и усложнение кода из-за необходимости всяких блокировок, очередей и пр.
          Упирался в процессор только когда на одном из форумов напарывался на 10Мб страничку. При ее разборе блокировался весь паук и ядро процессора нагружалось на 100%. Но я сомневаюсь что использование многопоточного паука решило бы эту проблему. Он так-же уперся бы в процессор.

          Хотя идеальный паук в моем видении должен использовать асинхронные сокеты для скачивания и пул потоков (еще лучше — пул процессов) для парсинга. Scrapy такой подход не реализует к сожалению и я не смог найти способа просто реализовать это на базе Scrapy. Это конечно жаль.
  • 0
    Поиграл в IE9 — тормозов не заметил.
    • +2
      Блин, думаю, что минусуют, я не в ту тему коммент оставил, извиняюсь.
  • 0
    Хорошая статья, сам похожую хотел давно написать.

    От себя добавлю что использую его на работе, очень доволен. Высокая производительность, легко расширять и подстраивать под свои нужды, отличная документация.

    Тем кто работал с Django особенно понравится, т.к. по структуре они очень похожи.

    Ну и по поводу написания пауков — я их пишу совсем по другому. У меня пауки наследуются не от CrawlSpider а от BaseSpider. Т.е. я сам формирую Request-ы а не доверяю это SgmlLinkExtractor-у. Немного больше кода, но гораздо больше контроля. И XPathItemLoader-ы я не использую, заполняю Item ручками из XPath, не знаю почему, может они недавно появились…
    • 0
      По мне, лучше использовать CrawlSpider, когда это возможно. Если сайт сделан нормально, не вижу смысла не доверять. А про BaseSpider думаю написать отдельную статью.

      О XPathItemLoader написано еще в 0.7 версии. Я решил показать, что они есть. Хоть они иногда ограничивают возможности, мне нравятся тем что они отделяют парсинг и валидацию. Еще extract() возвращает список, в котором обычно нужен только первый элемент. Это удобно сделать с помощью TakeFirst, прописав как дефолтный процессор.
  • +1
    Тема интересна, прошу продолжения)
    Если есть возможность — киньте ссылки на статьи\туториалы по Scrapy, кроме родного сайта, которые считаете интересными\полезными.
  • 0
    Спасибо за статью!
    Возник один вопрос.
    Не могу разобраться, как настроить краулинг через прокси… Кто-нибудь сталкивался?
    • 0
      В доке поиск есть. А вопрос есть в FAQ.
      HttpProxyMiddleware.
      • 0
        Знаю, что HttpProxyMiddleware.
        Только по тому, что написано в доке — не разберусь как его применить. Как указать список прокси?
        • 0
          Список? Вы хотите ротацию? Мидлварь это не умеет. Впрочем, косвенно из её доки, а прямо из её кода и кода scrapy.core.downloader.webclient.ScrapyHTTPClientFactory._set_connection_attributes() следует, что достаточно выставить request.meta['proxy'] в URL прокси.
          • 0
            Да, хотелось ротацию. Но пока не могу задать даже один прокси )
            Где я могу получить доступ к request.meta['proxy'], если я использую rules и реквесты формирует SgmlLinkExtractor?
            • 0
              > Но пока не могу задать даже один прокси )
              Переменную окружения выставляли?

              > Где я могу получить доступ к request.meta['proxy'], если я использую rules и реквесты формирует SgmlLinkExtractor?
              Тогда видимо придётся писать свою мидлварь (это и универсальнее) на основе кусков HttpProxyMiddleware.
              • 0
                Спасибо за советы =)
                Написать свой мидлварь — это хорошая идея!
                Кстати, возможно, я нашел место где можно задать request.meta['proxy']. С помощью process_request (параметр конструктора класса Rule)
                • 0
                  Подскажите, пожалуйста, где задать request.meta['proxy'] при использовании CrawlSpider?
  • 0
    Не совсем понял, что делать, если какая-то страница содержит и блок данных и ссылки на другие данные,
    причом блок данных на текущей надо связать с данными на страницах по ссылкам.

    Например, на странице учебного курса есть его описание, а по ссылкам — отдельные лекции.
    Каждую лекцию надо связать с описанием курса, ну хотябы название курса всобачить.
    (из предположения, что на странице самой лекции даже название курса не указано)
    • 0
      Или например, так:
      example.edu/classes/:
      Лекции по физике


      example.edu/classes/phys/1
      Вводные



      И надо собрать все заголовки либо в разные свойства айтемов лекций, либо в одно списочное.

      Как быть?
      • 0
        1. Как в примере выше, написать Pipeline, который будет писать в 2 csv-файла: по ссылкам с лекциями и по ссылкам с вводными. Вместо csv можно использовать любое хранилище, сводить данные там же или отдельно.
        2. Передавать собранные данные на странице с лекциями в request на страницу с вводными.
  • 0
    Может кто подскажет:
    python.su/forum/topic/14240/
  • 0
    Я стесняюсь, но все таки предложу взглянуть на
    web harvester ( web-harvest.sourceforge.net )
    и на
    dirs.info/spider

    Тоже можно собирать информацию через XPATH

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.