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

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

Пишем простой парсер на Scrapy

3 декабря 2017 г.

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

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

pip install scrapy

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

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 вы можете следующей командой из корня проекта:

scrapy crawl <spider_name>

scrapy crawl pycoder

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

 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(этот класс - индикатор текущей страницы).

Далее так отредактируем файлик pycdoer/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.

Метки

python
Если вам понравился пост, можете поделиться им в соцсетях: