Python 3 парсинг scrapy

Блог питониста

Изображение гика

Одна из часто встречающихся задач — парсинг каких-либо сайтов. Для этой цели удобно использовать фреймворк Scrapy. Этот open source’ный фреймворк построен на базе асинхронной библиотеки Twisted, поэтому он сам асинхронный, это значит, что можно отсылать реквесты, не дожидаясь ответа сервера на предыдущие запросы, что существенно ускоряет парсинг, если сравнивать с синхронным подходом.

Установить Scrapy можно через pip:

Для примера я напишу парсер своего же блога. Сначала необходимо создать проект:

scrapy startproject pycoder , где pycoder — название проекта.

Эта команда создаст папку pycoder следующего вида:

pycoder/ | scrapy.cfg - конфигурационный файл для деплоя | pycoder/ - python - модуль проекта, здесь будет лежать код | __init__.py | items.py - файлик с классом, описывающим то, что вы будете парсить | middlewares.py - описывается класс отвечающий за промежуточную обработку данных | pipelines.py - описывается как будет обработано и выведено то, что вы парсите | settings.py - настройки | spiders/ - папка для "пауков", это тот код, который собственно отвечает за парсинг | __init__.py

Создание паука

Spider’ы — это классы, которые вы определяете, а Scrapy использует для извлечения данных с сайта. Эти классы должны наследоваться от scrapy.Spider, в них должны быть описаны начальные запросы, как идти по ссылкам на страницах и какую информацию извлекать со страниц.

Cоздадим файлик pycoder_spider.py в папке pycoder/spiders следующего содержания:

 1 # python 3 2 import scrapy 3 from urllib.parse import urljoin 4 5 6 class PycoderSpider(scrapy.Spider): 7 name = "pycoder" 8 start_urls = [ 9 'http://pycoder.ru', 10 ] 11 12 def parse(self, response): 13 for post_link in response.xpath( 14 '//div[@class="post mb-2"]/h2/a/@href').extract(): 15 url = urljoin(response.url, post_link) 16 print(url) 

На строке 7 задается важный параметр — название spider’a, по этому названию spider будет запускаться. Атрибут класса start_urls — это список url’ов, которые будут использованы для начальных реквестов. Для заданного url’a будет вызван метод parse, в котором мы получаем все ссылки на конкретные посты на главной странице. Xpath — это язык для выбора опреденных элементов из html — кода. На строках 13, 14 я отбираю div’ы, имеющие классы post и mb-2, затем беру ссылки из h2 — элементов внутри div’ов.

Запустить этот spider вы можете следующей командой из корня проекта:

Если вы запустите этот спайдер, то увидите ссылки на конкретные посты, но не на все, а только на посты с главной страницы, чтобы пройтись по всем страницам и получить ссылки на все посты нужно изменить код нашего паука:

 1 # python 3 2 import scrapy 3 from urllib.parse import urljoin 4 5 6 class PycoderSpider(scrapy.Spider): 7 name = "pycoder" 8 start_urls = [ 9 'http://pycoder.ru/?page=1', 10 ] 11 visited_urls = [] 12 13 def parse(self, response): 14 if response.url not in self.visited_urls: 15 self.visited_urls.append(response.url) 16 for post_link in response.xpath( 17 '//div[@class="post mb-2"]/h2/a/@href').extract(): 18 url = urljoin(response.url, post_link) 19 print(url) 20 21 next_pages = response.xpath( 22 '//li[contains(@class, "page-item") and' 23 ' not(contains(@class, "active"))]/a/@href').extract() 24 next_page = next_pages[-1] 25 26 next_page_url = urljoin(response.url+'/', next_page) 27 yield response.follow(next_page_url, callback=self.parse) 

На строке 9 я изменил стартовый урл, чтобы спайдер не заходил дважды на первую страницу. На строке 11 я добавил атрибут класса visited_urls, где хранятся уже посещенные страницы, это необходимо сделать, иначе спайдер может несколько раз зайти на одну страницу. На строках 21-27 я ищу ссылку на следующую страничку, и для этой следующей странички рекурсивно вызываю функцию parse. Эта ссылка должна иметь класс page-item и не иметь класс active(этот класс — индикатор текущей страницы).

Далее так отредактируем файлик pycoder/items.py :

1 import scrapy 2 3 4 class PycoderItem(scrapy.Item): 5 title = scrapy.Field() 6 body = scrapy.Field() 7 date = scrapy.Field() 

Мне нужен только title и body и date у каждого поста. Далее небходимо не просто принтить ссылки на посты, но и переходить по ним, парсить их, поэтому нужно добавить вызов еще одной функции, которая будет отвечать за парсинг поста:

 1 # python 3 2 from pycoder.items import PycoderItem 3 4 import scrapy 5 from urllib.parse import urljoin 6 7 8 class PycoderSpider(scrapy.Spider): 9 name = "pycoder" 10 start_urls = [ 11 'http://pycoder.ru/?page=1', 12 ] 13 visited_urls = [] 14 15 def parse(self, response): 16 if response.url not in self.visited_urls: 17 self.visited_urls.append(response.url) 18 for post_link in response.xpath( 19 '//div[@class="post mb-2"]/h2/a/@href').extract(): 20 url = urljoin(response.url, post_link) 21 yield response.follow(url, callback=self.parse_post) 22 23 next_pages = response.xpath( 24 '//li[contains(@class, "page-item") and' 25 ' not(contains(@class, "active"))]/a/@href').extract() 26 next_page = next_pages[-1] 27 28 next_page_url = urljoin(response.url+'/', next_page) 29 yield response.follow(next_page_url, callback=self.parse) 30 31 def parse_post(self, response): 32 item = PycoderItem() 33 title = response.xpath( 34 '//div[contains(@class, "col-sm-9")]/h2/text()').extract() 35 item['title'] = title 36 body = response.xpath( 37 '//div[@class="block-paragraph"]//p/text()').extract() 38 39 item['body'] = body 40 date = response.xpath( 41 '//div[contains(@class, "col-sm-9")]/p/text()').extract() 42 item['date'] = date 43 yield item 

Добавил функцию parse_post, в ней я парсю старницы постов. Запустить парсер с генерацией csv можно так:

scrapy crawl pycoder -o output.csv -t csv

Если все нормально, то в корне проекта будет создан файл output.csv

Если вам нужен json на выходе, то:

scrapy crawl pycoder -o output.json

При проблеме с кодировкой установите настройку FEED_EXPORT_ENCODING в settings.py:

1 FEED_EXPORT_ENCODING = 'utf-8' 

Заключение

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

Источник

Собираем данные с помощью Scrapy

scrapy

Без проблем установил из репозитариев 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&region=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
  • поиск и извлечение данных их HTML и XML
  • преобразование данных перед экспортом
  • экспорт в форматы JSON, CSV, XML
  • скачивание файлов
  • расширение фреймворка собственными middlewares, pipelines
  • выполнение POST запросов, поддержка куков и сессий, аутентификации
  • подмена user-agent
  • shell консоль для отладки
  • система логирования
  • мониторинг через Web-интерфейс
  • управление через Telnet-консоль

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

Рабочий пример выложил на GitHub.

Источник

Читайте также:  Uncaught exception handler php
Оцените статью