Всем привет! В этой статье я решил показать один из методов парсинга на Python на примере маркетплейса Wildberries.

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

В проекте будут использоваться следующие библиотеки:

  1. requests - для парсинга данных API.

  2. aiogram 3.10.0 - одна из самых популярных библиотек для разработки telegram ботов.

Работа проекта

Приложение будет отправлять GET запрос к API, получая на выходе данные о карточках товара. Далее мы отфильтруем требуемые данные, такие как название бренда, название товара и т.п., после чего "завернем" приложение в telegram-бота. И естественно не забудем про деплой.

Wildberries, как и другие крупные сервисы могут блокировать IP бота-парсера, поэтому я предлагаю использовать proxies.

Разбор API

Как было уточнено выше - wildberries использует API для получения интересующей нам информации на странице. Давайте перейдем на сайт и откроем любую категорию. Пусть это будет Электроника -> Гарнитура и наушники.

Теперь клавишей откроем инструмент разработчика, переключимся во вкладку network для вывода запросов, которые отправляются сайтом и выберем фильтр Fetch/XHR. Перезагрузим страницу.

Здесь нас интересует запрос, начинающийся на catalog?. Кликаем на него, и, изучая содержимое во вкладке Preview, можем перейти по ключу data - products и увидеть содержащиеся внутри данные продуктов!

Теперь мы знаем как выглядит запрос и можем перейти к написанию кода, так как совсем скоро нам понадобятся данные из DevTools.

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

Сначала импортируем необходимую библиотеку - requests, объявим переменную для прокси и зададим структуру main.py.

import requests

proxies = 'ЗАПИШИТЕ-СЮДА-СВОЙ-ПРОКСИ-В-НУЖНОМ-ФОРМАТЕ'

def get_category():
	pass 
	
def format_items(response):
    pass
            
def main():
    pass

if __name__ == '__main__':
    main()

Как вы видите, будет использоваться 3 основных функции:

  • В get_category() мы укажем url и headers, которые мы получим, скопировав curl (bash) запроса через DevTools. И вернем response запроса GET в json:

def get_category():
    url = 'https://catalog.wb.ru/catalog/electronic14/v2/catalog?ab_testing=false&appType=1&cat=9468&curr=rub&dest=-1185367&sort=popular&spp=30'
    
    headers = {
        'Accept': '*/*',
        'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7',
        'Connection': 'keep-alive',
        'DNT': '1',
        'Origin': 'https://www.wildberries.ru', 
        'Referer': 'https://www.wildberries.ru/catalog/elektronika/igry-i-razvlecheniya/aksessuary/garnitury',
        'Sec-Fetch-Dest': 'empty',
        'Sec-Fetch-Mode': 'cors',
        'Sec-Fetch-Site': 'cross-site',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36',
        'sec-ch-ua': '"Not)A;Brand";v="99", "Google Chrome";v="127", "Chromium";v="127"',
        'sec-ch-ua-mobile': '?0',
        'sec-ch-ua-platform': '"Windows"',
    }
    
    response = requests.get(url=url, headers=headers, proxies=proxies)
    
    return response.json()
  • format_items() - принимает response запроса. Здесь с помощью цикла for пробежимся по данным товаров, предварительно сделав проверки на наличие самих товаров, и запишем все в products:

def format_items(response):
    products = []
    
    products_raw = response.get('data', {}).get('products', None)
    
    if products_raw != None and len(products_raw) > 0:
        for product in products_raw:
            print(product.get('name', None))
            products.append({
                'brand': product.get('brand', None),
                'name': product.get('name', None),
                'id': product.get('id', None),
                'reviewRating': product.get('reviewRating', None),
                'feedbacks': product.get('feedbacks', None),
            })
            
            
    return products

Получаем products_raw (все продукты) с помощью метода get в response. Нам нужно дойти до products, который мы видели в инструменте разработчика в браузере. Для этого сначала получаем содержимое data, а после - самого products.

Что за метод GET и как с ним работать? Сначала берется любой ключ, в нашем случае - это product. И в методе get, первым параметром указывается название следующего ключа/переменной, содержащие в себе какое-то значение. Вторым параметром указывается то, что будет возвращено, если такого ключа/переменной не найдется. Если найдено - возвращается словарь с содержащимися внутри ключа/переменной данными. С помощью append мы записываем словарь с данными в массив products, объявленный в начале функции.

  • в main() вызовем функции и попробуем вывести данные карточек:

def main():
    response = get_category()
    products = format_items(response)
    
    print(products)

Теперь можно запустить. На выходе успешно получаем выделенные данные!

Адаптируем код для работы бота

Когда логика парсера готова, мы можем написать бота. Он будет простым - бот не будет давать пользователю выбрать категорию, а просто отправит 10 карточек по выбранной нами категории.

Добавим нужные импорты:

import os
import asyncio
import time

import logging

from aiogram import Bot, Dispatcher, types
from aiogram.filters import CommandStart
from aiogram.enums.parse_mode import ParseMode
from aiogram.types.inline_keyboard_button import InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder

Запустим logging и объявим класс бота с диспетчером и переменную прокси:

logging.basicConfig(level=logging.INFO)

proxies = os.getenv('PROXIES')

bot = Bot(os.getenv("TOKEN"))
dp = Dispatcher()

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

Теперь немного изменим main(), сделаем его асинхронным и вместо обычного вызова переменной main, используем asyncio для запуска асинхронных функций:

async def main():
    await bot.delete_webhook(drop_pending_updates=True)
    await dp.start_polling(bot)
    
if __name__ == '__main__':
    asyncio.run(main())

И самое главное: обработчик команды /start:

@dp.message(CommandStart)
async def start(message: types.Message):
    response = get_category()
    products = format_items(response)
    
    items = 0
    
    for product in products:
        text=f"<b>Категория</b>: Гарнитуры и наушники\n\n<b>Название</b>: {product['name']}\n<b>Бренд</b>: {product['brand']}\n\n<b>Отзывов всего</b>: {product['feedbacks']}\n<b>Средняя оценка</b>: {product['reviewRating']}"
        
        builder = InlineKeyboardBuilder()
        builder.add(InlineKeyboardButton(text="Открыть", url=f"https://www.wildberries.ru/catalog/{product['id']}/detail.aspx"))
        
        await message.answer(text, parse_mode=ParseMode.HTML, reply_markup=builder.as_markup())
        
        if items >= 10:
            break
        items += 1
        
        time.sleep(0.3)

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

Бот отправляет до 10 самых популярных товаров (фильтр можно задать в параметрах url в первой функции) с перерывами в 0.3 секунды, чтобы API telegram не жаловался на слишком частые отправки сообщений.

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

Запускаем, и при команде старт видим, что все работает!

Весь код выглядит так:

import requests
import os
import asyncio
import time

import logging

from aiogram import Bot, Dispatcher, types
from aiogram.filters import CommandStart
from aiogram.enums.parse_mode import ParseMode
from aiogram.types.inline_keyboard_button import InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder

logging.basicConfig(level=logging.INFO)

proxies = os.getenv('PROXIES')

bot = Bot(os.getenv("TOKEN"))
dp = Dispatcher()

def get_category():
    url = 'https://catalog.wb.ru/catalog/electronic14/v2/catalog?ab_testing=false&appType=1&cat=9468&curr=rub&dest=-1185367&sort=popular&spp=30'
    
    headers = {
        'Accept': '*/*',
        'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7',
        'Connection': 'keep-alive',
        'DNT': '1',
        'Origin': 'https://www.wildberries.ru', 
        'Referer': 'https://www.wildberries.ru/catalog/elektronika/igry-i-razvlecheniya/aksessuary/garnitury',
        'Sec-Fetch-Dest': 'empty',
        'Sec-Fetch-Mode': 'cors',
        'Sec-Fetch-Site': 'cross-site',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36',
        'sec-ch-ua': '"Not)A;Brand";v="99", "Google Chrome";v="127", "Chromium";v="127"',
        'sec-ch-ua-mobile': '?0',
        'sec-ch-ua-platform': '"Windows"',
    }
    
    response = requests.get(url=url, headers=headers, proxies=proxies)
    
    return response.json()

def format_items(response):
    products = []
    
    products_raw = response.get('data', {}).get('products', None)
    
    if products_raw != None and len(products_raw) > 0:
        for product in products_raw:
            products.append({
                'brand': product.get('brand', None),
                'name': product.get('name', None),
                'id': product.get('id', None),
                'reviewRating': product.get('reviewRating', None),
                'feedbacks': product.get('feedbacks', None),
            })
            
    return products
            
@dp.message(CommandStart)
async def start(message: types.Message):
    response = get_category()
    products = format_items(response)
    
    items = 0
    
    for product in products:
        text=f"<b>Категория</b>: Гарнитуры и наушники\n\n<b>Название</b>: {product['name']}\n<b>Бренд</b>: {product['brand']}\n\n<b>Отзывов всего</b>: {product['feedbacks']}\n<b>Средняя оценка</b>: {product['reviewRating']}"
        
        builder = InlineKeyboardBuilder()
        builder.add(InlineKeyboardButton(text="Открыть", url=f"https://www.wildberries.ru/catalog/{product['id']}/detail.aspx"))
        
        await message.answer(text, parse_mode=ParseMode.HTML, reply_markup=builder.as_markup())
        
        if items >= 10:
            break
        items += 1
        
        time.sleep(0.3)
            
async def main():
    await bot.delete_webhook(drop_pending_updates=True)
    await dp.start_polling(bot)
    
if __name__ == '__main__':
    asyncio.run(main())

Деплой бота в Amvera

Разворачивать нашего бота мы будем в сервисе Amvera.

Amvera - одно из лучший решений для быстрого деплоя ботов из-за своей простоты в загрузке файлов (через git или интерфейс) и простоты настройки (вам не нужно настраивать виртуальную машину, зависимости и т.д.). Единственное, что надо настроить - переменные окружения, задать параметры в конфигурации и создать файл зависимостей (requirements.txt). Хоть это и звучит страшно, но на деле делается в пару кликов.

В Amvera вы сможете быстро обновлять код через git (консольную утилиту или интерфейс IDE) буквально за 3 команды.

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

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

Открываем страницу проектов и нажимаем кнопку “Создать”. В открывшемся окне прописываем название проекта на латинице или кириллице, выбираем понравившийся тариф, тип сервиса оставляем “Приложение”.

Нажимаем далее. Теперь доступно окно загрузки данных. Можно загрузить сейчас через интерфейс, а можно через Git. Я рекомендую использовать Git, если в будущем будут обновления проекта.

Выбор не важен - вы в любом случае сможете пользоваться и интерфейсом и Git.

Нажимаем далее и нас встречает окно с конфигурацией. Это и есть инструкции для запуска проекта. Все просто - Выбираем окружение Python, инструмент pip, указываем версию python, название запускаемого скрипта и все. Больше нам ничего настраивать не нужно.

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

Файл зависимостей создается просто - нужно указать название библиотеки и ее версию в формате

библиотека1==версия1
библиотека2==версия

В нашем случае requirements.txt

aiogram==3.10.0
requests==2.32.3

Финальная настройка и доставка кода в репозиторий (GIT)

Перед загрузкой файлов нужно настроить переменные окружения. Нужно зайти на страницу проекта и перейти во вкладку “Переменные”, где создать секреты с указанным в коде названием и нужным значением.

Мы готовы к загрузке кода! Если хотите - можно быстро загрузить через интерфейс сайта во вкладке “Репозиторий”, но я воспользуюсь git.

Я покажу последовательность команд для первого коммита и отправки (пуша) файлов:

  1. git init - инициализирует git локально

  2. git remote add amvera https://git.amvera.ru/Ваш_Ник/Имя_проекта - команда для подключения к репозиторию Amvera. Команду можно найти во вкладке “Репозиторий”

  3. git add . - добавляет все файлы в локальном репозитории

  4. git commit -m "Комментарий" - первый коммит

  5. git push amvera master - пуш в репозиторий.

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

При пуше автоматически начинается сборка. Если вы загрузили через интерфейс - перейдите во вкладку “Конфигурация” и нажмите кнопку “Собрать”. Важно именно собирать проект при обновлении кода/конфигурации.

Итог

Когда приложение собралось - вам осталось дождаться запуска бота и проверить его работоспособность.

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

Комментарии (11)


  1. S0mbre
    11.08.2024 20:12
    +5

    Если уж асинхронный бот, то всё приложение надо асинхронным делать. Соответственно, вместо requests надо использовать httpx, aiohttp или им подобные.


    1. rSedoy
      11.08.2024 20:12
      +1

      ага, а если учесть что у aiogram под капотом aiohttp, то можно сказать, что асинхронный клиент уже досутпен из коробки.


  1. VirRus77
    11.08.2024 20:12

    А зачем ссылки на сайт параметризированны?

    https_://amvera.ru/?utm_source=habr&utm_medium=article&utm_campaign=bot-parser


    1. VadimMichaylov
      11.08.2024 20:12

      Это обычная UTM-метка


    1. Arbane
      11.08.2024 20:12

      Это значит, что статья - реклама. И маркетинг смотрит профит. Автор выбирает свой сервис как будто случайно, без вступления, что вот это Мы, просто пошла история, что лучше бы бота поднять вооот тут (да конечно!)


      1. MarkovM Автор
        11.08.2024 20:12
        +3

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


  1. mykytashch
    11.08.2024 20:12

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


    1. GoDevSeoTaxi
      11.08.2024 20:12
      +1

      А вы когда пользователем заходите на страницу - вы тоже получаете разрешение на получения данных?

      В том - что никто не дает гарантию, что завтра API поменяется - думаю и так понятно.
      В том, что ВБ не сделала проверку на таких ботов - это конечно не доработка ВБ.


  1. Twilizer
    11.08.2024 20:12

    А как вы решили проблему того, что товар уже встречался на странице(к примеру чтобы одни и те же наушники не присылались по N-раз подряд)? Писал собственного бота ещё полгода назад для подобной темы, однако нормальной идеи кроме создания базы данных и учёта последних 10 товаров придумать не смог


    1. unstopppable
      11.08.2024 20:12

      Ну можно просто накачивать set до тех пор, пока в нем не наберётся 10, и затем отправить пачкой в телегу


      1. Twilizer
        11.08.2024 20:12

        можно, но если обновления товаров не было, вы просто будете отправлять всё те же 10 товаров(не знаю как часто обновляются товары WB, однако сайт, который я парсил таким методом обновлял товары только когда продавец выложит что-то новое) и поскольку запрос я делал раз в 5 секунд то данным методом вы могли за час отправить 720 одинаковых товаров в бота, что как-то бесполезно, вот я и хочу узнать как подобную проблему решил автор статьи