Веб-парсинг сайтов с помощью Scrapy: расширенные примеры

Оглавление

Введение в веб-скрейпинг

Вебскраппинг - это один из инструментов, имеющихся в распоряжении разработчика при сборе данных из Интернета. Хотя использование данных через API стало обычным делом, большинство сайтов в Интернете не имеют API для предоставления данных потребителям. Чтобы получить доступ к искомым данным, веб-скреперы и краулеры считывают страницы и ленты сайта, анализируя его структуру и язык разметки в поисках подсказок. Как правило, информация, полученная в результате скраппинга, передается в другие программы для проверки, очистки и ввода в хранилище данных или используется в других процессах, таких как цепочки инструментов обработки естественного языка (NLP) или модели машинного обучения (ML). Существует несколько пакетов Python, которые мы могли бы использовать в качестве иллюстрации, но в данном примере мы остановимся на Scrapy. Scrapy позволяет быстро создавать прототипы и разрабатывать веб-скреперы на Python.

Scrapy против Selenium и Beautiful Soup

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

Концепции Scrapy

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

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

import scrapy

class NewsSpider(scrapy.Spider):
	name = 'news'
	... 

Селекторы: Селекторы - это механизмы Scrapy для поиска данных на страницах сайта. Они называются селекторами, поскольку предоставляют интерфейс для "выбора" определенных частей HTML-страницы, причем эти селекторы могут быть как в CSS, так и в выражениях XPath.

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

import scrapy

class Article(scrapy.Item):
	headline = scrapy.Field()
	...

Первая страница без редактирования

Предположим, что нам нравятся изображения, размещенные на Reddit, но не нужны комментарии и самопосты. Мы можем использовать Scrapy для создания Reddit Spider, который будет получать все фотографии с главной страницы и помещать их на нашу собственную HTML-страницу, которую мы сможем просматривать вместо Reddit.

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

import scrapy

class RedditSpider(scrapy.Spider):
	name = 'reddit'
	start_urls = [
    	    'https://www.reddit.com'
	]

Выше мы определили RedditSpider, унаследовавший Spider от Scrapy. Мы назвали его reddit и заполнили атрибут start_urls класса URL-адресом Reddit, из которого мы будем извлекать изображения.

На этом этапе нам необходимо начать определять логику синтаксического анализа. Нам необходимо определить выражение, которое RedditSpider сможет использовать для определения того, найдено ли изображение. Если мы посмотрим на файл robots.txt Reddit, то увидим, что наш паук не может просматривать страницы с комментариями, не нарушая файл robots.txt, поэтому нам нужно будет перехватывать URL-адреса изображений, не переходя на страницы с комментариями.

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

class RedditSpider(scrapy.Spider):
    ...
    def parse(self, response):
       links = response.xpath('//a/@href')
    	 for link in links:
           ...

В методе parse нашего класса RedditSpider я начал определять, как мы будем разбирать наш ответ для получения результатов. Для начала мы перебираем все атрибуты href у ссылок страницы с помощью базового селектора XPath. Теперь, когда мы перечислили все ссылки страницы, можно приступить к анализу ссылок на предмет наличия изображений.

def parse(self, response):
    links = response.xpath('//a/@href')
    for link in links:
        # Extract the URL text from the element
        url = link.get()
        # Check if the URL contains an image extension
        if any(extension in url for extension in ['.jpg', '.gif', '.png']):
            ...

Для того чтобы получить доступ к текстовой информации из атрибута href ссылки, мы используем функцию Scrapy .get(), которая вернет назначение ссылки в виде строки. Далее мы проверяем, содержит ли URL расширение файла изображения. Для этого мы используем встроенную функцию Python any(). Она не является всеобъемлющей для всех расширений файлов изображений, но это только начало. Далее мы можем поместить наши изображения в локальный HTML-файл для просмотра.

def parse(self, response):
    links = response.xpath('//img/@src')
    html = ''

    for link in links:
        # Extract the URL text from the element
        url = link.get()
        # Check if the URL contains an image extension
        if any(extension in url for extension in ['.jpg', '.gif', '.png']):
            html += '''
            < a href="{url}" target="_blank">
                < img src="{url}" height="33%" width="33%" />
            < /a>
            '''.format(url=url)

    	# Open an HTML file, save the results
    	    with open('frontpage.html', 'a') as page:
            page.write(html)
    	    # Close the file
    	    page.close()

Для начала мы начинаем собирать содержимое HTML-файла в виде строки, которая в конце процесса будет записана в файл с именем frontpage.html. Вы заметите, что вместо того, чтобы извлекать местоположение изображения из ‘//a/@href/‘, мы обновили селектор links, чтобы использовать атрибут src изображения: ‘//img/@src’. Это позволит получить более стабильные результаты и выбирать только изображения.

По мере того как парсер RedditSpider's находит изображения, он строит ссылку с изображением превью и сбрасывает строку в нашу переменную html. После того как мы собрали все изображения и сгенерировали HTML, мы открываем локальный HTML-файл (или создаем его) и перезаписываем его нашим новым HTML-содержимым, после чего снова закрываем файл с помощью page.close(). Если мы выполним команду scrapy runspider reddit.py, то увидим, что файл создан правильно и содержит изображения с главной страницы Reddit.

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

Если мы посмотрим на frontpage.html, то увидим, что большая часть активов Reddit приходится на redditstatic.com и redditmedia.com. Мы просто отфильтруем эти результаты и сохраним все остальные. После этих обновлений наш класс RedditSpider теперь выглядит следующим образом:

import scrapy

class RedditSpider(scrapy.Spider):
    name = 'reddit'
    start_urls = [
        'https://www.reddit.com'
    ]

    def parse(self, response):
        links = response.xpath('//img/@src')
    	  html = ''

    	  for link in links:
            # Extract the URL text from the element
        	url = link.get()
        	# Check if the URL contains an image extension
        	if any(extension in url for extension in ['.jpg', '.gif', '.png'])\
               and not any(domain in url for domain in ['redditstatic.com', 'redditmedia.com']):
                html += '''
                < a href="{url}" target="_blank">
                    < img src="{url}" height="33%" width="33%" />
                < /a>
                '''.format(url=url)

    	   # Open an HTML file, save the results
    	       with open('frontpage.html', 'w') as page:
               page.write(html)

    	   # Close the file
    	   page.close()

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

Извлечение данных о ценах Amazon

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

В этой задаче мы будем извлекать данные о ценах из поисковых объявлений на Amazon и использовать полученные результаты для получения некоторых основных выводов. Если мы зайдем на страницу результатов поиска Amazon и осмотрим ее, то заметим, что Amazon хранит цену в ряде div'ов, в основном используя класс .a-offscreen. Мы можем сформулировать CSS-селектор, который извлекает цену со страницы:

prices = response.css('.a-price .a-offscreen::text').getall()

С учетом этого CSS-селектора построим наш AmazonSpider.

import scrapy

from re import sub
from decimal import Decimal


def convert_money(money):
	return Decimal(sub(r'[^\d.]', '', money))


class AmazonSpider(scrapy.Spider):
	name = 'amazon'
	start_urls = [
    	    'https://www.amazon.com/s?k=paint'
	]

	def parse(self, response):
    	    # Find the Amazon price element
    	    prices = response.css('.a-price .a-offscreen::text').getall()

    	    # Initialize some counters and stats objects
    	    stats = dict()
    	    values = []

    	    for price in prices:
        	  value = convert_money(price)
        	  values.append(value)

    	    # Sort our values before calculating
    	    values.sort()

    	    # Calculate price statistics
    	    stats['average_price'] = round(sum(values) / len(values), 2)
    	    stats['lowest_price'] = values[0]
    	    stats['highest_price'] = values[-1]
    	    Stats['total_prices'] = len(values)

    	    print(stats)

Несколько замечаний по поводу нашего класса AmazonSpider: convert_money(): Этот помощник просто преобразует строки в формате '$45.67' и приводит их к типу Python Decimal, который может быть использован для вычислений, и позволяет избежать проблем с локалью, не включая '$' в регулярное выражение. getall(): Функция .getall() - это функция Scrapy, работающая аналогично функции .get(), которую мы использовали ранее, но она возвращает все извлеченные значения в виде списка, с которым мы можем работать. Выполнив команду scrapy runspider amazon.py в папке проекта, мы получим вывод, похожий на следующий:

{'average_price': Decimal('38.23'), 'lowest_price': Decimal('3.63'), 'highest_price': Decimal('689.95'), 'total_prices': 58}

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

Соображения при масштабировании

По мере создания большего числа веб-краулеров и дальнейшего использования более продвинутых рабочих процессов скрапинга вы, вероятно, заметите несколько вещей:

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

Сайты меняются, сейчас как никогда

В некоторых случаях, например, AliExpress возвращает страницу входа в систему, а не поисковые объявления. Иногда Amazon решает ввести Captcha, или Twitter возвращает ошибку. Иногда эти ошибки могут быть просто "мерцанием", но в других случаях потребуется полностью перестроить архитектуру веб-скреперов. В настоящее время современные фронтенд-фреймворки часто предварительно компилируются для браузера, что может привести к искажению имен классов и идентификационных строк, а иногда дизайнер или разработчик меняет имя HTML-класса в процессе редизайна. Важно, чтобы наши краулеры Scrapy были устойчивыми, но не забывайте, что со временем в них будут происходить изменения.

Получение согласованных результатов на тысячах страниц - сложная задача

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

Соображения производительности могут иметь решающее значение

Прежде чем пытаться обработать с ноутбука 10 000 сайтов за одну ночь, необходимо убедиться, что он работает хотя бы умеренно эффективно. По мере роста массива данных работа с ним становится все более затратной с точки зрения памяти и вычислительной мощности. Аналогичным образом можно извлечь текст из одной новостной статьи за раз, а не загружать все 10 000 статей сразу. Как мы уже видели в этом учебном пособии, выполнять сложные операции скрапинга, используя фреймворк Scrapy, довольно просто. Следующие шаги могут включать загрузку селекторов из базы данных и скраппинг с использованием очень общих классов Spider, а также использование прокси-серверов или модифицированных user-agents для проверки изменения HTML в зависимости от местоположения или типа устройства. Скраппинг в реальном мире становится сложным из-за всех побочных эффектов, а Scrapy предоставляет простой способ построить эту логику на Python.

Вернуться на верх