Делаем простого бота в Telegram на Python. Интеграция с Excel +1




"История ничему не учит, а только наказывает за незнание уроков."
(с) В.О. Ключевский

Друг предложил мне поработать над проектом для нашей кафедры. ТЗ пока не сформулировано, но точно известно, что это будет бот в телеге. Я хоть и питонист, но с ботами дел никогда не имел, поэтому пишу эту статью, как заметку, для себя и молодых перспективных ребят, чтоб у них было от чего оттолкнуться. Постараюсь писать максимально понятным и простым языком. Профессионалам тут особо делать не чего, хотя, может и придёт какая-нибудь идея.

Краткое описание бота

Наш бот будет создан на тематику Белого движения в гражданской войне в России (1917-1922) /сегодня разговор не о политике. Это просто пример/

  • По команде /start бот будет приветствие и посылать стикер;

  • После приветствия появится клавиатура под строкой набора сообщений с двумя кнопками "Вывести случайную цитату белогвардейца" и "Литература"

  • Нажатие на первую кнопку выведет рандомную цитату из excel файла (такое извращение нужно для подготовки к будущему проекту)

  • Нажатие на кнопку "Литература" выдаст инлайновую клавиатуру (далее будет прояснение, для тех кто не понял что это) с названиями произведений белогвардейских авторов или о белогвардейцах.После нажатия клавиатура пропадёт (просто этим навыком, думаю, необходимо обладать), начнётся загрузка pdf файла и появится оповещение "Приятного чтения!"

Вот такой простенький бот у нас по выйдет.

Начало. Создание бота и добавление библиотек.

Для начала добавим библиотеку pyTelegramBotAPI обычным пипом в cmd.

pip install pyTelegramBotAPI

А также загрузим простенькую библиотеку для работы с Excel. (ЧИТАЕТ ТОЛЬКО .xls!!!)

pip install xlrd

Время импортировать все необходимые библиотеки.

import telebot #импорт pyTelegramBotAPI 
from telebot import types #также достанем типы
import random #рандом обязательно
import xlrd #библиотка чтения экселевских файлов

Далее нужно создать самого бота в телеграме. Для этого пройдёмся по следующим шагам:

  1. Найти в поиске телеграма @BotFather

  2. Написать ему команду /newbot

  3. Первым сообщение отправить имя бота, а вторым его юзернейм (который пишется с @). Он должен быть уникальный и оканчиваться на Bot или _bot

После этих шагов батя пришлёт нам ссылку на нашёго бота и его API.

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

Если коротко, то API — это контракт, который предоставляет программа. «Ко мне можно обращаться так и так, я обязуюсь делать то и это».
(Более подробная инфа тут API)

Кодим

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

bot = telebot.TeleBot("ВАШ API")

В начале, при написании команды /start, бот у нас выдаст приветствие. Соответственно, вставляем декоратор обработчика сообщений.

@bot.message_handler(commands=['start'])

Если кто не знает что такое декораторы, то есть достаточно подробная статья, рекомендую ознакомиться.

Коротко: Декораторы — это, по сути, просто своеобразные «обёртки», которые дают нам возможность делать что-либо до и после того, что сделает декорируемая функция, не изменяя её.

Теперь наш бот понимает команду /start, но ещё ничего не делает. Создаём функцию приветственного сообщения.

@bot.message_handler(commands=['start'])
def send_welcome(message):
	stic = open('stic/welcome.webp', 'rb') #чтение файла в двоичном формате

	# клавиатура
	markup = types.ReplyKeyboardMarkup(resize_keyboard=True)
	but1 = types.KeyboardButton("Вывести случайную цитату белогвардейца")
	but2 = types.KeyboardButton("Литература")
	markup.add(but1, but2)

	bot.reply_to(message, "Здравствуй, {0.first_name}\nСмотрю, ты за Единую, Великую и Недилимую".format(message.from_user)
  ,parse_mode='html',reply_markup=markup)
	bot.send_sticker(message.chat.id,stic)

Пробежимся по коду

Строкой stic = open('stic/welcome.webp', 'rb') мы записываем в переменную путь к нашему стикеру в формате .webp (скачать его можно из сообщений телеграма правой кнопкой мыши, "сохранить как")

Ниже происходит создание клавиатуры
markup = types.ReplyKeyboardMarkup(resize_keyboard=True) ,а точнее запись в переменную подстрочной клавиатуры. Параметр resize_keyboard=True подгоняет кнопки по высоте до возможного минимума. Также создаём две кнопочки и добавляем в клавиатуру:

but1 = types.KeyboardButton("Вывести случайную цитату белогвардейца")
but2 = types.KeyboardButton("Литература")
markup.add(but1, but2)

Клавиатура готова, но пока не используется. Пока оставим её и переключимся на сообщение. Одной строкой мы указываем боту отправить сообщение с текстом, именем отправителя ({0.first_name} , а вытягивает его он из.format(message.from_user) мы можем дописать .format(message.from_user, bot.get_me()) и вызвать имя бота, добавив в текст {1. first_name}), правилом оформления parse_mode='html' (также можно выбрать 'markdown') и определением клавиатуры, которую создали выше.

bot.reply_to(message, "Здравствуй, {0.first_name}\nСмотрю, ты за Единую, Великую и Недилимую".format(message.from_user)
,parse_mode='html',reply_markup=markup)

Последней строкой отправляем стикер bot.send_sticker(message.chat.id,stic)

Приветственное сообщение и клавиатура
Приветственное сообщение и клавиатура

Время дать возможность боту коммуницировать

@bot.message_handler(func=lambda message: True)
def menu(message):
	if message.chat.type == 'private':
		if message.text == "Вывести случайную цитату белогвардейца":

			#достаём циататы из ворда
			rb = xlrd.open_workbook('citat/citat.xls', formatting_info=True)
			sheet = rb.sheet_by_index(0)
			for rownum in range(sheet.nrows):
				rand = int(random.randint(0,rownum))
				row = sheet.row_values(rand)
			bot.send_message(message.chat.id, row)


		elif message.text == "Литература":

			#инлайновая клавиатура
			inMurkup = types.InlineKeyboardMarkup(row_width=1)
			but1 = types.InlineKeyboardButton("И.Ф. Плотников - Александр Васильевич Колчак. Исследователь, адмирал, Верховный правитель России",callback_data='book1')
			but2 = types.InlineKeyboardButton("А.В. Туркул - Дроздовцы в огне", callback_data='book2')
			but3 = types.InlineKeyboardButton("П.Н. Врангель - Записки", callback_data='book3')
			but4 = types.InlineKeyboardButton("М.Г. Дроздовский - Дневник", callback_data='book4')
			inMurkup.add(but1, but2, but3, but4)

			bot.send_message(message.chat.id, "Книги на любой вкус", reply_markup=inMurkup)
		else:
			bot.send_message(message.chat.id, "Я не знаю что и ответить")

Букв много, но сейчас всё раскидаем. Берём знакомы декоратор и делаем проверку лямбдой сообщение. Если не в курсе что такое лямбда-функция, то сюда. Бот у нас работает через личные сообщения, поэтому пропишем if message.chat.type == 'private':, если требуется, то можете указать условия для “group”, “supergroup” или “channel” , но нам это не нужно.

Далее идёт строка с текстом. ВАЖНО текст должен совпадать с названием кнопки клавиатуры, которую указывали выше.

Создаём экселевский файлик, в первую колонку вписываем цитаты, сохраняем в формате .xls в папку, как делали со стикером.

Со спокойной душой вызываем на файл на чтение
rb = xlrd.open_workbook('citat/citat.xls', formatting_info=True), указываем лист с которого считываем инфу sheet = rb.sheet_by_index(0) индексы как у массива (первый элемент нулевой).

Далее определяем диапазон заполненных строк в листе и записываем их количество в rownum. Используем эту переменную как верхнюю границу до которой может сгенерироваться случайное число rand = int(random.randint(0,rownum)). Получив ячейку, вытаскиваем из неё значение row = sheet.row_values(rand) отправляем его bot.send_message(message.chat.id, row).

Вышло как-то так
Вышло как-то так

Для литературы будем использовать инлайновую клавиатуру.

elif message.text == "Литература":

	#инлайновая клавиатура
	inMurkup = types.InlineKeyboardMarkup(row_width=1)
	but1 = types.InlineKeyboardButton("И.Ф. Плотников - Александр Васильевич Колчак. Исследователь, адмирал, Верховный правитель России", callback_data='book1')
	but2 = types.InlineKeyboardButton("А.В. Туркул - Дроздовцы в огне", callback_data='book2')
	but3 = types.InlineKeyboardButton("П.Н. Врангель - Записки", callback_data='book3')
	but4 = types.InlineKeyboardButton("М.Г. Дроздовский - Дневник", callback_data='book4')
	inMurkup.add(but1, but2, but3, but4)

	bot.send_message(message.chat.id, "Книги на любой вкус", reply_markup=inMurkup)

Определяем клавиатуру inMurkup = types.InlineKeyboardMarkup(row_width=1) параметр row_width=1 говорит о том, что на одной строке будет одна кнопка. Принцип кнопок создания такой же как и у обычной клавиатуры, но появляется параметр callback_data значения которого примет бот после нажатия на кнопку и поймёт что нужно сделать (далле это всё опишем).

Последней строкой отправляем сообщение и цепляем к нему инлайновую клавиатуру.

Работа с callback_data

@bot.callback_query_handler(func=lambda call: True)
def callback_inline(call):
	try:
		if call.message:
			if call.data == 'book1':
				doc = open('boo/Plotnikov_Ivan-Aleksandr_Vasilevich_Kolchak_Issledovatel_admiral_Verhovnyi_pravitel_Rossii.pdf', 'rb')
				bot.send_document(call.message.chat.id, doc)
			elif call.data == 'book2':
				doc = open('boo/Turkul_-_Drozdovtsy_v_ogne.pdf','rb')
				bot.send_document(call.message.chat.id, doc)
			elif call.data == 'book3':
				doc = open('boo/Vrangel_P_Zapiski_a4.pdf', 'rb')
				bot.send_document(call.message.chat.id, doc)
			elif call.data == 'book4':
				doc = open('boo/Drozdovsky_dnevnik_1963__ocr.pdf', 'rb')
				bot.send_document(call.message.chat.id, doc)
			#удаляем инлайновую клаву
			bot.edit_message_text(chat_id=call.message.chat.id, message_id=call.message.message_id, text="Книги на любой вкус",
				reply_markup=None)
			#Создаём уведомление
			bot.answer_callback_query(callback_query_id=call.id, show_alert=False,
				text='Приятного чтения!')
	except Exception as e:
		print(repr(e))

Первым делом вызываем декоратор обработки колбэка и подтверждаем, что он был получен @bot.callback_query_handler(func=lambda call: True). Далее создаём функцию с конструкцией try-except (Что это? Чекай). Код хоть и приличный по объёму, но весь однотипный. Через if мы понимает какая "дата" пришла от кнопки, например
if call.data == 'book1': отвечает за первую кнопку с книгой про Колчака. Соответственно, командой

doc = open('boo/Plotnikov_Ivan-Aleksandr_Vasilevich_Kolchak_Issledovatel_admiral_Verhovnyi_pravitel_Rossii.pdf', 'rb')

мы создаём переменную doc в которую "суём" pdf файл, который сохранили в папку "boo" заранее.

Теперь строкой bot.send_document(call.message.chat.id, doc) отправляем файл в чат.

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

Вызываем функцию bot.edit_message_text По тексту определяется сообщение и
в параметр, который мы использовали для добавления клавиатуры reply_markup вносим значение None.

Уведомление призывается заклинанием

Параметр show_alert отвечает за вид оповещения (False простое временное уведомление, True уведомление с кнопкой "Ок")

1 — False, 2 — True
1 — False, 2 — True

Завершаем эту ступень кода конструкцией проверки исключений (ошибок).

except Exception as e:
	print(repr(e))

Финальный аккорд:

bot.polling(none_stop=True)

Именно этой командой и закончим наш код. Теперь бот постоянно проверяет не написал ли ему кто.

В итоге получился такой код:

import telebot
from telebot import types
import random
import xlrd

bot = telebot.TeleBot("TOKEN")

@bot.message_handler(commands=['start'])
def send_welcome(message):
	stic = open('stic/welcome.webp', 'rb')

	# клавиатура
	markup = types.ReplyKeyboardMarkup(resize_keyboard=True)
	but1 = types.KeyboardButton("Вывести случайную цитату белогвардейца")
	but2 = types.KeyboardButton("Литература")
	markup.add(but1, but2)

	bot.reply_to(message, "Здравствуй, {0.first_name}\nСмотрю, ты за Единую, Великую и Недилимую".format(message.from_user),parse_mode='html',reply_markup=markup)
	bot.send_sticker(message.chat.id,stic)



@bot.message_handler(func=lambda message: True)
def menu(message):
	if message.chat.type == 'private':
		if message.text == "Вывести случайную цитату белогвардейца":

			#достаём циататы из ворда
			rb = xlrd.open_workbook('citat/citat.xls', formatting_info=True)
			sheet = rb.sheet_by_index(0)
			for rownum in range(sheet.nrows):
				rand = int(random.randint(0,rownum))
				row = sheet.row_values(rand)
			bot.send_message(message.chat.id, row)


		elif message.text == "Литература":

			#инлайновая клавиатура
			inMurkup = types.InlineKeyboardMarkup(row_width=1)
			but1 = types.InlineKeyboardButton("И.Ф. Плотников - Александр Васильевич Колчак. Исследователь, адмирал, Верховный правитель России", callback_data='book1')
			but2 = types.InlineKeyboardButton("А.В. Туркул - Дроздовцы в огне", callback_data='book2')
			but3 = types.InlineKeyboardButton("П.Н. Врангель - Записки", callback_data='book3')
			but4 = types.InlineKeyboardButton("М.Г. Дроздовский - Дневник", callback_data='book4')
			inMurkup.add(but1, but2, but3, but4)

			bot.send_message(message.chat.id, "Книги на любой вкус", reply_markup=inMurkup)
		else:
			bot.send_message(message.chat.id, "Я не знаю что и ответить")

#обработка callback
@bot.callback_query_handler(func=lambda call: True)
def callback_inline(call):
	try:
		if call.message:
			if call.data == 'book1':
				doc = open('boo/Plotnikov_Ivan-Aleksandr_Vasilevich_Kolchak_Issledovatel_admiral_Verhovnyi_pravitel_Rossii.pdf', 'rb')
				bot.send_document(call.message.chat.id, doc)
			elif call.data == 'book2':
				doc = open('boo/Turkul_-_Drozdovtsy_v_ogne.pdf','rb')
				bot.send_document(call.message.chat.id, doc)
			elif call.data == 'book3':
				doc = open('boo/Vrangel_P_Zapiski_a4.pdf', 'rb')
				bot.send_document(call.message.chat.id, doc)
			elif call.data == 'book4':
				doc = open('boo/Drozdovsky_dnevnik_1963__ocr.pdf', 'rb')
				bot.send_document(call.message.chat.id, doc)
			#удаляем инлайновую клаву
			bot.edit_message_text(chat_id=call.message.chat.id, message_id=call.message.message_id, text="Книги на любой вкус",
				reply_markup=None)
			#Создаём уведомление
			bot.answer_callback_query(callback_query_id=call.id, show_alert=False,
				text='Приятного чтения!')
	except Exception as e:
		print(repr(e))

bot.polling(none_stop=True)

Пара слов от автора

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

Больше функций, параметров и всего прочего находится в документации. Справочник на русском. Библиотеки pyTelegramBotAPI и xlrd.

Огромное спасибо за прочтение статьи! Надеюсь она оказалась полезной. А если понравилась, то поднимай статью в рейтинге и оставляй комменты)

Приглашаю прочесть мои статьи:

Применение библиотеки FuzzyWuzzy для нечёткого сравнения в Python. Расстояние Левенштейна (редакционное расстояние)

Как открыть ссылку в Python. Работа с WebBrowser и решение проблемы с Internet Explorer

Автор обитает тут: ВК, Инстаграм




Комментарии (12):

  1. ecliptic
    /#23274096 / +2

    api key спрячьте

    • Hartpanzer
      /#23274098

      Пхахахах спасибо, упустил из вида

  2. zazar
    /#23274128

    import telebot

    Зачем масса людей выбирает эту библиотеку? Есть же aiogram или PTB.

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

    • Hartpanzer
      /#23274142

      Так вот обучаемся как раз) Спасибо за библиотеки, учтём и рассмотрим

    • twistfire92
      /#23274418

      Для старта telebot отлично подходит. Лично мне было проще с ней разобраться. Плюс новичков может пугать async/await в aiogram. Документации и там и там не особо много, чтобы понять многие вещи, приходилось шариться по исходникам, смотреть как работает все под капотом. Так вот внутрянка telebot более понятная что-ли. В любом случае это мой личный опыт. Кому-то может сразу aiogram зайдет.

  3. alexs963
    /#23274220

    В чем смысл использовать в этой задаче excel вместо текстового файла?

    • 13werwolf13
      /#23274294

      думается мне это из разряда "сверху сказали так, а я и не спорю"

    • Hartpanzer
      /#23274408

       (такое извращение нужно для подготовки к будущему проекту)

  4. twistfire92
    /#23274414

    А зачем хранить в *.XLC, тянуть какую-то стороннюю библиотеку для работы с этими файлами?
    можно же было все это либо в CSV хранить и редактировать в том же Exel (при этом работая со встроенной библиотекой csv), либо простенькую SQLite базу создать и также с ней работать.
    UPD: коммент долго был на модерации.

    В функции callback_inline много повторяющегося. Попробуйте создать словарь, где ключами будут значения в callback_data ваших кнопок, а значениями — пути к соответствующим файлам.

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

    • Hartpanzer
      /#23274438

      Решения действительно стоящие, благодарю. А файлы в формате .XLC нужны из-за будущего проекта, так как в них много информации сейчас хранится (в универе)

      • twistfire92
        /#23274550

        Тогда можно попробовать второй лист xls файла отвести под хранение ID загруженных файлов. И сначала искать там, и если не найдутся — отправлять непосредственно файл.

        Но небольшой нюанс. Если создать нового бота и попытаться использовать в нем те же ID файлов — ничего не выйдет. ID файлов существуют в области видимости одного бота.

      • mixsture
        /#23274850

        Добавлю немного.
        Выбирать информацию из эксель обычно быстрее всего по ADO.
        Читать каждый раз файл при команде не стоит. Это узкое место, где можно получить блокировки, когда будет много параллельных чатов. Напрашивается кэш в памяти.
        Наличие цикла при выборе цитаты похоже на логическую ошибку. Кажется, там достаточно сгенерить случайный номер строки из диапазона и прочитать цитату по индексу — цикла не надо.