Создание парсеров с помощью Scrapy и Python

9511

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

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

Базовые знания HTML и CSS помогут лучше и быстрее освоить материал.

Обзор Scrapy

Процесс веб-скрапинга (парсинга)

Веб-скрапинг (парсинг) стал эффективным путем извлечения информации из сети для последующего принятия решений и анализа. Он является неотъемлемым инструментом в руках специалиста в области data science. Дата сайентисты должны знать, как собирать данные с веб-страниц и хранить их в разных форматах для дальнейшего анализа.

Любую страницу в интернете можно исследовать в поисках информации, а все, что есть на странице — можно извлечь. У каждой страницы есть собственная структура и веб-элементы, из-за чего необходимо писать собственных сканеров для каждой страницы.

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

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

Scrapy vs. Beautiful Soup

В этом разделе будет дан обзор одного из самых популярных инструментов для парсинга, BeautifulSoup, и проведено его сравнение со Scrapy.

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

BeautifulSoup также широко используется для веб-скрапинга. Это пакет Python для парсинга документов в форматах HTML и XML и извлечения данных из них. Он доступен для Python 2.6+ и Python 3.

Вот основные отличия между ними:

ScrapyBeautifulSoup
Функциональность
Scrapy — это самый полный набор инструментов для загрузки веб-страниц, их обработки и сохранения в файлы и базы данныхBeautifulSoup — это в принципе просто парсер HTML и XML, требующий дополнительных библиотек, таких как requests и urlib2 для открытия ссылок и сохранения результатов.
Кривая обучения
Scrapy — это движущая сила веб-сканирования, предлагающая массу способов парсинга страниц. Обучение тому, как он работает, требует много времени, но после освоения процесс сканирования превращается в одну строку кода. Потребуется время, чтобы стать экспертом в Scrapy и изучить все его особенностиBeautifulSoup относительно прост для понимания новичкам в программировании и позволяет решать маленькие задачи за короткий срок.
Скорость и нагрузка
Scrapy с легкостью выполняет крупную по объему работу. Он может сканировать несколько ссылок одновременно менее чем за минуту в зависимости от общего количества. Это происходит плавно благодаря Twister, который работает асинхронно (без блокировки)BeautifulSoup используется для простого и эффективного парсинга. Он работает медленнее Scrapy, если не использовать multiprocessing.
Расширяемость
Scrapy предоставляет функциональность Item pipelines, с помощью которой можно писать функции для веб-сканера. Они будут включать инструкции о том, как робот должен проверять, удалять и сохранять данные в базу данных. Spider Contracts используются для проверки парсеров, благодаря чему можно создавать как базовые, так и глубокие парсеры. Он же позволяет настраивать множество переменных: повторные попытки, перенаправление и т. д.Если проект не предполагает большого количества логики, BeautifulSoup отлично для этого подходит, но если нужна настраиваемость, например прокси, управление куки и распределение данных, то Scrapy справляется лучше.

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

Установка Scrapy

С установленным Python 3.0 (и новее) при использовании Anaconda можно применить команду conda для установки scrapy. Напишите следующую команду в Anaconda:

conda install -c conda-forge scrapy

Чтобы установить Anaconda, посмотрите эти руководства PythonRu для Mac и Windows.

Также можно использовать установщик пакетов pyhton pip. Это работает в Linux, Mac и Windows:

pip install scrapy

Scrapy Shell

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

Откройте командную строку и напишите следующую команду:

 scrapy shell

При использовании Anaconda можете написать эту же команду прямо в anaconda prompt. Вывод будет выглядеть приблизительно вот так:

2020-03-14 16:28:16 [scrapy.utils.log] INFO: Scrapy 2.0.0 started (bot: scrapybot)
2020-03-14 16:28:16 [scrapy.utils.log] INFO: Versions: lxml 4.3.2.0, libxml2 2.9.9, cssselect 1.1.0, parsel 1.5.2, w3lib 1.21.0, Twisted 19.10.0, Python 3.7.3 (default, Mar 27 2019, 17:13:21) [MSC v.1915 64 bit (AMD64)], pyOpenSSL 19.0.0 (OpenSSL 1.1.1c  28 May 2019), cryptography 2.6.1, Platform Windows-10-10.0.18362-SP0
....
[s]   fetch(req)                  Fetch a scrapy.Request and update local objects
[s]   shelp()           Shell help (print this help)
[s]   view(response)    View response in a browser

Необходимо запустить парсер на странице с помощью команды fetch в оболочке. Он пройдет по странице, загружая текст и метаданные.
fetch(“https://www.aliexpress.com/category/200216607/tablets.html”)

Примечание: всегда заключайте ссылку в кавычки, одинарные или двойные

Вывод будет следующий:

2020-03-14 16:39:53 [scrapy.core.engine] INFO: Spider opened
2020-03-14 16:39:53 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://www.aliexpress.com/robots.txt> (referer: None)
2020-03-14 16:39:55 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://www.aliexpress.com/category/200216607/tablets.html> (referer: None)

Парсер возвращает response (ответ), который можно посмотреть с помощью команды view(response). А страница откроется в браузере по умолчанию.
С помощью команды print(response.text) можно посмотреть сырой HTML.

Отобразится скрипт, который генерирует страницу. Это то же самое, что вы видите по нажатию правой кнопкой мыши в пустом месте и выборе «Просмотр кода страница» или «Просмотреть код». Но поскольку нужна конкретная информация, а не целый скрипт, с помощью инструментов разработчика в браузере необходимо определить требуемый элемент. Возьмем следующие элементы:

  • Название планшета
  • Цена планшета
  • Количество заказов
  • Имя магазина

Нажмите правой кнопкой по элементу и кликните на «Просмотреть код».

Инструменты разработчика сильно помогут при работе с парсингом. Здесь видно, что есть тег <a> с классом item-title, а сам текст включает название продукта:

<a data-p4p="true" class="item-title" href="//aliexpress.ru
/item/32719104234.html?spm=a2g0o.productlist.0.0.248552054LjgSt&
amp;algo_pvid=ff95978b-3cdf-4d85-8ab6-da9c1cf5f78b&amp;
algo_expid=ff95978b-3cdf-4d85-8ab6-da9c1cf5f78b-0&amp;
btsid=0b0a3f8115842002308095415e318f&amp;
ws_ab_test=searchweb0_0,searchweb201602_,searchweb201603_" 
title="Планшет с 10,1-дюймовым дисплеем, восьмиядерным 
процессором 3g 4g LTE, Android 9,0, ОЗУ 8 Гб, ПЗУ 128 ГБ, 10 
дюймов" target="_blank" data-spm-anchor-
id="a2g0o.productlist.0.0">Планшет с 10,1-дюймовым дисплеем, 
восьмиядерным процессором 3g 4g LTE, Android 9,0, ОЗУ 8 Гб, ПЗУ 
128 ГБ, 10 дюймов</a>

Использование CSS-селекторов для извлечения

Извлечь эту информацию можно с помощью атрибутов элементов или CSS-селекторов в виде классов. Напишите следующее в оболочке Scrapy, чтобы получить имя продукта:

response.css(".item-title::text").extract_first()

Вывод:

'Планшет с 10,1-дюймовым дисплеем, восьмиядерным процессором 3g 4g LTE, Android 9,0, ОЗУ 8 Гб, ПЗУ 128 ГБ, 10 дюймов'

extract_first() извлекает первый элемент, соответствующий селектору css. Для извлечения всех названий нужно использовать extract():

response.css(".item-title::text").extract()

Следующий код извлечет ценовой диапазон этих продуктов:

response.css(".price-current::text").extract()

То же можно повторить для количества заказов и имени магазина.

Использование XPath для извлечения

XPath — это язык запросов для выбора узлов в документах типа XML. Ориентироваться по документу можно с помощью XPath. Scrapy использует этот же язык для работы с объектами документа HTML. Использованные выше CSS-селекторы также конвертируются в XPath, но в большинстве случаев CSS очень легко использовать. И тем не менее важно значить, как язык работает в Scrapy.

Откройте оболочку и введите fetch("https://www.aliexpress.com/category/200216607/tablets.html/") как и раньше. Попробуйте написать следующий код:

response.xpath('/html').extract()

Он покажет весь код в теге <html>. / указывает на прямого потомка узла. Если нужно получить теги <div> в html, то необходимо писать:

response.xpath('/html//div').extract()

Для XPath важно научиться понимать, как используются / и //, чтобы ориентироваться в дочерних узлах.

Если необходимо получить все теги <div>, то нужно написать то же самое, но без /html:

response.xpath("//div").extract()

Можно и дальше фильтровать начальные узлы, чтобы получить нужные узлы с помощью атрибутов и их значений. Это синтаксис использования классов и их значений.

response.xpath("//div[@class='quote'/span[@class='text']").extract()

response.xpath("//div[@class='quote']/span[@class='text']/text()").extract() 

Используйте text() для извлечения всего текста в узлах

Создание проекта Scrapy и собственного робота (Spider)

Парсинг хорошо подходит для создания агрегатора, который будет использоваться для сравнения данных. Например, нужно купить планшет, предварительно сравнив несколько продуктов и их цены. Для этого можно исследовать все страницы и сохранить данные в файл Excel. В этом примере продолжим парсить aliexpress.com на предмет информации о планшетах.

Создадим робота (Spider) для страницы. В первую очередь необходимо создать проект Scrapy, где будут храниться код и результаты. Напишите следующее в терминале или anaconda.

scrapy startproject aliexpress

Это создаст скрытую папку в директории с Python или Anaconda по умолчанию. Она будет называться aliexpress, но можно выбрать любое название. Структура директории следующая:

Структура проекта Scrapy

Файл/папкаНазначение
scrapy.cfgФайл настройки развертывания
aliexpress/Модуль Python проекта, отсюда импортируется код
__init.py__Файл инициализации
items.pyPython файл с элементами проекта
pipelines.pyФайл, который содержит пайплайн проекта
settings.pyФайл настроек проекта
spiders/Папка, в которой будут храниться роботы
__init.py__Файл инициализации

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

scrapy genspider aliexpress_tabletshttps://www.aliexpress.com/category/200216607/tablets.html

Это создает файл шаблона с названием aliexpress_tables.py в папке spiders, как и было описано выше. Вот код из этого файла:

import scrapy

class AliexpressTabletsSpider(scrapy.Spider):
    name = 'aliexpress_tablets'
    allowed_domains = ['aliexpress.com']
    start_urls = ['https://www.aliexpress.com/category/200216607/tablets.html']


    def parse(self, response):
         pass

В коде можно увидеть name, allowed_domains, start_urls и функцию parse.

  • name — это имя робота. Удачные и правильно подобранные имена позволят проще отслеживать всех имеющихся роботов. Они должны быть уникальны, ведь именно они используются для запуска командой scrapy crawl name_of_spider.
  • allowed_domains (опционально) — список разрешенных для парсинга доменов. Запрос к URL, не указанным в этом списке, не будет выполнен. Он должен включать только домен сайта (например, aliexpress.com), а не целый URL, указанный в start_urls, иначе возникнут ошибки.
  • start_urls — запрос к упомянутым URL. С них робот начнет проводить поиск, если конкретный URL не указан. Первыми загруженными страницами будут те, что указаны здесь. Последующие запросы будут генерироваться последовательно из данных, сохраненных в начальных URL.
  • parse — эта функция вызывается, когда парсинг URL успешно выполнен. Ее еще называют функцией обратного вызова. Response (используемый в оболочке Scrapy) возвращается как результат парсинга, передается этой функции, а внутри нее находится код для извлечения.

можно использовать функцию parse() из BeautifulSoup в Scrapy для парсинга HTML-документа.

Примечание: извлечь данные можно с помощью css-селекторов, используя как response.css(), так и XPath (XML), что позволит получить доступ к дочерним элементам. Пример response.xpath() будет описан в коде функции pass().

Добавим изменения в файл aliexpress_tablet.py. В start_urls теперь еще один URL. Логику извлечения можно указать в функции pass():

import scrapy


class AliexpressTabletsSpider(scrapy.Spider):
    name = 'aliexpress_tablets'
    allowed_domains = ['aliexpress.com']
    start_urls = ['https://www.aliexpress.com/category/200216607/tablets.html',
                 'https://www.aliexpress.com/category/200216607/tablets/2.html?site=glo&g=y&tag=']


    def parse(self, response):

        print("procesing:"+response.url)
        # Извлечение данных с помощью селекторов CSS
        product_name=response.css('.item-title::text').extract()
        price_range=response.css('.price-current::text').extract()
        # Извлечение данных с использованием xpath
        orders=response.xpath("//em[@title='Total Orders']/text()").extract()
        company_name=response.xpath("//a[@class='store $p4pLog']/text()").extract()

        row_data=zip(product_name,price_range,orders,company_name)

        # извлечение данных строки
        for item in row_data:
            # создать словарь для хранения извлеченной информации
            scraped_info = {
                'page': response.url,
                'product_name': item[0],  # item[0] означает продукт в списке и т. д., индекс указывает, какое значение назначить
                'price_range': item[1],
                'orders': item[2],
                'company_name': item[3],
            }

            # генерируем очищенную информацию для скрапа
            yield scraped_info

zip() берет n элементов итерации и возвращает список кортежей. Элемент с индексом i в кортеже создается с помощью элемента с индексом i каждого элемента итерации.

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

Теперь можно запустить робота и посмотреть результат:

scrapy crawl aliexpress_tablets

Экспорт данных

Данные должны быть представлены в форматах CSV или JSON, чтобы их можно было анализировать. Этот раздел руководства посвящен тому, как получить такой файл из имеющихся данных.

Для сохранения файла CSV откройте settings.py в папке проекта и добавьте следующие строки:

FEED_FORMAT="csv" 
FEED_URI="aliexpress.csv"

После сохранения settings.py снова запустите scrapy crawl aliexpress_tablets в папке проекта. Будет создан файл aliexpress.csv.

Примечание: при каждом запуске паука он будет добавлять файл.

  • FEED_FORMAT — настройка необходимого формата сохранения данных. Поддерживаются следующие:
    + JSON	
    + CSV
    + JSON Lines
    + XML
    
  • FEED_URI — местоположение файла. Его можно сохранить в локальном хранилище или по FTP.

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

  • %(time)s — заменяется на временную метку при создании ленты
  • %(name)s — заменяется на имя робота

Например:

  • Сохранить по FTP используя по одной папке на робота:

Изменения для FEED, сделанные в settings.py, будут применены ко всем роботам в проекте. Можно указать и отдельные настройки для конкретного робота, которые перезапишут те, что есть в settings.py.

import scrapy


class AliexpressTabletsSpider(scrapy.Spider):
    name = 'aliexpress_tablets'
    allowed_domains = ['aliexpress.com']
    start_urls = ['https://www.aliexpress.com/category/200216607/tablets.html',
                 'https://www.aliexpress.com/category/200216607/tablets/2.html?site=glo&g=y&tag=']

    custom_settings={ 'FEED_URI': "aliexpress_%(time)s.json",
                       'FEED_FORMAT': 'json'}

    def parse(self, response):

        print("procesing:"+response.url)
        # Извлечение данных с помощью селекторов CSS
        product_name=response.css('.item-title::text').extract()
        price_range=response.css('.price-current::text').extract()
        # Извлечение данных с использованием xpath
        orders=response.xpath("//em[@title='Total Orders']/text()").extract()
        company_name=response.xpath("//a[@class='store $p4pLog']/text()").extract()

        row_data=zip(product_name,price_range,orders,company_name)

        # извлечение данных строки
        for item in row_data:
            # создать словарь для хранения извлеченной информации
            scraped_info = {
                'page': response.url,
                'product_name': item[0],  # item[0] означает продукт в списке и т. д., индекс указывает, какое значение назначить
                'price_range': item[1],
                'orders': item[2],
                'company_name': item[3],
            }

            # генерируем очищенную информацию для скрапа
            yield scraped_info

response.url вернет URL страницы, с которой был сгенерирован ответ. После запуска парсера с помощью scrapy crawl aliexpress_tables можно просмотреть json-файл в каталоге проекта.

Следующие страницы, пагинация

Вы могли обратить внимание на две ссылки в start_urls. Вторая — это страница №2 результатов поиска планшетов. Добавлять все ссылки непрактично. Робот должен быть способен исследовать все страницы сам, а в start_urls указывается только одна стартовая точка.

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

Вот такой код:

<div class="list-pagination"><div class="product-pagination-wrap"><div class="next-pagination next-medium next-normal"><div class="next-pagination-pages"><button disabled="" aria-label="Previous page, current page 1" type="button" class="next-btn next-medium next-btn-normal next-pagination-item next-prev" role="button"><i class="next-icon next-icon-arrow-left next-xs next-btn-icon next-icon-first"></i>Предыдущая</button><div class="next-pagination-list"><button aria-label="Page 1, 7 pages" type="button" class="next-btn next-medium next-btn-normal next-pagination-item next-current" role="button">1</button>
...
<button aria-label="Page 7, 7 pages" type="button" class="next-btn next-medium next-btn-normal next-pagination-item" role="button">7</button></div><button aria-label="Next page, current page 1" type="button" class="next-btn next-medium next-btn-normal next-pagination-item next-next" role="button">След. стр.<i class="next-icon next-icon-arrow-right next-xs next-btn-icon next-icon-last"></i></button></div></div><div class="jump-aera"><span class="total-page">Всего 24 стр</span><span>Перейти на страницу</span><span class="next-input next-large"><input aria-label="Large" height="100%" autocomplete="off" value=""></span><span class="jump-btn">ОК</span></div></div></div>

Здесь видно, что тег <span> с классом .ui-pagination-active — это текущая страница, а дальше идут теги <a> со ссылками на следующие страницы. Каждый раз нужно будет получать тег <a> после тега <span>. Здесь в дело вступает немного CSS. В этом случае нужно получить соседний, а на дочерний узел, так что потребуется сделать CSS-селектор, который будет искать теги <a> после тега <span> с классом .ui-pagination-active.

Запомните! У каждой веб-страницы собственная структура. Нужно будет изучить ее, чтобы получить желаемый элемент. Всегда экспериментируйте с response.css(SELECTOR) в Scrapy Shell, прежде чем переходить к коду.

Измените aliexpress_tabelts.py следующим образом:

import scrapy


class AliexpressTabletsSpider(scrapy.Spider):
    name = 'aliexpress_tablets'
    allowed_domains = ['aliexpress.com']
    start_urls = ['https://www.aliexpress.com/category/200216607/tablets.html']

    custom_settings={ 'FEED_URI': "aliexpress_%(time)s.json",
                       'FEED_FORMAT': 'json'}

    def parse(self, response):

        print("procesing:"+response.url)
        # Извлечение данных с помощью селекторов CSS
        product_name=response.css('.item-title::text').extract()
        price_range=response.css('.price-current::text').extract()
        # Извлечение данных с использованием xpath
        orders=response.xpath("//em[@title='Total Orders']/text()").extract()
        company_name=response.xpath("//a[@class='store $p4pLog']/text()").extract()

        row_data=zip(product_name,price_range,orders,company_name)

        # извлечение данных строки
        for item in row_data:
            # создать словарь для хранения извлеченной информации
            scraped_info = {
                'page': response.url,
                'product_name': item[0],  # item[0] означает продукт в списке и т. д., индекс указывает, какое значение назначить
                'price_range': item[1],
                'orders': item[2],
                'company_name': item[3],
            }

            # генерируем очищенную информацию для скрапа
            yield scraped_info

	    NEXT_PAGE_SELECTOR = '.ui-pagination-active + a::attr(href)'
            next_page = response.css(NEXT_PAGE_SELECTOR).extract_first()
            if next_page:
                yield scrapy.Request(
                response.urljoin(next_page),
                callback=self.parse)

В этом коде:

  • Сначала извлекается ссылка следующей страницы с помощью next_page = response.css(NET_PAGE_SELECTOR).extract_first(), а потом, если переменная next_page получает ссылку и она не пустая, запускается тело if.
  • response.urljoin(next_page) — метод parse() будет использовать этот метод для построения нового URL и получения нового запроса, который будет позже направлен вызову.
  • После получения нового URL он парсит ссылку, исполняя тело for и снова начинает искать новую страницу. Так будет продолжаться до тех пор, пока страницы не закончатся.

Теперь можно просто расслабиться и смотреть, как робот парсит страницы. Этот извлечет все из последующих страниц. Процесс займет много времени, а размер файла будет 1,1 МБ.

Scrapy сделает для вас все!

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

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

Тест на знание python

Какой будет результат выполнения этого кода?
Какой будет результат выполнения кода — print(+-1) ?
Какой будет результат выполнения этого кода?
Верно ли данное утверждение: "В Python есть два типа чисел: целые числа и числа с плавающей точкой"?
Что выведет этот код?
Александр
Я создал этот блог в 2018 году, чтобы распространять полезные учебные материалы, документации и уроки на русском. На сайте опубликовано множество статей по основам python и библиотекам, уроков для начинающих и примеров написания программ. Пишу на популярные темы: веб-разработка, работа с базами данных, data sciense и другие...