История о том, как Python помог купить мебель в ИКЕА

История о том, как Python помог купить мебель в ИКЕА

Автор: person-13

Хорошо клиентам – хорошо и нам


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



В связи с уходом ИКЕА с российского рынка 5-го июля 2022 года в магазине стартовала онлайн-распродажа. Желающих купить стильные и недорогие вещи для дома оказалось настолько много, что сайт компании в первый день распродажи перестал работать, из-за чего её перенесли на пару дней. Сотрудники компании нашли выход из сложившийся ситуации - создали электронную очередь (см. рисунок 1).


Рисунок 1 - Страница ожидания.

Это помогло снизить нагрузку на сервера, но стрессовое бремя на пользователей сайта возросло. Потенциальным покупателям приходилось часами/днями ждать своей очереди, обновлять страницу и не отходить от компьютера. Некоторые мои знакомые потеряли 3 дня отпуска на «сизифов труд», но справедливости ради они успели сделать 4 заказа. Чтобы не тратить столько времени на сайте компании, я решил реализовать следующую идею:

«Написать за максимально короткое время программу на Python, которая через Telegram бота будет оповещать о доступности сайта интернет-магазина ИКЕА».

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

Содержание

  1. Первое сообщение от Telegram-бота
  2. Автоматизированное посещение сайта
  3. Заключение
  4. Обратная связь

Первое сообщение от Telegram-бота

После изучения различной информации в интернете о том, как отправлять сообщения через Telegram-бота, я понял, что мне для этого необходимо получить token (ключ доступа к боту) и chat_id (уникальный идентификатор чата). Token позволит управлять ботом, например, получать сообщения, которые были ему отправлены от пользователей, или, наоборот, отправлять сообщения, получать nickname пользователей и т.д. Подробнее о том, как создать бота и получить его token, можно прочитать в спойлере.


Создание Telegram-бота








Token получен, но этого не достаточно для отправки сообщения пользователю, потому что, во-первых, боты в Telegram не имеют права писать юзерам, которые до этого с ним не взаимодействовали (защита от спама), во-вторых, бот должен "понимать" кому именно следует отправить сообщение. Решить вторую проблему нам поможет chat_id, но, чтобы chat_id сформировался, пользователь должен самостоятельно отправить сообщение Telegram-боту. Получить chat_id поможет метод getUpdates (подробнее о методе можно почитать здесь). Сделаем GET-запрос и посмотрим на вывод.

import json
import requests

TOKEN = 'ВСТАВЬТЕ СЮДА ТОКЕН ВАШЕГО БОТА'

r = requests.get(f'https://api.telegram.org/bot{TOKEN}/getUpdates')
answer = json.loads(r.text)
print(answer)

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

>>> {
 'ok': True,
 'result': []
}

Если с ботом было взаимодействие (например, отправлено сообщение (см. рисунок 2)),

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

>>> {
  'ok': True,
  'result': [{
       'update_id': 83593437228,
     'message': {
           'message_id': 44335,
         'from': {
               'id': 4973423306934,
              'is_bot': False,
              'first_name': 'X',
              'last_name': 'X',
              'username': 'XxX',
              'language_code': 'ru'
              },
         'chat': {
               'id': 4973423306934,
             'first_name': 'X',
             'last_name': 'X',
             'username': 'XxX',
             'type': 'private'
               },
         'date': 1658143480,
         'text': 'Hello bot !!!'
           }
       }
  ]
}

Теперь давайте автоматизируем получение id чата.

r = requests.get(f'https://api.telegram.org/bot{TOKEN}/getUpdates')
answer = json.loads(r.text)
chat_id = answer['result']['message']['chat']['id']
print(chat_id)

Вывод:

>>> 4973423306934

Я понимаю, что решение по получению chat_id далеко не оптимально, поскольку может возникнуть ряд сложностей. Например, любой пользователь, обнаруживший бота, может отправить ему сообщение. В ответе, полученном с помощью метода getUpdates, будет несколько различных chat_id. Получается, что сообщение от бота, которое полагается одному пользователю, вероятнее, получит иной. В моём случае было важно написать программу за максимально короткое время, а не создавать универсальное решение

На данном этапе получен token и chat_id. Можно переходить к отправке сообщения в Telegram с помощью Python. Отправить сообщение поможет метод sendMessage (подробнее о методе можно почитать здесь). Сделаем POST-запрос и посмотрим на вывод.

message = 'Hello'   # сообщение
chat_id = answer['result']['message']['chat']['id']   # id чата

params = {
    'chat_id' : chat_id,
    'text' : message
}

requests.post(
    f'https://api.telegram.org/bot{TOKEN}/sendMessage',   # отправляем сообщение
    data = params     # передаем параметры в метод post
)

Вывод можно посмотреть на рисунке 3.

С второстепенной задачей справились, теперь можно переходить к решению основной.

Автоматизированное посещение сайта

В предыдущей главе была протестирована отправка сообщений в Telegram. Теперь применим эти знания для решения практической задачи. Напомню цель – необходимо купить товар в ИКЕА во время распродажи и при этом не стоять самостоятельно в электронной очереди. Сформулируем задачу: отправлять сообщение о доступности сайта ИКЕА в Telegram с помощью Python.

В процессе было выдвинуто новое требование:

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

После постановки задачи можно переходить к её решению. MVP базируется на трёх основных библиотеках:


  • requests – позволяет отправлять http-запросы;
  • bs4 (BeautifulSoup) – позволяет парсить HTML и XML документы;
  • selenium – автоматизирует действия веб-браузера. Данная библиотека в основном используется для тестирования, однако в нашем случае она будет применяться для запуска браузера и интернет страницы.
import json   # позволяет кодировать и декодировать данные формата JSON
import requests
import time   # модуль для работы со временем

from bs4 import BeautifulSoup
from IPython.display import clear_output   # очищает ввывод в jupiter notebook
from selenium import webdriver
from webdriver_manager.chrome import ChromeDriverManager   # драйвер для 
                                                           # управления браузером
                                                           # Google Chrome

Для начала необходимо автоматизировать запуск браузера и интернет страницы. За это отвечает функция open_page, которая принимает в качестве параметра уникальный адрес страницы. Внутри себя эта функция вызывает две другие:

  • add_chrome_options– отвечает за добавление новых опций. Например, можно добавить опцию options.add_argument('--headless'), тогда Chrome запустится в автономном режиме. В данном случае была добавлена одна новая опция связанная с user agent (идентифицирует браузер и операционную систему), поскольку только с ним удалось получить доступ к сайту ИКЕА (возможно, на момент прочтения статьи что-то изменится).
  • install_chrome_driver– отвечает за установку драйвера для управления браузером Google Chrome. Следующий код ChromeDriverManager().install() устанавливает наиболее актуальную версию драйвера. Конечно, устанавливать драйвер лучше вне функции open_page, так как при каждом открытии страницы он, возможно, будет устанавливаться заново. Для MVP это не критично, и высока вероятность, что при повторных открытиях страницы драйвер подтянется из кэша.
def add_chrome_options():
    options = webdriver.ChromeOptions()
    options.add_argument(
        'user-agent=Mozilla/5.0' +
        '(Windows NT 10.0; Win64; x64)' + 
        'AppleWebKit/537.36 (KHTML, like Gecko)' + 
        'Chrome/79.0.3945.79 Safari/537.36'
    )
    
    return options
  
def install_chrome_driver():
    
    return ChromeDriverManager().install()
  
def open_page(url):
    options = add_chrome_options()
    install_driver = install_chrome_driver()
    
    driver = webdriver.Chrome(
        install_driver, 
        chrome_options=options
    )
    driver.get(url)
    
    return driver

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

Возникает логичный вопрос - как определить, что мы перешли на главную страницу сайта? Ответ предельно прост - сравним для этого заголовок 1-го уровня на текущей открытой странице с <h1> заголовком главной страницы интернет-магазина (я узнал его заранее – 'ИКЕА - официальный интернет-магазин мебели '). Если они совпадут, будем считать, что главная страница доступна. Заголовок 1-го уровня получим с помощью парсинга страницы. Функция get_ikea_html возвращает html-код страницы, а функция get_h1 возвращает заголовок 1-го уровня (<h1>).

def get_ikea_html(url, driver):
    page_source = driver.page_source
    
    return page_source

def get_h1(page_source):
    soup = BeautifulSoup(page_source, 'lxml')
    html_h1 = soup.find_all('h1')

    return html_h1

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

Заголовок страницы ожидания



Осталось лишь отправить оповещение о доступности главной страницы. За отправку сообщения в Telegram отвечает процедура post_message.В качестве параметров она принимает token, chat_id (id чата - адрес получателя), message (сообщение, которое будет отправлено получателю).

def get_chat_id(token):
    r = requests.get(f'https://api.telegram.org/bot{token}/getUpdates')
    answer = json.loads(r.text)
    chat_id = answer['result'][-1]['message']['chat']['id']
    
    return chat_id

def post_message(token, id_chat, message):
    params = {
        'chat_id' : chat_id,
        'text' : message
    }
    requests.post(
        f'https://api.telegram.org/bot{token}/sendMessage',
        data = params
    )

Все основные функции реализованы, теперь можем переходить к решению поставленной задачи. Код ниже запустит Chrome и откроет страницу ожидания ИКЕА, после этого он будет проверять у страницы каждую секунду заголовок 1-го уровня. Если заголовок 1-го уровня страницы совпадет с заголовком главной страницы интернет-магазина, бот отправит сообщение в телеграмм о доступности сайта.

URL = 'https://www.ikea.com/ru/ru/'   # целевая страница
start_page_ikea_h1 = (
  'ИКЕА - официальный интернет-магазин мебели '
)   # заголовок 1-го уровня на главной странице ИКЕА

start_h1 = (-1)   # начальное значение переменной
driver = open_page(URL)   # запускаем браузер и открываем необходимую страницу (URL)

cnt = 0   # счетчик

while start_h1 != start_page_ikea_h1:   # цикл остановится когда полученный заголовок
                                        # будет равен заголовку на главной странице
    page_source = get_ikea_html(URL, driver)   # получаем html-код страницы
    start_h1 = get_h1(page_source)[0].text   # получаем заголовок 1-го уровня
    print(start_h1)
    time.sleep(1)
    driver.refresh()   # обновляем страницу (не обязательно)
    
    if cnt%100 == 0:   # после 100-го раза очищает ввывод в jupiter notebook
        clear_output(wait=False)
    cnt += 1
    
chat_id = get_chat_id(TOKEN)   # получаем id чата
message = 'ГЛАВНАЯ СТРАНИЦА ОТКРЫТА'   # сообщение, которое будет отправлено
post_message(TOKEN, chat_id, message)   # отправляем сообщение

Отправка сообщений нескольким пользователям

Если хотим отправить информацию нескольким пользователем, то переопределяем функции get_chat_id и post_message следующем образом:


def get_chat_id(token):
    r = requests.get(f'https://api.telegram.org/bot{token}/getUpdates')
    answer = json.loads(r.text)
    
    chats_id = set()   # определим множество
    
    for i in range(len(answer['result'])):
        chat_id = answer['result'][i]['message']['chat']['id']
        chats_id.add(chat_id)   # добавляем id чата в множество
    
    return chats_id


def post_message(token, chats_id, message):
    for chat_id in chats_id:   # перебираем все id чата
        params = {
            'chat_id' : chat_id,
            'text' : message
        }
        requests.post(
            f'https://api.telegram.org/bot{token}/sendMessage',
            data = params
        )

Прототип выше изложенного решения можно увидеть ниже на видео.

https://www.youtube.com/watch?v=JKhHk-4zRPQ&feature=emb_logo


Заключение

В статье была описана небольшая программа на Python, которая в ходе выполнение проверяет доступность сайта интернет-магазина и в случае положительного исхода оповещает пользователя с помощью Telegram-бота. С помощью данной программы у меня получилось сэкономить себе время и заказать товары. Этот код лишь MVP. Вы можете его усовершенствовать при необходимости. Например, написать часть кода, которая будет добавлять товар в корзину или открывать сразу несколько вкладок.

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


Report Page