Время прочтения: 9 мин.

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

Введение

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

Рассмотрим принцип работы парсинга. Данный процесс происходит в несколько этапов:

  1. Отправка HTTP-запроса на сервер.
  2. Поиск необходимых данных.
  3. Трансформация полученных данных.

При отправке HTTP-запроса на сервер у нас есть два варианта:

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

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

Основные проблемы парсинга

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

  • блокировка доступа к данным: использование CAPTCHA, блокирование IP-адресов и другое;
  • скорость выполнения: большой объем данных требует много ресурсов и времени;
  • сложность обработки ошибок: ошибки соединения, ошибки синтаксиса и другие;
  • работа с динамическим контентом: необходимо разрабатывать специальные инструменты для анализа сайтов, использующих технологии ajax и javascript.

Реализация парсера на основе Beautiful Soup. Обзор возможностей.

Beautiful Soup — это библиотека Python для извлечения данных из файлов форматов HTML и XML. Beautiful Soup (или BS4) использует DOM-модель (Document Object Model) для трансформации и извлечения данных.

Основными возможностями BS4 являются:

  • поиск элементов на странице по тегу, классу, id и другим атрибутам;
  • извлечение текста и атрибутов элементов;
  • навигация по дереву элементов страницы;
  • манипуляции с HTML-кодом, такие как добавление, удаление или изменение элементов.

Для извлечения данных из HTML-кода необходимо использовать конструктор BeautifulSoup(), который принимает два аргумента: разметку (HTML-код) и анализатор (необходим для обработки HTML-кода). BS4 поддерживает различные библиотеки для синтаксического анализа, включая стандартные html.parser, а также более быстрые, такие как lxml и html5lib. В нашем случае будем использовать lxml. Также, для отправки запросов на сайт воспользуемся библиотекой requests.

Реализация кода на базе Beautiful Soup

Для начала установим и импортируем библиотеки.

# установка 
pip install beautifulsoup4
pip install requests
pip install lxml
# импорт
from bs4 import BeautifulSoup
import requests

Проиллюстрируем пример парсинга одной страницы многостраничного сайта www.banki.ru. Данную задачу можно разбить на два этапа:

  1. Выгрузка необходимых данных;
    В нашем случае мы используем отзывы на инвестиционные компании.
  2. Парсинг HTML-кода в удобный формат.
    В нашем случае будем сохранять информацию в файл с расширением .json.
# веб-сайт, который хотим спарсить
url = 'https://www.banki.ru/investment/responses/list/'
# определим главную страницу
root = 'https://www.banki.ru'

# GET()-запрос на сайт
page = requests.get(url)
# проверка подключения
print(page.status_code)

# получаем содержимое страницы
content = page.text
# создаем объект BeautifulSoup
soup = BeautifulSoup(content, "lxml")

# сохраняем все с тэгом 'article' и классом 'responses__item'
# в них будет находится весь код отзыва, включая заголовок, оценку, текст и прочее
allfd = soup.find_all('article', class_="responses__item")

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

# воспользуемся циклом, чтобы получить все необходимые ссылки
links = []
for href in allfd:
    links.append(href.find('a', href=True)['href'])

Пройдёмся по каждой ссылке в цикле и сохраним информацию о заголовке и тексте отзыва. На последнем этапе выгрузим полученную информацию в файл banks_one_page.json.

with open ('banks_one_page.json', 'a+', encoding="utf-8") as file:
    for link in links:
        result = requests.get(f'{root}{link}')
        if (page.status_code == 200):
            content = result.text
            soup = BeautifulSoup(content, 'lxml')
            fd = soup.find('div', class_='article-text response-page__text markup-inside-small markup-inside-small--bullet').get_text(strip=True, separator=' ')
            title = soup.find('h1').get_text(strip=True, separator=' ')
            json.dump({'title': title,'review': fd}, file, ensure_ascii=False, indent=4)

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

i = 1
page = requests.get(f'https://www.banki.ru/investment/responses/list?page={i}&isMobile=0')
with open ('banks_all_pages.json', 'a+', encoding="utf-8") as file:
    while (page.status_code == 200):
        content = page.text
        soup = BeautifulSoup(content, "lxml")
        allfd = soup.find_all('article', class_="responses__item")

        links = []
        for href in allfd:
            links.append(href.find('a', href=True)['href'])

        for link in links:
            result = requests.get(f'{root}{link}')
            if (page.status_code == 200):
                #time.sleep(1)
                content = result.text
                soup = BeautifulSoup(content, 'lxml')
                fd = soup.find('div', class_='article-text response-page__text markup-inside-small markup-inside-small--bullet').get_text(strip=True, separator=' ')
                title = soup.find('h1').get_text(strip=True, separator=' ')
                json.dump({'title': title,'review': fd}, file, ensure_ascii=False, indent=4)
        i += 1
        page = requests.get(f'https://www.banki.ru/investment/responses/list?page={i}&isMobile=0')

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

Реализация парсера на основе Scrapy. Обзор возможностей.

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

Основными возможностями Scrapy являются:

  • автоматическая обработка запросов и ответов с использованием асинхронности;
  • извлечение данных из HTML и XML документов с помощью XPath и CSS-селекторов;
  • эффективная обработка веб-форм и управление сессиями;
  • расширяемость за счет огромного количества плагинов, упрощающих разработку и настройку веб-пауков.

Реализация кода на базе Scrapy

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

Будем работать через терминал anaconda.

conda install -c conda-forge scrapy

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

# banks_one_page — имя проекта
scrapy startproject banks_one_page
# перейдём в созданный каталог
cd banks_one_page

Перед парсингом сайта необходимо пояснить, что же такое веб-паук.

Веб-паук — класс, определяющий алгоритм сканирования веб-страниц. Существует несколько типов веб-пауков:

  • scrapy.Spider – веб-паук, предоставляющий базовую структуру и функциональность для создания других пауков;
  • Generic Spiders – шаблоны веб-пауков, которые можно использовать для создания подклассов scrapy.Spider:
    • CrawlSpider – веб-паук, использующий набор правил (rules), задаваемых пользователем, каждое из которых определяет поведение для сканирования сайта;
    • XMLFeedSpiders – веб-паук, предназначенный для извлечения данных из XML-файлов;
    • CSVFeedSpider – веб-паук, предназначенный для извлечения данных из CSV-файлов;
    • SitemapSpider – веб-паук, предназначенный для извлечения данных из sitemap.xml файлов, в которых хранится информация о URL-адресах, доступных для сканирования.

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

# one_page — имя веб-паука
# после конструкции -t указываем необходимый шаблон паука, в нашем случае crawl
scrapy genspider -t crawl one_page www.banki.ru/investment/responses/list

Теперь в папке со всеми веб-пауками spiders, которая находится внутри проекта, должен быть создан новый скрипт с именем паука one_page, в котором записан шаблон для сканирования веб-сайта.

Далее запустим оболочку Scrapy для нашего сайта. Это необходимо для тестирования кода.

scrapy shell
url = 'https://www.banki.ru/investment/responses/list?page=1&isMobile=0'
r = scrapy.Request(url)
fetch(r)

На этом этапе мы столкнулись с ошибкой HTTP 429 Too Many Requests. Чаще всего эта ошибка означает, что количество запросов к сайту достигло предела. Но в нашем случае мы отправили всего один запрос, поэтому, скорее всего, веб-сайт блокирует наши запросы, потому что не может нас идентифицировать.

В данном случае нам следует передать информацию о клиенте в User-Agent. В оболочке scrapy это можно сделать следующий образом.

r = scrapy.Request(url, headers={'User-Agent': ' Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 YaBrowser/23.5.2.625 Yowser/2.5 Safari/537.36'}) # Был использован User-Agent моего браузера.  При реализации кода вы должен использовать свой User-Agent.

fetch(r)
# получим html-текст страницы
response.body

Мы получили код «200», что говорит об успешно выполненном запросе.

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

response.xpath('//h1/text()').get()

Откроем скрипт one_page.py и пропишем работу паука. Перед этим необходимо настроить USER_AGENT в settings.py.

import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule

class OnePageSpider(CrawlSpider):
    # name – имя паука
    name = "one_page"
    # allowed_domains – домены сайта, в пределах которого необходимо сканировать
    allowed_domains = ["www.banki.ru"]
    # start_urls – список начальных адресов
    start_urls = ["https://www.banki.ru/investment/responses/list"]
    
    # rules - правила, определяющие поведение паука
    # первое правило: проваливаемся внутрь отзыва для того, чтобы достать заголовок и текст отзыва
    rules = (Rule(LinkExtractor(restrict_xpaths="//div[@class = 'responses__item__message']/a"), callback='parse_item', follow = True),
            )
    def parse_item(self, response):
        item = {}
        item['title'] = response.xpath("//h1[contains(@class, 'response-page__title')]/text()").get().strip()
        item['review'] = response.xpath("//div[contains(@class, 'article-text')]/text()").get().strip()
        return item

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

scrapy crawl one_page -o feedback_one_page.json

Произведём парсинг многостраничного сайта.

# all_pages — имя веб-паука
scrapy genspider –t crawl all_pages www.banki.ru/investment/responses/list

Откроем скрипт all_pages.py и пропишем работу паука.

import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule

class AllPagesSpider(CrawlSpider):
    # name – имя паука
    name = "all_pages"
    # allowed_domains – домены сайта, в пределах которого необходимо сканировать
    allowed_domains = ["www.banki.ru"]
    # start_urls – список начальных адресов
    start_urls = ["https://www.banki.ru/investment/responses/list"]
    """
    rules - правила, определяющие поведение паука
    первое правило: проваливаемся внутрь отзыва для того, чтобы достать заголовок и текст отзыва
    второе правило: переходим по страницам
    """
    rules = (Rule(LinkExtractor(restrict_xpaths="//div[@class = 'responses__item__message']/a"), callback='parse_item', follow = True)
             ,Rule(LinkExtractor(restrict_xpaths="//li[@class='ui-pagination__item ui-pagination__next']/a"))
            )

    def parse_item(self, response):
        item = {}
        item['title'] = response.xpath("//h1[contains(@class, 'response-page__title')]/text()").get().strip()
        item['review'] = response.xpath("//div[contains(@class, 'article-text')]").get().strip()
        return item

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

scrapy crawl all_pages -o feedback_all_pages.json

В результате мы получили файл с названием и текстом отзыва, скрипт отработал значительно быстрее, чем BS4.

Краткие итоги

Сравнительная таблица работы BeautifulSoup и Scrapy:

Вывод

Каждый инструмент заслуживает отдельного внимания, нельзя явно указать, что один из них лучше другого, потому что для каждого типа задачи необходимо выбирать тот инструмент, который лучше для нее подходит. Если вам нужно быстро создавать масштабируемые и мощные парсеры – ваш выбор однозначно Scrapy, однако BeautifulSoup дает вам большое поле для экспериментов, позволяя настраивать свои параметры с нуля, что крайне необходимо в неклассических задачах, которые могут возникнуть в вашей работе.

Описать все детали в одной публикации невозможно, поэтому, для более глубокого/детального изучения, предлагаю воспользоваться следующими ресурсами:

  1. Документация:
    1. BS4: https://beautiful-soup-4.readthedocs.io/en/latest/
    1. Scrapy: https://docs.scrapy.org/en/latest/
  2. Статьи:
    1. High performance distributed web scraper: https://cyberleninka.ru/article/n/high-performance-distributed-web-scraper/viewer
  3. Курсы:
    1. Udemy.
      Frank Andrade «Web Scraping in Python BeautifulSoup, Selenium & Scrapy 2023»: https://www.udemy.com/course/web-scraping-course-in-python-bs4-selenium-and-scrapy/