Scrapy — Простой скрапинг сайтов

0
509

Scrapy является фреймворком, что прекрасно подойдет для скрапинга веб сайтов. Он без особых проблем справляется с самыми популярными случаями веб скрапинга, среди которых:

  • Многопоточность;
  • Веб-краулер для перехода от ссылке к ссылки;
  • Извлечение данных;
  • Проверка данных;
  • Сохранение в другой формат/базу данных;
  • Многое другое.

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

К недостаткам Scrapy можно отнести и тот факт, что начать ему обучаться бывает довольно сложно. Но мы ведь для этого здесь и собрались.

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

Обзор основ работы со Scrapy

Установить Scrapy можно через pip. Но будьте внимательны. В документации Scrapy настоятельно рекомендуется устанавливать его в специальной виртуальной среде во избежание конфликтов с пакетами вашей системы.

Я использую Virtualenv и Virtualenvwrapper:

python3.7 -m venv scrapy_env
source scrapy_env/bin/activate

После активации виртуальной среды, у вас в терминале должен изменится ввод команд. Например:

(scrapy_env):~$ pip list

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

pip install Scrapy

Теперь можно создать cкрапи проект с помощью этой команды:

scrapy startproject product_scraper

Это создаст все необходимые файлы для проекта:

├── product_scraper
│   ├── __init__.py
│   ├── __pycache__
│   ├── items.py
│   ├── middlewares.py
│   ├── pipelines.py
│   ├── settings.py
│   └── spiders
│       ├── __init__.py
│       └── __pycache__
└── scrapy.cfg

Далее представлено краткое описание файлов и папок:

  • items.py является моделью для извлеченных данных. Вы можете определить настраиваемую модель (например Product), что унаследует класс Item.
  • middlewares.py Middleware используется для изменения жизненного цикла запросов и ответов. Например, вы можете создать промежуточное программное обеспечение для ротации пользовательских агентов или использовать API вроде ScrapingBee вместо того, чтобы выполнять запросы самостоятельно.
  • pipelines.py В скрапи, пайплайны, или конвейеры используются для обработки извлеченных данных, очистки HTML, проверки данных, их экспорта в пользовательский формат или сохранения в базе данных.
  • /spiders является папкой, содержащей классы Spider. В Scrapy Spider являются классом, которые определяют, как должен быть проведен скрапинг сайта, в том числе по какой ссылке следовать и как извлечь данные для этих ссылок.
  • scrapy.cfg является файлом конфигурации для изменения некоторых настроек.

Скрапинг продуктов из интернет магазина на Python

В данном примере будет проведен скрапинг одного продукта с фиктивного сайта онлайн магазина. Вот первый товар, скрапингом которого мы займемся:

Scrapy — Простой скрапинг сайтов

https://clever-lichterman-044f16.netlify.com/products/taba-cream.1/

Мы извлечем название продукта, изображение, цену и описание.

Оболочка Scrapy Shell в командной строке

Scrapy поставляется со встроенной оболочкой, что помогает отладить код скрапинга в режиме реального времени. С ним можно быстро протестировать XPath-выражения или CSS селекторы. Это очень крутой инструмент для написания веб скраперов, и я всегда им пользуюсь!

Можно настроить Scrapy Shell на использование другой консоли вместо стандартной консоли Python как IPython. У вас появится возможность автозаполнения и другие приятные бонусы вроде цветного вывода.

Для его использования в оболочке Scrapy Shell нужно добавить следующую строку в файл scrapy.cfg:

shell = ipython

После завершения конфигурации можно использовать оболочку Scrapy:

[scrapy.core.engine] DEBUG: Crawled (404) <GET https://clever-lichterman-044f16.netlify.com/robots.txt> (referer: None)

В данном случае нет никакого robot.txt, поэтому мы получаем 404 HTTP код. Если бы у нас был файл robot.txt, по умолчанию Scrapy придерживался бы правил.

Можно отключить такое поведение, изменив boolean параметр в settings.py:

ROBOTSTXT_OBEY = True

После этого у вас должен появится лог подобного рода:

[scrapy.core.engine] DEBUG: Crawled (200) <GET https://clever-lichterman-044f16.netlify.com/products/taba-cream.1/> (referer: None)

Теперь можно увидеть объект ответа, заголовки ответа и попробовать различные XPath выражения и CSS селекторы, чтобы извлечь нужные данные.

Увидеть ответ прямо в браузере, можно использовав данную функцию:

view(response)

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

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

Извлечение данных с сайта через Scrapy

Scrapy по умолчанию не выполняет Javascript код. По этой причине при попытке сделать скрапинг на сайте, что использует Javascript-фреймворки вроде Angular или React.js, у вас могут возникнуть проблемы с получением доступа к запрашиваемым данным.

Попробуем использовать некоторые XPath выражения для извлечения названия и цены продукта:

Scrapy — Простой скрапинг сайтов

Для извлечения цены мы используем выражение XPath, выберем первый span после div с классом my-4

In [16]: response.xpath("//div[@class='my-4']/span/text()").get()
Out[16]: '20.00$'

Я мог также использовать следующий CSS селектор:

In [21]: response.css('.my-4 span::text').get()
Out[21]: '20.00$'

Создаем Scrapy Spider

В Scrapy, Spider является классом, где определяется поведение при анализе (какие ссылки или URL должны пройти через скрапинг) и при скрапинге (что нужно извлечь).

Предусмотрено несколько этапов, которые Spider использует для скрапинга вебсайта:

  • Все начинается с просмотра атрибута класса start_urls и вызова этих URL-адресов с помощью метода start_requests(). Вы можете переопределить этот метод, если нужно изменить HTTP verb, добавить некоторые параметры в запрос (например, отправив запрос POST вместо GET).
  • Затем генерируется объект Request для каждого URL и отправляется ответ в функцию обратного вызова parse().
  • Затем метод parse() извлекает данные (в нашем случае — цену продукта, изображение, описание, заголовок) и возвращает либо словарь, объект Item, запрос или итерацию.

Может показаться удивительным, что метод parse может возвращать так много разных объектов. Это сделано для гибкости. Допустим, вы хотите сделать скрапинг онлайн магазина, на котором нет карты сайта. Вы можете начать со скрапинга товарных категорий, это будет первый метод парсинга.

Данный метод затем передает объект Request для каждой категории продуктов новому методу обратного вызова parse2(). Для каждой категории нужно обрабатывать нумерацию страниц. Затем для каждого продукта производится фактический скрапинг, что генерирует элемент и третью функцию parse.

С Scrapy можно возвращать данные скрапинга в виде простого словаря Python, но рекомендуется использовать встроенный класс Scrapy Item. Это простой контейнер для данных скрапинга. Scrapy будет просматривать его поля для экспорта данных в различные форматы (JSON / CSV…), источника элемента и многого другого.

Далее показан базовый класс Product:

import scrapy

class Product(scrapy.Item):
    product_url = scrapy.Field()
    price = scrapy.Field()
    title = scrapy.Field()
    img_url = scrapy.Field()

Теперь можно сгенерировать Spider при помощи командной строки:

scrapy genspider myspider mydomain.com

Также это можно сделать вручную, поместив код Spider внутри папки /spiders.

В Scrapy есть разные типы Spider, предназначенные для решения самых частых случаев веб-скрапинга:

  • Spider, который мы будем использовать, принимает список start_urls и скрапит каждый элемент через метод parse.
  • CrawlSpider переходит по ссылкам, которые соответствуют определенным набором правил.
  • SitemapSpider извлекается URL, определенные в карте сайта.
  • Многие другие.
# -*- coding: utf-8 -*-
import scrapy

from product_scraper.items import Product

class EcomSpider(scrapy.Spider):
    name = 'ecom_spider'
    allowed_domains = ['clever-lichterman-044f16.netlify.com']
    start_urls = ['https://clever-lichterman-044f16.netlify.com/products/taba-cream.1/']

    def parse(self, response):
        item = Product()
        item['product_url'] = response.url
        item['price'] = response.xpath("//div[@class='my-4']/span/text()").get()
        item['title'] = response.xpath('//section[1]//h2/text()').get()
        item['img_url'] = response.xpath("//div[@class='product-slider']//img/@src").get(0)
        return item

В данном классе EcomSpider есть два запрашиваемых атрибута:

  • name, что является названием Spider (можно запустить при использовании scrapy runspider spider_name)
  • start_urls, что является начальным URL

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

Затем поля Product просто заполняются при помощи выражений XPath для извлечения нужных данных, и возвращается элемент.

Чтобы экспортировать результат в JSON (его также можете экспортировать в CSV), можно запустить код следующим образом:

scrapy runspider ecom_spider.py -o product.json

После этого вы должны получить требуемый файл JSON:

[
  {
    "product_url": "https://clever-lichterman-044f16.netlify.com/products/taba-cream.1/",
    "price": "20.00$",
    "title": "Taba Cream",
    "img_url": "https://clever-lichterman-044f16.netlify.com/images/products/product-2.png"
  }
]

Во время извлечения данных с веб страницы вы можете столкнуться со следующими проблемами:

  • На одном и том же сайте лейаут страницы и основной HTML могут отличаться. При скрапинге онлайн магазина вам часто будет встречаться стандартная цена и цена после скидки, у которых разные XPath и CSS селекторы.
  • Данные могут быть представлены несколько беспорядочно, поэтому зачастую требуется последующая обработка. Опять же, в онлайн магазине могут возникнуть проблемы с различным отображением цен, например — ($1.00, $1, $1,00).

Scrapy поставляется со встроенным решением проблемы — ItemLoaders. Это интересный способ заполнения объекта Product.

Вы можете добавить несколько выражений XPath одному и тому же полю, и он проверит все последовательно. По умолчанию, если найдено несколько XPath, они будут помещены в список.

В официальной документации Scrapy можно найти много примеров операций ввода и вывода данных.

Это очень полезно, если вам понадобится изменить или очистить извлеченные данные. К примеру, получение списка доступных валют, изменение одной единицы измерения в другую (сантиметры в метры, градусы Цельсия в Фаренгейты и так далее).

На нашей веб странице можно найти название продукта через разные XPath выражения: //title и //section[1]//h2/text()

В данном случае можно использовать Itemloader:

def parse(self, response):
    l = ItemLoader(item=Product(), response=response)
    l.add_xpath('price', "//div[@class='my-4']/span/text()")
    l.add_xpath('title', '//section[1]//h2/text()')
    l.add_xpath('title', '//title')
    l.add_value('product_url', response.url)
    return l.load_item()

Обычно требуется только первый совпадающий XPath, поэтому нужно добавить output_processor=TakeFirst() в конструктор поля элемента.

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

from scrapy.loader import ItemLoader
from scrapy.loader.processors import TakeFirst, MapCompose, Join

def remove_dollar_sign(value):
    return value.replace('$', '')

class ProductLoader(ItemLoader):
    default_output_processor = TakeFirst()
    price_in = MapCompose(remove_dollar_sign)

Мы также добавили price_in, что является вводным процессором для удаления знака доллара из цены. Здесь используется MapCompose, что является встроенным процессором, который принимает несколько функций, которые выполняются последовательно. Можно добавить столько функций, сколько хочется. Нужно добавить _in или _out в название поля Item для добавления к нему процессора ввода или вывода.

Существует довольно много процессоров, прочитать о них подробнее можно в официальной документации.

Скрапинг нескольких страниц через Scrapy

Теперь, когда мы познакомились со скрапингом одной страницы, пришло время научиться скрапить несколько страниц. Например весь каталог товаров. Как мы видели ранее, есть разные виды Spider.

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

Зачастую можно найти один в base_url/sitemap.xml. Парсинг карты сайта может быть сложным, однако Scrapy может значительно облегчить процесс.

В нашем случае карту сайта можно найти здесь: https://clever-lichterman-044f16.netlify.com/sitemap.xml

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

<url>
  <loc>
  https://clever-lichterman-044f16.netlify.com/blog/post-1/
  </loc>
  <lastmod>2019-10-17T11:22:16+06:00</lastmod>
</url>
<url>
  <loc>
  https://clever-lichterman-044f16.netlify.com/products/
  </loc>
  <lastmod>2019-10-17T11:22:16+06:00</lastmod>
</url>
<url>
  <loc>
  https://clever-lichterman-044f16.netlify.com/products/taba-cream.1/
  </loc>
  <lastmod>2019-10-17T11:22:16+06:00</lastmod>
</url>

К счастью, можно отфильтровать адреса URL и парсить только те, что соответствуют определенному паттерну. Это не сложно, мы будем использовать только те URL, внутри адресов которых значится совпадение с /products/.

class SitemapSpider(SitemapSpider):
    name = "sitemap_spider"
    sitemap_urls = ['https://clever-lichterman-044f16.netlify.com/sitemap.xml']
    sitemap_rules = [
        ('/products/', 'parse_product')
    ]

    def parse_product(self, response):
        # ... скрапинг товара ...

Запустить скрипт для скрапинга всех товаров и экспорта результата в CSV файл можно следующим образом: scrapy runspider sitemap_spider.py -o output.csv

Что же делать, если на сайте нет никакой карты? У Scrapy есть решение и для таких случаев.

Позвольте представить вам… CrawlSpider.

CrawlSpider изучит сайт, начав со списка start_urls. Затем для каждого url он извлечет все ссылки, базирующиеся на списке Rule. В нашем случае все довольно просто, у товаров одинаковый URL паттерн /products/product_title, поэтому потребуется отфильтровать только эти URL.

import scrapy
from scrapy.spiders import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractor
from product_scraper.productloader import ProductLoader
from product_scraper.items import Product

class MySpider(CrawlSpider):
    name = 'crawl_spider'
    allowed_domains = ['clever-lichterman-044f16.netlify.com']
    start_urls = ['https://clever-lichterman-044f16.netlify.com/products/']

    rules = (
        
        Rule(LinkExtractor(allow=('products', )), callback='parse_product'),
    )

    def parse_product(self, response):
      # .. парсинг товара

Как видите, все встроенные «Пауки» довольно легко использовать. Начинать процесс с нуля было бы намного проблематичнее.

Вместе со Scrapy вам не нужно думать о логике парсинга, добавлении новых URL в очередь, фиксировании уже изученных адресов и многопоточности.

Заключение

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

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

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

Для более подробного ознакомления со Scrapy можете изучить официальную документацию.

Удачного скрапинга!