Хорошо клиентам – хорошо и нам
В момент размышления над заголовком статьи ко мне пришло осознание: тема этой публикации настолько актуальна, что мне бы позавидовал любой студент. Вспоминается, как долго и мучительно я выбирал тему для диплома, когда учился на последних курсах университета, а сейчас жизненные обстоятельства сами подкидывают мне идеи.
В связи с уходом ИКЕА с российского рынка 5-го июля 2022 года в магазине стартовала онлайн-распродажа. Желающих купить стильные и недорогие вещи для дома оказалось настолько много, что сайт компании в первый день распродажи перестал работать, из-за чего её перенесли на пару дней. Сотрудники компании нашли выход из сложившийся ситуации - создали электронную очередь (см. рисунок 1).
![Рисунок 1 - Страница ожидания. Рисунок 1 - Страница ожидания.](https://habrastorage.org/getpro/habr/upload_files/901/372/be1/901372be17552337f64735a61414fcfa.png)
Это помогло снизить нагрузку на сервера, но стрессовое бремя на пользователей сайта возросло. Потенциальным покупателям приходилось часами/днями ждать своей очереди, обновлять страницу и не отходить от компьютера. Некоторые мои знакомые потеряли 3 дня отпуска на «сизифов труд», но справедливости ради они успели сделать 4 заказа. Чтобы не тратить столько времени на сайте компании, я решил реализовать следующую идею:
«Написать за максимально короткое время программу на Python, которая через Telegram бота будет оповещать о доступности сайта интернет-магазина ИКЕА».
В статье я подробно расскажу, как у меня получилось воплотить данную идею и сэкономить себе время и нервы.
Содержание
Первое сообщение от Telegram-бота
Автоматизированное посещение сайта
Заключение
Обратная связь
Первое сообщение от Telegram-бота
После изучения различной информации в интернете о том, как отправлять сообщения через Telegram-бота, я понял, что мне для этого необходимо получить token (ключ доступа к боту) и chat_id (уникальный идентификатор чата). Token позволит управлять ботом, например, получать сообщения, которые были ему отправлены от пользователей, или, наоборот, отправлять сообщения, получать nickname пользователей и т.д. Подробнее о том, как создать бота и получить его token, можно прочитать в спойлере.
Создание Telegram-бота
Для того, чтобы создать своего бота в Telegram, необходимо в поисковой строке мессенджера ввести следующие слово «BotFather». Перед вами появится отец всех ботов в Telegram (см. рисунок А.1).
![Рисунок A.1 - BotFather. Рисунок A.1 - BotFather.](https://habrastorage.org/getpro/habr/upload_files/395/27a/84f/39527a84fb1d2fbe64769695dafbdb81.png)
Я предполагаю, что создаваемый нами бот наследуется от класса 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 - Опции.](https://habrastorage.org/getpro/habr/upload_files/c79/f48/70a/c79f4870afedd08b5f60e14079f95000.png)
Чтобы создать своего бота, необходимо нажать/написать «newbot». Следом BotFather попросит вас дать название своему боту. Оно будет высвечивать в общем доступе (см. рисунок А.3). Я своего назвал «my_bot_ikea_is_access». Сразу оговорюсь, что нет смысла добавлять данного бота в Telegram, так как для вас он будет бесполезным.
![Рисунок А.3 - Название бота в общем доступе Рисунок А.3 - Название бота в общем доступе](https://habrastorage.org/getpro/habr/upload_files/61f/ad0/05f/61fad005f426922027f3bfda0108d8ea.png)
После того, как вы назовете своего бота, останется последний шаг - дать ему username (аналог логина). У username есть два ограничения:
Должно оканчиваться на «bot»;
Должно быть уникальным.
Если вы справились с username, получите token (см. рисунок А.4), с помощью которого в дальнейшем сможете управлять ботом.
![Рисунок А.4 - Token. Рисунок А.4 - Token.](https://habrastorage.org/getpro/habr/upload_files/a93/975/85a/a9397585a9dae2361bff58484cd33faf.png)
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-боту.](https://habrastorage.org/getpro/habr/upload_files/c6b/d61/3e7/c6bd613e752bc529071967a0666bd82b.png)
результат будет содержать системную информацию о сообщении, дату отправки сообщения, отправителя, текст сообщения и т.д.
>>> {
'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-бота.](https://habrastorage.org/getpro/habr/upload_files/21a/d04/377/21ad043778f30bdbbb8ca0952ed20a06.png)
С второстепенной задачей справились, теперь можно переходить к решению основной.
Автоматизированное посещение сайта
В предыдущей главе была протестирована отправка сообщений в 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 - Пример страницы ожидания.](https://habrastorage.org/getpro/habr/upload_files/2ad/1be/158/2ad1be1588405ba2cd20477da9281c59.png)
Возникает логичный вопрос - как определить, что мы перешли на главную страницу сайта? Ответ предельно прост - сравним для этого заголовок 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>
страницы ожидания.
Заголовок страницы ожидания
Для примера, получим заголовок 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. Вы можете его усовершенствовать при необходимости. Например, написать часть кода, которая будет добавлять товар в корзину или открывать сразу несколько вкладок.
Программный код, используемый в статье, вы можно найти здесь.
Обратная связь
Обязательно оставляйте комментарии.
Пишите в Telegram или на Почту (если mailto ссылка не сработала: ratmirmigranov@yandex.ru), с удовольствием всем отвечу.
Всем спасибо, кто смог осилить статью и дошел до этого места!