Скрейпинг веб-сайтов на Python c помощью Scrapy

Введение

Веб-скрейпинг, часто называемый веб-скрейлингом или веб-спайдингом, – это программный просмотр веб-страниц и извлечение из них данных. Он является мощным инструментом для работы с информацией в Интернете.

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

В этом руководстве вы узнаете об основах процесса скрейпинга и спайдинга, исследуя набор данных c помощью Scrapy. Мы будем использовать Quotes to Scrape, базу данных цитат, размещенную на сайте, предназначенном для тестирования веб-спайдеров. К концу этого урока у вас будет полностью функциональный веб-спайдер на языке Python, который будет проходить через несколько страниц с цитатами и отображать их на экране.

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

Предисловие

Для выполнения этого урока вам понадобится локальная среда разработки для Python 3. Для настройки всего необходимого вы можете следовать инструкции “Как установить и настроить локальную среду программирования для Python 3”.

Шаг 1 – Создание базового скрейпера

Скрейпинг – это процесс, состоящий из двух следующих этапов:

  1. Систематический поиск и загрузка веб-страниц.
  2. Извлечение информации из загруженных страниц.

Оба этих этапа могут быть реализованы различными способами и на разных языках.

Вы можете создать скрейпер с нуля, используя модули или библиотеки, предоставляемые вашим языком программирования, но тогда вам придется иметь дело с некоторыми потенциальными трудностями по мере усложнения вашего скрейпера: обрабатывать несколько потоков, чтобы можно было просматривать несколько страницы одновременно, преобразовывать собранные данные в различные форматы, такие как CSV, XML или JSON и иметь дело с сайтами, которые требуют особых настроек и шаблонов доступа.

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

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

Scrapy, как и большинство пакетов Python, можно установить с помощью PyPI (также известном как pip). PyPI (Python Package Index) – это хранилище всех опубликованных программ Python, принадлежащее сообществу .

Если у вас установлен Python, то у вас также уже имеется pip. С помощью него вы можете установить Scrapy посредством следующей команды:

$ pip install scrapy

Если у вас возникли проблемы с установкой, или вы хотите установить Scrapy без использования pip, ознакомьтесь с официальной документацией по установке.

Установив Scrapy, создайте новую папку для проекта. Это можно сделать в терминале, выполнив команду:

$ mkdir quote-scraper

Теперь перейдите в новый каталог, который вы только что создали:

$ cd quote-scraper

Затем создайте новый файл .py для нашего скрейпера под названием scraper.py. В этом скрипте мы разместим весь наш код для данного урока.

Начнем проект с создания простого скрейпера, который использует Scrapy в качестве основы. Для этого вам нужно будет создать класс, который будет подклассом scrapy.Spider, базового класса Паука, предоставляемого Scrapy. Этот класс будет иметь два обязательных атрибута:

  • name – имя Паука.
  • start_urlsсписок URL адресов с интересующими нас данными. Мы начнем с одного URL.

Откройте файл scrapy.py в IDE и добавьте этот код для создания базового Паука:

import scrapy


class QuoteSpider(scrapy.Spider):
    name = 'quote-spdier'
    start_urls = ['https://quotes.toscrape.com']

Давайте разберем построчно:

Во-первых, мы импортируем scrapy, чтобы мы могли использовать классы, которые предоставляет этот пакет.

Затем мы возьмем класс Spider, предоставляемый Scrapy, и создадим его подкласс под названием BrickSetSpider. Считайте, что подкласс – это более специализированная форма родительского класса. Класс Spider описывает методы и поведение, которые определяют, как следовать по URL и извлекать данные из найденных страниц, но он не знает, где и какие данные искать. Создав его подкласс, мы сможем предоставить ему эту информацию.

Наконец, мы назовем класс quote-spider и дадим нашему скрейперу один URL, с которого он начнет работу: https://quotes.toscrape.com. Если вы откроете этот URL в браузере, он приведет сайт, где будет открыта первая из многих страниц, содержащих известные цитаты.

Теперь протестируем скрейпер. Обычно файлы Python запускаются командой типа python path/to/file.py. Однако Scrapy поставляется с собственным интерфейсом командной строки для упрощения процесса запуска скрейпера. Запустите свой скрейпер следующей командой:

$ scrapy runspider scraper.py

На экран выведется примерно следующее:

Output
2022-12-02 10:30:08 [scrapy.utils.log] DEBUG: Using reactor: twisted.internet.epollreactor.EPollReactor
2022-12-02 10:30:08 [scrapy.extensions.telnet] INFO: Telnet Password: b4d94e3a8d22ede1
2022-12-02 10:30:08 [scrapy.middleware] INFO: Enabled extensions:
['scrapy.extensions.corestats.CoreStats',
 ...
 'scrapy.extensions.logstats.LogStats']
2022-12-02 10:30:08 [scrapy.middleware] INFO: Enabled downloader middlewares:
['scrapy.downloadermiddlewares.httpauth.HttpAuthMiddleware',
 ...
 'scrapy.downloadermiddlewares.stats.DownloaderStats']
2022-12-02 10:30:08 [scrapy.middleware] INFO: Enabled spider middlewares:
['scrapy.spidermiddlewares.httperror.HttpErrorMiddleware',
 ...
 'scrapy.spidermiddlewares.depth.DepthMiddleware']
2022-12-02 10:30:08 [scrapy.middleware] INFO: Enabled item pipelines:
[]
2022-12-02 10:30:08 [scrapy.core.engine] INFO: Spider opened
2022-12-02 10:30:08 [scrapy.extensions.logstats] INFO: Crawled 0 pages (at 0 pages/min), scraped 0 items (at 0 items/min)
2022-12-02 10:30:08 [scrapy.extensions.telnet] INFO: Telnet console listening on 127.0.0.1:6023
2022-12-02 10:49:32 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://quotes.toscrape.com> (referer: None)
2022-12-02 10:30:08 [scrapy.core.engine] INFO: Closing spider (finished)
2022-12-02 10:30:08 [scrapy.statscollectors] INFO: Dumping Scrapy stats:
{'downloader/request_bytes': 226,
 ...
 'start_time': datetime.datetime(2022, 12, 2, 18, 30, 8, 492403)}
2022-12-02 10:30:08 [scrapy.core.engine] INFO: Spider closed (finished)

Информации очень много, поэтому давайте разделим её на части.

  • Скрейпер инициализировал и загрузил дополнительные компоненты и расширения, необходимые для чтения данных.
  • Он использовал URL, который мы указали в списке start_urls, и извлек HTML, как это сделал бы ваш веб-браузер.
  • Он передал HTML в метод parse, который по умолчанию ничего не делает. Поскольку мы так и не написали свой собственный метод parse, Паук просто завершает работу, не выполняя никаких дополнительных действий.

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

Шаг 2 – Извлечение данных со страницы

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

Если вы посмотрите на страницу, которую мы хотим отскрейпить, вы увидите, что она имеет следующую структуру:

  • Заголовок, который присутствует на каждой странице.
  • Ссылка для входа в систему.
  • Цитаты, представленные в виде таблицы или упорядоченного списка. Каждая цитата имеет похожий формат.

При написании скрейпера вам нужно будет просмотреть исходный HTML-файл и ознакомиться с его структурой. Вот пример файла(теги, которые не имеют отношения к нашей цели, удалены для удобства чтения):

quotes.toscrape.com<body>
...
    <div class="quote" itemscope itemtype="http://schema.org/CreativeWork">
        <span class="text" itemprop="text">“I have not failed. I've just found 10,000 ways that won't work.”</span>
        <span>by <small class="author" itemprop="author">Thomas A. Edison</small>
        <a href="/author/Thomas-A-Edison">(about)</a>
        </span>
        <div class="tags">
            Tags:
            <meta class="keywords" itemprop="keywords" content="edison,failure,inspirational,paraphrased" /    > 
            
            <a class="tag" href="/tag/edison/page/1/">edison</a>
            
            <a class="tag" href="/tag/failure/page/1/">failure</a>
            
            <a class="tag" href="/tag/inspirational/page/1/">inspirational</a>
            
            <a class="tag" href="/tag/paraphrased/page/1/">paraphrased</a>
            
        </div>
    </div>
...    
</body>

Скрейпинг этой страницы состоит из двух этапов:

  1. Берем каждую цитату, отыскивая те части страницы, в которых содержатся нужные нам данные.
  2. Из каждой цитаты получаем нужные данные, извлекая их из HTML-тегов.

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

Сейчас мы будем использовать CSS-селекторы, поскольку CSS идеально подходит для поиска всех наборов на странице. Если вы посмотрите на HTML документ, то увидите, что каждая цитата задается с помощью класса quote. Поскольку мы ищем класс, мы используем .quote для нашего CSS-селектора. Часть “.” селектора ищет атрибут class в элементах. Все, что нам нужно сделать, это создать новый метод в нашем классе с именем parse и передать этот селектор в объект response, как показано ниже:

class QuoteSpider(scrapy.Spider):
    name = 'quote-spdier'
    start_urls = ['https://quotes.toscrape.com']

    def parse(self, response):
        QUOTE_SELECTOR = '.quote'
        TEXT_SELECTOR = '.text::text'
        AUTHOR_SELECTOR = '.author::text'
        
        for quote in response.css(QUOTE_SELECTOR):
            pass

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

Еще один взгляд на источник страницы, которую мы разбираем, говорит нам о том, что текст каждой цитаты хранится в span с классом text, а автор цитаты – в теге <small> с классом author:

quotes.toscrape.com        ...
        <span class="text" itemprop="text">“I have not failed. I've just found 10,000 ways that won't work.”</span>
        <span>by <small class="author" itemprop="author">Thomas A. Edison</small>
        ...

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

class QuoteSpider(scrapy.Spider):
    name = 'quote-spdier'
    start_urls = ['https://quotes.toscrape.com']

    def parse(self, response):
        QUOTE_SELECTOR = '.quote'
        TEXT_SELECTOR = '.text::text'
        AUTHOR_SELECTOR = '.author::text'
        
        for quote in response.css(QUOTE_SELECTOR):
            yield {
                'text': quote.css(TEXT_SELECTOR).extract_first(),
                'author': quote.css(AUTHOR_SELECTOR).extract_first(),
            }

Примечание: Запятая после extract_first() – это не опечатка. В словарях Python синтаксис допускает запятую, что является хорошим способом оставить место для добавления дополнительных элементов. Это мы и сделаем позже.

Вы заметите две вещи, происходящие в этом коде:

  • Мы добавляем ::text к нашим селекторам для цитаты и автора. Это псевдоселектор CSS, который берет текст внутри тега, а не сам тег.
  • Мы вызываем extract_first() для объекта, возвращаемого quote.css(TEXT_SELECTOR), потому что нам нужен только первый элемент, соответствующий селектору. Это дает нам строку, а не список элементов.

Сохраните файл и запустите скрейпер снова:

$ scrapy runspider scraper.py

На этот раз вывод будет содержать цитаты и их авторов:

Output
...
2022-12-02 11:00:53 [scrapy.core.scraper] DEBUG: Scraped from <200 https://quotes.toscrape.com>
{'text': '“There are only two ways to live your life. One is as though nothing is a miracle. The other is as though everything is a miracle.”', 'author': 'Albert Einstein'}
2022-12-02 11:00:53 [scrapy.core.scraper] DEBUG: Scraped from <200 https://quotes.toscrape.com>
{'text': '“The person, be it gentleman or lady, who has not pleasure in a good novel, must be intolerably stupid.”', 'author': 'Jane Austen'}
2022-12-02 11:00:53 [scrapy.core.scraper] DEBUG: Scraped from <200 https://quotes.toscrape.com>
{'text': "“Imperfection is beauty, madness is genius and it's better to be absolutely ridiculous than absolutely boring.”", 'author': 'Marilyn Monroe'}
...

Давайте продолжим код, добавив новые селекторы для ссылок на страницы об авторе и тегов для цитаты. Исследуя HTML для каждой цитаты, мы обнаруживаем, что:

  • Ссылка на страницу с информацией об авторе хранится в ссылке сразу после его имени.
  • Теги хранятся в виде коллекции тегов a, каждый тег, отнесенный к определенному классу, хранится в элементе div с классом tags.

Итак, давайте изменим скрейпер, чтобы получить эту новую информацию:

class QuoteSpider(scrapy.Spider):
    name = 'quote-spdier'
    start_urls = ['https://quotes.toscrape.com']

    def parse(self, response):
        QUOTE_SELECTOR = '.quote'
        TEXT_SELECTOR = '.text::text'
        AUTHOR_SELECTOR = '.author::text'
        ABOUT_SELECTOR = '.author + a::attr("href")'
        TAGS_SELECTOR = '.tags > .tag::text'

        for quote in response.css(QUOTE_SELECTOR):
            yield {
                'text': quote.css(TEXT_SELECTOR).extract_first(),
                'author': quote.css(AUTHOR_SELECTOR).extract_first(),
                'about': 'https://quotes.toscrape.com' + 
                        quote.css(ABOUT_SELECTOR).extract_first(),
                'tags': quote.css(TAGS_SELECTOR).extract(),
            }

Сохраните изменения и запустите скрейпер снова:

$ scrapy runspider scraper.py

Теперь вывод будет содержать новые данные:

Output
2022-12-02 11:14:28 [scrapy.core.scraper] DEBUG: Scraped from <200 https://quotes.toscrape.com>
{'text': '“There are only two ways to live your life. One is as though nothing is a miracle. The other is as though everything is a miracle.”', 'author': 'Albert Einstein', 'about': 'https://quotes.toscrape.com/author/Albert-Einstein', 'tags': ['inspirational', 'life', 'live', 'miracle', 'miracles']}
2022-12-02 11:14:28 [scrapy.core.scraper] DEBUG: Scraped from <200 https://quotes.toscrape.com>
{'text': '“The person, be it gentleman or lady, who has not pleasure in a good novel, must be intolerably stupid.”', 'author': 'Jane Austen', 'about': 'https://quotes.toscrape.com/author/Jane-Austen', 'tags': ['aliteracy', 'books', 'classic', 'humor']}
2022-12-02 11:14:28 [scrapy.core.scraper] DEBUG: Scraped from <200 https://quotes.toscrape.com>
{'text': "“Imperfection is beauty, madness is genius and it's better to be absolutely ridiculous than absolutely boring.”", 'author': 'Marilyn Monroe', 'about': 'https://quotes.toscrape.com/author/Marilyn-Monroe', 'tags': ['be-yourself', 'inspirational']}

Давайте превратим этот скрейпер в паука, который будет переходить по ссылкам.

Шаг 3 – Сканирование нескольких страниц

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

Вы заметите, что вверху и внизу каждой страницы есть маленький знак “больше” (>), который ссылается на следующую страницу результатов. Вот пример HTML кода:

quotes.toscrape.com...
    <nav>
        <ul class="pager">
            
            
            <li class="next">
                <a href="/page/2/">Next <span aria-hidden="true">&rarr;</span></a>
            </li>
            
        </ul>
    </nav>
...

В источнике вы найдете тег li с классом next, а внутри этого тега – тег a со ссылкой на следующую страницу. Все, что нам нужно сделать, это указать скрейперу перейти по этой ссылке, если она существует.

Измените свой код следующим образом:

class QuoteSpider(scrapy.Spider):
    name = 'quote-spdier'
    start_urls = ['https://quotes.toscrape.com']

    def parse(self, response):
        QUOTE_SELECTOR = '.quote'
        TEXT_SELECTOR = '.text::text'
        AUTHOR_SELECTOR = '.author::text'
        ABOUT_SELECTOR = '.author + a::attr("href")'
        TAGS_SELECTOR = '.tags > .tag::text'
        NEXT_SELECTOR = '.next a::attr("href")'

        for quote in response.css(QUOTE_SELECTOR):
            yield {
                'text': quote.css(TEXT_SELECTOR).extract_first(),
                'author': quote.css(AUTHOR_SELECTOR).extract_first(),
                'about': 'https://quotes.toscrape.com' + 
                        quote.css(ABOUT_SELECTOR).extract_first(),
                'tags': quote.css(TAGS_SELECTOR).extract(),
            }

        next_page = response.css(NEXT_SELECTOR).extract_first()
        if next_page:
            yield scrapy.Request(response.urljoin(next_page))

Сначала мы определяем селектор для ссылки “следующая страница”, извлекаем первое совпадение и проверяем, существует ли ссылка. scrapy.Request – это новый объект запроса, который означает, что scrappy должен получить и разобрать следующий запрос.

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

Теперь, если вы сохраните свой код и запустите паука снова, вы увидите, что он не останавливается после того, как пройдется по первой странице: он продолжает просматривать все 100 цитат на всех 10 страницах. По большому счету, это не такой уж серьезный объем данных, но теперь вы освоили процесс, с помощью которого можно автоматически находить новые страницы для сканирования.

Вот наш готовый код для этого урока:

import scrapy


class QuoteSpider(scrapy.Spider):
    name = 'quote-spdier'
    start_urls = ['https://quotes.toscrape.com']

    def parse(self, response):
        QUOTE_SELECTOR = '.quote'
        TEXT_SELECTOR = '.text::text'
        AUTHOR_SELECTOR = '.author::text'
        ABOUT_SELECTOR = '.author + a::attr("href")'
        TAGS_SELECTOR = '.tags > .tag::text'
        NEXT_SELECTOR = '.next a::attr("href")'

        for quote in response.css(QUOTE_SELECTOR):
            yield {
                'text': quote.css(TEXT_SELECTOR).extract_first(),
                'author': quote.css(AUTHOR_SELECTOR).extract_first(),
                'about': 'https://quotes.toscrape.com' + 
                        quote.css(ABOUT_SELECTOR).extract_first(),
                'tags': quote.css(TAGS_SELECTOR).extract(),
            }

        next_page = response.css(NEXT_SELECTOR).extract_first()
        if next_page:
            yield scrapy.Request(
                response.urljoin(next_page),
            )

Заключение

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

Перевод статьи Justin Duke «How To Crawl A Web Page with Scrapy and Python 3».