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

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

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

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

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

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

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

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

Содержание

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

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

  3. Заключение

  4. Обратная связь

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

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

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

Для того, чтобы создать своего бота в Telegram, необходимо в поисковой строке мессенджера ввести следующие слово «BotFather». Перед вами появится отец всех ботов в Telegram (см. рисунок А.1).

Рисунок A.1 -  BotFather.Рисунок A.1 — BotFather.

Я предполагаю, что создаваемый нами бот наследуется от класса BotFather. После перехода в чат перед вами появится приветственное сообщение (в какой-то степени даже диктаторское):

BotFather is the one bot to rule them all. Use it to create new bot accounts and manage your existing bots. …

Чтобы перейти к более детальному общению необходимо нажать кнопку »start» (в общем, как и всегда при первом общении с ботами в Telegram). Затем появится ряд опций (см. рисунок А.2).

Рисунок А.2 - Опции.Рисунок А.2 — Опции.

Чтобы создать своего бота, необходимо нажать/написать »newbot». Следом BotFather попросит вас дать название своему боту. Оно будет высвечивать в общем доступе (см. рисунок А.3). Я своего назвал «my_bot_ikea_is_access». Сразу оговорюсь, что нет смысла добавлять данного бота в Telegram, так как для вас он будет бесполезным.

Рисунок А.3 - Название бота в общем доступеРисунок А.3 — Название бота в общем доступе

После того, как вы назовете своего бота, останется последний шаг — дать ему username (аналог логина). У username есть два ограничения:

Если вы справились с username, получите token (см. рисунок А.4), с помощью которого в дальнейшем сможете управлять ботом.

Рисунок А.4 -  Token.Рисунок А.4 — Token.

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)),

Рисунок 2 - Первое сообщение Telegram-боту.Рисунок 2 — Первое сообщение Telegram-боту.

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

>>> {
  '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.

Рисунок 3 - Первое сообщение от Telegram-бота.Рисунок 3 — Первое сообщение от Telegram-бота.

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

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

В предыдущей главе была протестирована отправка сообщений в 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) на главную страницу сайта.

Рисунок 4 - Пример страницы ожидания.Рисунок 4 — Пример страницы ожидания.

Возникает логичный вопрос — как определить, что мы перешли на главную страницу сайта? Ответ предельно прост — сравним для этого заголовок 1-го уровня на текущей открытой странице с 

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

).

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

В спойлере рассмотрим, как получить заголовок

страницы ожидания.

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

Для примера, получим заголовок 1-го уровня страницы ожидания (см. рисунок 4)

URL = 'https://www.ikea.com/ru/ru/'

driver = open_page(URL)
page_source = get_ikea_html(URL, driver)
start_h1 = get_h1(page_source)[0].text
print(start_h1)

Вывод:

>>> """
Вы находитесь на странице ожидания для перехода на IKEA.ru. 
Не обновляйте страницу, чтобы сохранить свое место в очереди. 
Ожидание зависит от количества пользователей на сайте и может 
занять как несколько минут, так и более часа.\nОбратите внимание, 
что время вашего пребывания на сайте будет ограничено – через 10-15
минут система может вернуть вас обратно в очередь.\nЛичный кабинет 
и список покупок на сайте не доступны – добавляйте товары непосредственно 
в корзину.
"""

Осталось лишь отправить оповещение о доступности главной страницы. За отправку сообщения в 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)   # отправляем сообщение

Если возникнет необходимость отправить сообщение нескольким пользователям, можно переопределить две функции (см. в спойлере ниже) и это станет возможным. Однако в этом практически нет смысла, так как одна http-сессия — одна электронная очередь. Что это значит? Если у вас страница стала доступна, это не значит, что она доступна и у вашего знакомого.

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

Если хотим отправить информацию нескольким пользователем, то переопределяем функции 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
        )

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

Заключение

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

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

Обратная связь

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

  2. Пишите в Telegram или на Почту (если mailto ссылка не сработала: ratmirmigranov@yandex.ru), с удовольствием всем отвечу.

Всем спасибо, кто смог осилить статью и дошел до этого места!

© Habrahabr.ru