Web scraping с помощью Scrapy и Python 3

Web scraping (также web spidering, кроулинг, «веб-паук») – это техника сбора данных о страницах для занесения этих данных в базу поисковой системы.

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

Данное руководство ознакомит вас с основами работы поисковых роботов.

Примечание: В руководстве используется BrickSet, поддерживаемый сообществом сайт о наборах LEGO. Выполнив руководство, вы получите полностью готового к работе «веб-паука», который проанализирует ряд страниц Brickset и извлечёт данные о наборах LEGO.

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

Требования

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

1: Базовый поиск

Scraping состоит из двух этапов:

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

Создать поискового робота с нуля можно с помощью различных модулей и библиотек, которые предоставляет язык программирования, однако в дальнейшем – по мере роста программы – это может вызвать ряд проблем. К примеру, вам понадобится переформатировать извлечённые данные в CSV, XML или JSON. Также вы можете столкнуться с сайтами, для анализа которых необходимы специальные настройки и модели доступа.

Поэтому лучше сразу разработать робота на основе библиотеки, которая устраняет все эти потенциальные проблемы. Для этого в данном руководстве используются Python и Scrapy.

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

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

Если вы следовали одному из предложенных руководств (раздел Требования), пакетный менеджер pip уже установлен на вашу машину. Чтобы установить Scrapy, введите:

pip install scrapy

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

После установки создайте папку для проекта.

mkdir brickset-scraper

Откройте новый каталог:

cd brickset-scraper

Создайте файл для поискового робота по имени scraper.py. В этом файле будет храниться весь код «паука». Введите в терминал:

touch scraper.py

Также вы можете создать файл с помощью текстового редактора или графического файлового менеджера.

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

  • name – название «паука».
  • start_urls – список ссылок на страницы, которые нужно проанализировать.

Откройте scrapy.py и добавьте следующий код:

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

Рассмотрим этот код подробнее:

  • Первая строка импортирует scrapy, что позволяет использовать доступные классы библиотеки.
  • Строка class BrickSetSpider(scrapy.Spider) добавляет класс Spider из библиотеки Scrapy и создаёт подкласс BrickSetSpider. Подкласс – это, по сути, просто более узкий, специализированный вариант родительского класса. Класс Spider предоставляет методы для отслеживания URL-ов и извлечения данных с веб-страниц, но он не знает, где искать страницы и какие именно данные нужно извлечь. Чтобы передать классу недостающие данные, мы создали подкласс.
  • Имя поискового робота: brickset_spider.
  • Ссылка в конце кода – это ссылка на страницу, которую нужно просканировать. Если вы откроете её в браузере, вы увидите первую страницу результатов поиска наборов LEGO.

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

scrapy runspider scraper.py

Команда вернёт:

2016-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) <GET http://brickset.com/sets/year-2016> (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, указанный в start_urls, и загружает HTML (как это делает браузер).
  • После этого робот передаёт HTML методу parse, который по умолчанию не делает ничего. Поскольку ранее мы не написали ни одно метода parse, «паук» больше ничего не делает.

Теперь попробуйте загрузить данные со страницы.

2: Извлечение данных с веб-страницы

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

Рассмотрите внимательно тестовую страницу. Она имеет такую структуру:

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

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

brickset.com/sets/year-2016
<body>
<section class="setlist">
<article class='set'>
<a class="highslide plain mainimg" href=
"http://images.brickset.com/sets/images/10251-1.jpg?201510121127"
onclick="return hs.expand(this)"><img src=
"http://images.brickset.com/sets/small/10251-1.jpg?201510121127"
title="10251-1: Brick Bank"></a>
<div class="highslide-caption">
<h1><a href='/sets/10251-1/Brick-Bank'>Brick Bank</a></h1>
<div class='tags floatleft'>
<a href='/sets/10251-1/Brick-Bank'>10251-1</a> <a href=
'/sets/theme-Advanced-Models'>Advanced Models</a> <a class=
'subtheme' href=
'/sets/theme-Advanced-Models/subtheme-Modular-Buildings'>Modular
Buildings</a> <a class='year' href=
'/sets/theme-Advanced-Models/year-2016'>2016</a>
</div>
<div class='floatright'>
&copy;2016 LEGO Group
</div>
<div class="pn">
<a href="#" onclick="return hs.previous(this)" title=
"Previous (left arrow key)">&#171; Previous</a> <a href="#"
onclick="return hs.next(this)" title=
"Next (right arrow key)">Next &#187;</a>
</div>
</div>
...
</article>
<article class='set'>
...
</article>
</section>
</body>

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

  1. Извлечение всех наборов LEGO после анализа тех компонентов, которые содержат нужные данные.
  2. Загрузка искомых данных по тегам HTML.

Scrapy извлекает данные на основе заданных селекторов. Селекторы – это шаблоны, которые позволяют найти элементы страницы, содержащие необходимые данные. Scrapy поддерживает селекторы CSS и XPath.

Мы используем CSS-селекторы, поскольку это самый простой вариант. Обратите внимание: каждый набор, опубликованный на странице, имеет класс set. Используйте CSS-селектор .set, чтобы выбрать этот класс. Передайте этот селектор объекту response:

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

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

Также можно заметить, что название наборов хранится в тегах a внутри тега h1.

brickset.com/sets/year-2016
<h1><a href='/sets/10251-1/Brick-Bank'>Brick Bank</a></h1>

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

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 a ::text'
yield {
'name': brickset.css(NAME_SELECTOR).extract_first(),
}

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

Обратите внимание:

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

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

scrapy runspider scraper.py

Теперь в выводе появятся названия наборов:

...
[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'}
...

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

brickset.com/sets/year-2016
<article class="set">
<a class="highslide plain mainimg" href="http://images.brickset.com/sets/images/10251-1.jpg?201510121127" onclick="return hs.expand(this)">
<img src="http://images.brickset.com/sets/small/10251-1.jpg?201510121127" title="10251-1: Brick Bank"></a>
...
<div class="meta">
<h1><a href="/sets/10251-1/Brick-Bank"><span>10251:</span> Brick Bank</a> </h1>
...
<div class="col">
<dl>
<dt>Pieces</dt>
<dd><a class="plain" href="/inventories/10251-1">2380</a></dd>
<dt>Minifigs</dt>
<dd><a class="plain" href="/minifigs/inset-10251-1">5</a></dd>
...
</dl>
</div>
...
</div>
</article>

Обратите внимание:

  • Изображение набора хранится в атрибуте src тега img внутри тега а в начале набора. Эти значения можно извлечь с помощью других селекторов CSS.
  • Получить набор компонентов набора немного сложнее. Тег dt содержит текст Pieces и тег dd, который содержит количество частей набора. Такой селектор сложно составить. Используйте язык запросов XPath, чтобы извлечь эти данные.
  • Получить количество фигурок в наборе можно так же, как и количество компонентов. Тег dt содержит текст Minifigs, за которым следует тег dd, который содержит количество фигурок.

Откорректируйте код поискового робота:

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 a ::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

Программа соберёт такие данные:

2016-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'}

Теперь можно превратить программу в «паука», который сможет переходить по ссылкам.

3: Анализ нескольких страниц

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

Вы, наверное, заметили, что каждая страница начинается и заканчивается символом >, который ссылается на следующую страницу результата.

brickset.com/sets/year-2016
<ul class="pagelength">
...
<li class="next">
<a href="http://brickset.com/sets/year-2017/page-2">&#8250;</a>
</li>
<li class="last">
<a href="http://brickset.com/sets/year-2016/page-32">&#187;</a>
</li>
</ul>

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

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 a ::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 страницы и передавать его методу для обработки, после чего он будет искать следующую страницу.

То есть, переходя на новую страницу, робот будет искать ссылку на следующую страницу. Поиск ссылок и переход по ним – очень важный аспект web scraping-а.

Сохраните код и запустите «паука», чтобы убедиться, что он работает должным образом. Он должен просмотреть все 779 результатов на 23 страницах.

В результате код «паука» имеет такой вид:

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 a ::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 год (http://brickset.com/sets/year-2016). Как обработать результаты за другие годы?
  2. Также на страницах можно найти цену каждого набора. Как извлечь эти данные? (Подсказка: эти данные можно найти в теге dt).
  3. Большая часть результатов содержит теги с семантическими данными о наборах. Попробуйте извлечь их.

Больше подробной информации о библиотеке Scrapy можно найти в официальной документации Scrapy.

Tags: , ,

1 комментарий

Добавить комментарий