Блог

Дропшиппинг интернет-магазин на Django (часть 6)

6. Добавление модуля скрейпинга (парсинга) и автозаполнение БД прямо из интернета!

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

Содержание курса:

Как получить информацию с другого сайта?

Конечно же, самым удобным (и, кстати, более надёжным) способом будет взаимодействие нашего сайта с сайтом-поставщиком по API. Правда, для этого необходимо, чтобы

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

И хотя фреймворк Django позволяет организовать подобное взаимодействия собственными средствами, не прибегая к установке сторонних пакетов, тем не менее, лучше всего с решением этой задачи справится пакет Django REST ftamework (DRF), который обязательно будет рассмотрен в одном из наших ближайших курсов.

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

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

pip install beautifulsoup4
pip install requests

Структура веб страницы

Обычно данные на странице товаров сгруппированы в блоки. Внутри блоков однотипные данные находятся под одинаковыми селекторами (см. рисунок):

Если скачать и “распарсить” HTML-страницу со списком товаров, мы можем получить структурированный список данных. Конкретно для нашего случая по каждому блоку данных нам нужно получить вот такой словарь:

{
    'name': 'Труба профильная 40х20 2 мм 3м', 
    'image_url': 'https://my-website.com/30C39890-D527-427E-B573-504969456BF5.jpg', 
    'price': Decimal('493.00'), 
    'unit': 'за шт', 
    'code': '38140012'
 }

План действий

  • Создать модуль scraping.py в приложении shop
  • В этом модуле создать функцию scraping(), которая сможет:
    1. Получить HTML-код страницы (с помощью request)
    2. Обработать полученный HTML-код (с помощью beautifulsoup4)
    3. Сохранить результат в базе данных
  • Проверить работу функции scraping() “вручную”
  • Добавить кнопку запуска скрейпинга на страницу сайта

План готов - приступаем к его реализации!

Создаём модуль скрейпинга (парсинга) и получаем HTML-код с помощью пакета requests

Очевидно, что скрипт, отвечающий за считывание информации с другого сайта следует разместить в отдельном модуле приложения shop: shop/scraping.py. За отправку запроса по адресу URL_SCRAPING, считывание данных и запись этих данных в таблицу Product базы данных проекта, будет отвечать функция scraping().

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

import requests

def scraping():
    URL_SCRAPING = 'https://www.some-site.com'
    resp = requests.get(URL_SCRAPING, timeout=10.0)
    if resp.status_code != 200:
        raise Exception('HTTP error access!')

    data_list = []
    html = resp.text

Есть смысл сразу же посмотреть, что получили. Для этого добавим код, который подсчитает число символов в объекте html и заодно выведет на печать сам этот объект:

html = resp.text
    print(f'HTML text consists of {len(html)} symbols')
    print(50 * '=')
    print(html)


if __name__ == '__main__':
    scraping()

Модуль shop/scraaping.py не требует настроек Django (во всяком случае пока), поэтому запустить его можно как обычный Python-скрипт:

HTML text consists of 435395 symbols
==================================================
<!DOCTYPE html>
<html lang="ru">
  <head>
    <link rel="shortcut icon" type="image/x-icon" href="/bitrix/templates/elektro_light/favicon_new.ico"/>
    <meta name="robots" content="index, follow">
<meta name="keywords" content="Профильные трубы, уголки">
<meta name="description" content="Цены на профильные трубы, уголки от  руб. Описание. Характеристики. Отзывы. Скидки на  профильные трубы, уголки.">
    <meta name="viewport" content="width=device-width, initial-scale=1.0 user-scalable=no"/>
    <meta name="msapplication-TileColor" content="#ffffff">

Как видим результат действительно похож на HTML-страницу.

Первая часть задачи решена - доступ к данным сайта получен, и в тех 435 395 символах, что вывелась на экран, содержится вся необходимая нам информация. Всё, что нам теперь требуется - просто извлечь эту информацию и сохранить результат в БД.

Обрабатываем полученный HTML-код с помощью пакета BeautifulSoup

Дальнейшую обработку удобнее всего будет вести с помощью модуля beautifulsoup4. Для этого нам потребуется сначала создать объект soup, представляющий собой вложенную структуру данных HTML-документа:

from bs4 import BeautifulSoup

soup = BeautifulSoup(html, 'html.parser')

Более подробную информацию о том, как начать работать с этим пакетом можно найти на странице руководства: BeautifulSoup #quick-start

Также очень полезной может быть дополнительная информация о CSS-селекторах пакета beautifulsoup4, которую можно прочитать здесь: BeautifulSoup #css-selectors

Далее на странице поставщика более всего нас будет интересовать блок товаров - вёрстка повторяющихся карточек товара, имеющих сходную структуру данных. Получить список однотипных элементов из объекта soup можно с помощью метода select(), где в качестве аргумента указан CSS-selector этого блока. В нашем случае это будет class=”catalog-item-card”:

blocks = soup.select('.catalog-item-card ')

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

html = resp.text

    soup = BeautifulSoup(html, 'html.parser')
    blocks = soup.select('.catalog-item-card ')

    for block in blocks:
        print(f'HTML text consists of {len(block.text)} symbols')
        print(50 * '=')
        print(block.text)
        break

А вот так будет выглядеть распечатанный объект block.text:

HTML text consists of 382 symbols
==================================================
<div class="catalog-item-card" itemprop="itemListElement" itemscope="" itemtype="http://schema.org/Product">
<div class="catalog-item-info">
<div class="item-all-title">
<a class="item-title" href="/catalog/profilnye_truby_ugolki/truba_profilnaya_40kh20_2_mm_3m/" itemprop="url" title="Труба профильная 40х20 2 мм 3м">
<span itemprop="name">Труба профильная 40х20 2 мм 3м</span>
</a>

Как видим, число символов в блоке сократилось до 382-х. Что существенно упрощает нашу задачу.

Мы можем “разобрать” эти блоки на интересующие нас элементы с помощью метода soup.select_one(), который, в отличие от метода select(), выбирает не все элементы страницы, удовлетворяющее условию (аргументу метода), а только первый совпавший элемент. Важно также помнить, что текст, из полученного с помощью soup.select_one() объекта, можно извлечь с помощью метода text. Таким образом, применяя этот метод с определёнными аргументами, мы заполняем практически весь словарь данных, за исключением поля code:

soup = BeautifulSoup(html, 'html.parser')
    blocks = soup.select('.catalog-item-card ')

    for block in blocks:
        """{
        'name': 'Труба профильная 40х20 2 мм 3м', 
        'image_url': 'https://my-website.com/30C39890-D527-427E-B573-504969456BF5.jpg', 
        'price': Decimal('493.00'), 
        'unit': 'за шт', 
        'code': '38140012'
        }
        """
        data = {}
        name = block.select_one('.item-title[title]').get_text().strip()
        data['name'] = name

        image_url = URL_SCRAPING_DOMAIN + block.select_one('img')['src']
        data['image_url'] = image_url

        price_raw = block.select_one('.item-price ').text
        # '\r\n \t\t\t\t\t\t\t\t\t\t\t\t\t\t493.00\t\t\t\t\t\t\t\t\t\t\t\t  руб. '
        price = re.findall(r'\S\d+\.\d+\S', price_raw)[0]
        price = Decimal(price)
        data['price'] = price   # 493.00

        unit = block.select_one('.unit ').text.strip()
        # '\r\n \t\t\t\t\t\t\t\t\t\t\t\t\t\tза шт\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'
        data['unit'] = unit  # 'за шт'

Формируем дополнительную ссылку для перехода на страницу детализации и получение кода продукта

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

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

  • Сформировать ссылку на страницу детализации
  • Перейти по этой ссылке и считать с помощью requests.get() новый HTML-код уже этой страницы - страницы детализации
  • Сохранить полученные данные в новом объекте Beautiful Soup
  • Извлечь номер кода с помощью всё того же метода soup.select_one()

# find and open detail page
        url_detail = block.select_one('.item-title')
        # <a class="item-title" href="/catalog/profilnye_truby_ugolki/truba_profilnaya_40kh20_2_mm_3m/" itemprop="url" title="Труба профильная 40х20 2 мм 3м">

        url_detail = url_detail['href']
        # '/catalog/profilnye_truby_ugolki/truba_profilnaya_40kh20_2_mm_3m/'

        url_detail = URL_SCRAPING_DOMAIN + url_detail

        html_detail = requests.get(url_detail).text
        soup = BeautifulSoup(html_detail, 'html.parser')
        code_block = soup.select_one('.catalog-detail-property')
        code = code_block.select_one('b').text
        data['code'] = code

        data_list.append(data)

        print(data)

Если мы всё сделать правильно, то в итоге получим список словарей с данными по каждому блоку.

Добавляем обработку ошибок

Успех скрейпинга сайтов зависит от целого ряда параметров и обстоятельств. И большинство из них не зависит от нашего кода в Django, а именно:

  • Доступность (или НЕдоступность сайта-поставщика)
  • Изменение вёрстки страницы
  • Проблемы интернет-соединения
  • и так далее…

Разумеется, избежать всех этих ошибок не в наших силах. Но в наших силах перехватить эти ошибки, чтобы понять и, по-возможности, минимизировать или обойти причину их возникновения.

Удобнее всего это сделать с помощью класса Исключений (Exceptions). Прежде всего создаём класс исключений скрейпинга ScrapingError. И далее Наследует от него 3 новый пользовательских класса:

class ScrapingError(Exception):
    pass


class ScrapingTimeoutError(ScrapingError):
    pass


class ScrapingHTTPError(ScrapingError):
    pass


class ScrapingOtherError(ScrapingError):
    pass

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

try:
        resp = requests.get(URL_SCRAPING, timeout=10.0)
    except requests.exceptions.Timeout:
        raise ScrapingTimeoutError("request timed out")
    except Exception as e:
        raise ScrapingOtherError(f'{e}')

    if resp.status_code != 200:
        raise ScrapingHTTPError(f"HTTP {resp.status_code}: {resp.text}")

Сохраняем полученные данные в БД

Итак, данные по каждому блоку получены и преобразованы в удобный формат (список словарей). Теперь остаётся добавить эти данные в базу данных. Иными словами, заменить строчку кода print(data) на код, заполняющий таблицу Product:

for item in data_list:
        if not Product.objects.filter(code=item['code']).exists():
            Product.objects.create(
                name=item['name'],
                code=item['code'],
                price=item['price'],
                unit=item['unit'],
                image_url=item['image_url'],
            )

    return data_list

Как видно, продукт добавляется только в том случае, если его ещё нет в БД. Поиск ведётся по номеру кода продукта (значение поля code ).

Несмотря на то, что в самой функции scraping.py данные уже записываются в БД, мы всё равно возвращаем список data_list. Просто на всякий случай).

Однако, если сейчас мы попробуем воспроизвести этот скрипт, то получим ошибку:

"/home/su/Projects/django-apps/Projects/drop-ship-store/venv/lib/python3.8/site-packages/django/conf/__init__.py", line 67, in _setup
    raise ImproperlyConfigured(
django.core.exceptions.ImproperlyConfigured: Requested setting INSTALLED_APPS, but settings are not configured. You must either define the environment variable DJANGO_SETTINGS_MODULE or call settings.configure() before accessing settings.

Process finished with exit code 1

Всё дело в том, что теперь скрипт обращается к БД, а значит, требуется получить установки Django. Можно запустить этот код для проверки в management/commands (более подробно об этом можно найти здесь: https://docs.djangoproject.com/en/4.0/howto/custom-management-commands/) Но мы поступим иначе: сразу же добавим страницу запуска и проверим работу функции scraping() уже там.

Переносим управление скрейпингом на страницу сайта

Алгоритм добавления новой страницы остаётся прежним:

  • Придумывает url, который будет её вызывать (shop/fill-database/)
  • Добавляем конфигуратор urls.py в приложение shop
  • Устанавливаем в urls.py связь url и view (path('fill-database/', views.fill_database, name='fill_database'),
  • Переносим (копируем) файл из шаблона в проект
  • Создаём view в модуле shop/views.py
  • Проверяем результат!

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

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

Более подробно с со всеми деталями этого этапа вы сможете познакомиться из этого видео:






Переход на следующий этап проекта