Хорошо клиентам – хорошо и нам
В момент размышления над заголовком статьи ко мне пришло осознание: тема этой публикации настолько актуальна, что мне бы позавидовал любой студент. Вспоминается, как долго и мучительно я выбирал тему для диплома, когда учился на последних курсах университета, а сейчас жизненные обстоятельства сами подкидывают мне идеи.
В связи с уходом ИКЕА с российского рынка 5-го июля 2022 года в магазине стартовала онлайн-распродажа. Желающих купить стильные и недорогие вещи для дома оказалось настолько много, что сайт компании в первый день распродажи перестал работать, из-за чего её перенесли на пару дней. Сотрудники компании нашли выход из сложившийся ситуации - создали электронную очередь (см. рисунок 1).
Это помогло снизить нагрузку на сервера, но стрессовое бремя на пользователей сайта возросло. Потенциальным покупателям приходилось часами/днями ждать своей очереди, обновлять страницу и не отходить от компьютера. Некоторые мои знакомые потеряли 3 дня отпуска на «сизифов труд», но справедливости ради они успели сделать 4 заказа. Чтобы не тратить столько времени на сайте компании, я решил реализовать следующую идею:
«Написать за максимально короткое время программу на Python, которая через Telegram бота будет оповещать о доступности сайта интернет-магазина ИКЕА».
В статье я подробно расскажу, как у меня получилось воплотить данную идею и сэкономить себе время и нервы.
Содержание
Первое сообщение от Telegram-бота
Автоматизированное посещение сайта
Заключение
Обратная связь
Первое сообщение от Telegram-бота
После изучения различной информации в интернете о том, как отправлять сообщения через Telegram-бота, я понял, что мне для этого необходимо получить token (ключ доступа к боту) и chat_id (уникальный идентификатор чата). Token позволит управлять ботом, например, получать сообщения, которые были ему отправлены от пользователей, или, наоборот, отправлять сообщения, получать nickname пользователей и т.д. Подробнее о том, как создать бота и получить его token, можно прочитать в спойлере.
Создание Telegram-бота
Для того, чтобы создать своего бота в Telegram, необходимо в поисковой строке мессенджера ввести следующие слово «BotFather». Перед вами появится отец всех ботов в Telegram (см. рисунок А.1).
Я предполагаю, что создаваемый нами бот наследуется от класса 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).
Чтобы создать своего бота, необходимо нажать/написать «newbot». Следом BotFather попросит вас дать название своему боту. Оно будет высвечивать в общем доступе (см. рисунок А.3). Я своего назвал «my_bot_ikea_is_access». Сразу оговорюсь, что нет смысла добавлять данного бота в Telegram, так как для вас он будет бесполезным.
После того, как вы назовете своего бота, останется последний шаг - дать ему username (аналог логина). У username есть два ограничения:
Должно оканчиваться на «bot»;
Должно быть уникальным.
Если вы справились с username, получите token (см. рисунок А.4), с помощью которого в дальнейшем сможете управлять ботом.
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>
страницы ожидания.
Заголовок страницы ожидания
Для примера, получим заголовок 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), с удовольствием всем отвечу.
Всем спасибо, кто смог осилить статью и дошел до этого места!