Как сканировать веб-страницу с помощью Scrapy и Python 3

Вступление

Соскреб в Интернете, часто называемый веб-сканирование или веб-паутинга, или «программно просматривая коллекцию веб-страниц и извлекая данные», является мощным инструментом для работы с данными в Интернете.

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

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

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

Предпосылки

Чтобы завершить этот урок, вам понадобится локальная среда разработки для Python 3. Вы можете следоватьHow To Install and Set Up a Local Programming Environment for Python 3, чтобы настроить все, что вам нужно.

[[step-1 -—- Creating-a-basic-scraper]] == Шаг 1. Создание базового скребка

Соскоб состоит из двух этапов:

  1. Вы систематически находите и скачиваете веб-страницы.

  2. Вы берете эти веб-страницы и извлекаете из них информацию.

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

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

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

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

Scrapy, как и большинство пакетов Python, находится на PyPI (также известном какpip). PyPI, индекс пакетов Python, является общим хранилищем всего опубликованного программного обеспечения Python.

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

pip install scrapy

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

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

mkdir brickset-scraper

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

cd brickset-scraper

Затем создайте новый файл Python для нашего парсера с именемscraper.py. Мы поместим весь наш код в этот файл для этого урока. Вы можете создать этот файл в терминале с помощью командыtouch, например:

touch scraper.py

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

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

  • name - просто имя паука.

  • start_urls -list URL-адресов, с которых вы начинаете сканирование. Начнем с одного URL.

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

scraper.py

import scrapy


class BrickSetSpider(scrapy.Spider):
    name = "brickset_spider"
    start_urls = ['http://brickset.com/sets/year-2016']

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

Сначала мыimportscrapy, чтобы мы могли использовать классы, которые предоставляет пакет.

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

Затем мы даем пауку имяbrickset_spider.

Наконец, мы даем нашему парсеру единственный URL, с которого нужно начинать:http://brickset.com/sets/year-2016. Если вы откроете этот URL в своем браузере, он перейдет на страницу результатов поиска, на которой будет показана первая из многих страниц, содержащих наборы LEGO.

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

scrapy runspider scraper.py

Вы увидите что-то вроде этого:

Output2016-09-22 23:37:45 [scrapy] INFO: Scrapy 1.1.2 started (bot: scrapybot)
2016-09-22 23:37:45 [scrapy] INFO: Overridden settings: {}
2016-09-22 23:37:45 [scrapy] INFO: Enabled extensions:
['scrapy.extensions.logstats.LogStats',
 'scrapy.extensions.telnet.TelnetConsole',
 'scrapy.extensions.corestats.CoreStats']
2016-09-22 23:37:45 [scrapy] INFO: Enabled downloader middlewares:
['scrapy.downloadermiddlewares.httpauth.HttpAuthMiddleware',
 ...
 'scrapy.downloadermiddlewares.stats.DownloaderStats']
2016-09-22 23:37:45 [scrapy] INFO: Enabled spider middlewares:
['scrapy.spidermiddlewares.httperror.HttpErrorMiddleware',
 ...
 'scrapy.spidermiddlewares.depth.DepthMiddleware']
2016-09-22 23:37:45 [scrapy] INFO: Enabled item pipelines:
[]
2016-09-22 23:37:45 [scrapy] INFO: Spider opened
2016-09-22 23:37:45 [scrapy] INFO: Crawled 0 pages (at 0 pages/min), scraped 0 items (at 0 items/min)
2016-09-22 23:37:45 [scrapy] DEBUG: Telnet console listening on 127.0.0.1:6023
2016-09-22 23:37:47 [scrapy] DEBUG: Crawled (200)  (referer: None)
2016-09-22 23:37:47 [scrapy] INFO: Closing spider (finished)
2016-09-22 23:37:47 [scrapy] INFO: Dumping Scrapy stats:
{'downloader/request_bytes': 224,
 'downloader/request_count': 1,
 ...
 'scheduler/enqueued/memory': 1,
 'start_time': datetime.datetime(2016, 9, 23, 6, 37, 45, 995167)}
2016-09-22 23:37:47 [scrapy] INFO: Spider closed (finished)

Это большой выход, так что давайте разберемся.

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

  • Он использовал URL-адрес, который мы указали в спискеstart_urls, и захватил HTML, как и ваш веб-браузер.

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

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

[[step-2 -—- extract-data-from-a-page]] == Шаг 2 - Извлечение данных со страницы

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

Если вы посмотрите наthe page we want to scrape, вы увидите, что он имеет следующую структуру:

  • На каждой странице есть заголовок.

  • Есть некоторые данные поиска на высшем уровне, включая количество совпадений, то, что мы ищем, и крошки для сайта.

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

При написании скребка рекомендуется взглянуть на источник файла HTML и ознакомиться со структурой. Так вот, некоторые вещи удалены для удобства чтения:

brickset.com/sets/year-2016
  

Очистка этой страницы состоит из двух этапов:

  1. Во-первых, возьмите каждый набор LEGO, ища те части страницы, в которых есть нужные нам данные.

  2. Затем для каждого набора извлеките из него нужные нам данные, извлекая данные из тегов HTML.

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

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

scraper.py

class BrickSetSpider(scrapy.Spider):
    name = "brickset_spider"
    start_urls = ['http://brickset.com/sets/year-2016']

    def parse(self, response):
        SET_SELECTOR = '.set'
        for brickset in response.css(SET_SELECTOR):
            pass

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

Другой взгляд наsource страницы, которую мы анализируем, говорит нам, что имя каждого набора хранится в тегеh1 для каждого набора:

brickset.com/sets/year-2016

Brick Bank

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

scraper.py

class BrickSetSpider(scrapy.Spider):
    name = "brickset_spider"
    start_urls = ['http://brickset.com/sets/year-2016']

    def parse(self, response):
        SET_SELECTOR = '.set'
        for brickset in response.css(SET_SELECTOR):

            NAME_SELECTOR = 'h1 ::text'
            yield {
                'name': brickset.css(NAME_SELECTOR).extract_first(),
            }

[.note] #Note: Последняя запятая послеextract_first() не является опечаткой. Мы собираемся добавить что-то в этот раздел в ближайшее время, поэтому мы оставили запятую, чтобы облегчить добавление в этот раздел позже.
#

В этом коде вы заметите две вещи:

  • Мы добавляем::text к нашему селектору для имени. Это CSSpseudo-selector, который выбирает текстinside тегаa, а не сам тег.

  • Мы вызываемextract_first() для объекта, возвращаемогоbrickset.css(NAME_SELECTOR), потому что нам нужен только первый элемент, соответствующий селектору. Это дает намstring, а не список элементов.

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

scrapy runspider scraper.py

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

Output...
[scrapy] DEBUG: Scraped from <200 http://brickset.com/sets/year-2016>
{'name': 'Brick Bank'}
[scrapy] DEBUG: Scraped from <200 http://brickset.com/sets/year-2016>
{'name': 'Volkswagen Beetle'}
[scrapy] DEBUG: Scraped from <200 http://brickset.com/sets/year-2016>
{'name': 'Big Ben'}
[scrapy] DEBUG: Scraped from <200 http://brickset.com/sets/year-2016>
{'name': 'Winter Holiday Train'}
...

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

Взгляните еще раз на HTML для определенного набора:

brickset.com/sets/year-2016

Мы можем увидеть несколько вещей, изучив этот код:

  • Изображение для набора хранится в атрибутеsrc тегаimg внутри тегаa в начале набора. Мы можем использовать другой селектор CSS для извлечения этого значения, как мы делали это, когда брали имя каждого набора.

  • Получить количество штук немного сложнее. Существует тегdt, содержащий текстPieces, а затем тегdd, следующий за ним, который содержит фактическое количество частей. Мы воспользуемсяXPath, языком запросов для обхода XML, чтобы получить его, потому что он слишком сложен для представления с помощью селекторов CSS.

  • Получение количества минифиг в наборе похоже на получение количества штук. Есть тегdt, содержащий текстMinifigs, за которым следует тегdd с номером.

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

scraper.py

class BrickSetSpider(scrapy.Spider):
    name = 'brick_spider'
    start_urls = ['http://brickset.com/sets/year-2016']

    def parse(self, response):
        SET_SELECTOR = '.set'
        for brickset in response.css(SET_SELECTOR):

            NAME_SELECTOR = 'h1 ::text'
            PIECES_SELECTOR = './/dl[dt/text() = "Pieces"]/dd/a/text()'
            MINIFIGS_SELECTOR = './/dl[dt/text() = "Minifigs"]/dd[2]/a/text()'
            IMAGE_SELECTOR = 'img ::attr(src)'
            yield {
                'name': brickset.css(NAME_SELECTOR).extract_first(),
                'pieces': brickset.xpath(PIECES_SELECTOR).extract_first(),
                'minifigs': brickset.xpath(MINIFIGS_SELECTOR).extract_first(),
                'image': brickset.css(IMAGE_SELECTOR).extract_first(),
            }

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

scrapy runspider scraper.py

Теперь вы увидите эти новые данные в выходных данных программы:

Output2016-09-22 23:52:37 [scrapy] DEBUG: Scraped from <200 http://brickset.com/sets/year-2016>
{'minifigs': '5', 'pieces': '2380', 'name': 'Brick Bank', 'image': 'http://images.brickset.com/sets/small/10251-1.jpg?201510121127'}
2016-09-22 23:52:37 [scrapy] DEBUG: Scraped from <200 http://brickset.com/sets/year-2016>
{'minifigs': None, 'pieces': '1167', 'name': 'Volkswagen Beetle', 'image': 'http://images.brickset.com/sets/small/10252-1.jpg?201606140214'}
2016-09-22 23:52:37 [scrapy] DEBUG: Scraped from <200 http://brickset.com/sets/year-2016>
{'minifigs': None, 'pieces': '4163', 'name': 'Big Ben', 'image': 'http://images.brickset.com/sets/small/10253-1.jpg?201605190256'}
2016-09-22 23:52:37 [scrapy] DEBUG: Scraped from <200 http://brickset.com/sets/year-2016>
{'minifigs': None, 'pieces': None, 'name': 'Winter Holiday Train', 'image': 'http://images.brickset.com/sets/small/10254-1.jpg?201608110306'}
2016-09-22 23:52:37 [scrapy] DEBUG: Scraped from <200 http://brickset.com/sets/year-2016>
{'minifigs': None, 'pieces': None, 'name': 'XL Creative Brick Box', 'image': '/assets/images/misc/blankbox.gif'}
2016-09-22 23:52:37 [scrapy] DEBUG: Scraped from <200 http://brickset.com/sets/year-2016>
{'minifigs': None, 'pieces': '583', 'name': 'Creative Building Set', 'image': 'http://images.brickset.com/sets/small/10702-1.jpg?201511230710'}

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

[[step-3 -—- crawling-multiple-pages]] == Шаг 3 - сканирование нескольких страниц

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

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

brickset.com/sets/year-2016

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

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

scraper.py

class BrickSetSpider(scrapy.Spider):
    name = 'brick_spider'
    start_urls = ['http://brickset.com/sets/year-2016']

    def parse(self, response):
        SET_SELECTOR = '.set'
        for brickset in response.css(SET_SELECTOR):

            NAME_SELECTOR = 'h1 ::text'
            PIECES_SELECTOR = './/dl[dt/text() = "Pieces"]/dd/a/text()'
            MINIFIGS_SELECTOR = './/dl[dt/text() = "Minifigs"]/dd[2]/a/text()'
            IMAGE_SELECTOR = 'img ::attr(src)'
            yield {
                'name': brickset.css(NAME_SELECTOR).extract_first(),
                'pieces': brickset.xpath(PIECES_SELECTOR).extract_first(),
                'minifigs': brickset.xpath(MINIFIGS_SELECTOR).extract_first(),
                'image': brickset.css(IMAGE_SELECTOR).extract_first(),
            }

        NEXT_PAGE_SELECTOR = '.next 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
            )

Сначала мы определяем селектор для ссылки «следующая страница», извлекаем первое совпадение и проверяем, существует ли он. scrapy.Request - это значение, которое мы возвращаем, говоря «Эй, просканируйте эту страницу», аcallback=self.parse говорит: «Как только вы получили HTML-код с этой страницы, передайте его обратно этому методу, чтобы мы могли проанализировать извлеките данные и найдите следующую страницу ».

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

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

Вот наш законченный код для этого урока с использованием подсветки для Python:

scraper.py

import scrapy


class BrickSetSpider(scrapy.Spider):
    name = 'brick_spider'
    start_urls = ['http://brickset.com/sets/year-2016']

    def parse(self, response):
        SET_SELECTOR = '.set'
        for brickset in response.css(SET_SELECTOR):

            NAME_SELECTOR = 'h1 ::text'
            PIECES_SELECTOR = './/dl[dt/text() = "Pieces"]/dd/a/text()'
            MINIFIGS_SELECTOR = './/dl[dt/text() = "Minifigs"]/dd[2]/a/text()'
            IMAGE_SELECTOR = 'img ::attr(src)'
            yield {
                'name': brickset.css(NAME_SELECTOR).extract_first(),
                'pieces': brickset.xpath(PIECES_SELECTOR).extract_first(),
                'minifigs': brickset.xpath(MINIFIGS_SELECTOR).extract_first(),
                'image': brickset.css(IMAGE_SELECTOR).extract_first(),
            }

        NEXT_PAGE_SELECTOR = '.next 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
            )

Заключение

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

  1. В данный момент мы анализируем только результаты за 2016 год, как вы могли догадаться по части2016 вhttp://brickset.com/sets/year-2016 - как бы вы сканировали результаты за другие годы?

  2. Розничная цена включена в большинство комплектов. Как вы извлекаете данные из этой ячейки? Как бы вы получили из этого числа? Hint: вы найдете данные вdt точно так же, как количество частей и минифигурок.

  3. У большинства результатов есть теги, которые определяют семантические данные о наборах или их контексте. Как мы можем сканировать их, учитывая, что есть несколько тегов для одного набора?

Этого должно быть достаточно, чтобы вы думали и экспериментировали. Если вам нужна дополнительная информация о Scrapy, посмотритеScrapy’s official docs. Дополнительные сведения о работе с данными из Интернета см. В нашем руководстве по"How To Scrape Web Pages with Beautiful Soup and Python 3”.

Related