Если погуглить, то решений пагинации не так то и много.
Есть библиотека telegram_bot_pagination
, но там пагинация выглядит следующим образом:
Внизу кнопки, каждая из которых отвечает за определенную страницу
а хотелось бы что-то такое:
Внизу кнопки перелистывания страниц
Есть похожее решение, только там скрываются кнопки Вправо
и Влево
на последней и Первой страницах соответственно.
Но смысл? Пусть на первой странице кнопка Влево
перелистывает на последнюю страницу, а кнопка Вправо
на последней странице - на первую. Да и зачем юзеру кнопка Скрыть
?
Для начала создадим бота
Находим БотаФазера, пишем ему /newbot
- команду для создания бота. Отвечаем на его вопросы: Название бота; Username бота. Забираем от него токен.
В коде импортируем telebot
- botAPI, types
из telebot
'а для создания кнопок и sqlite3
для работы с Базой данных
import telebot
from telebot import types
import sqlite3
Далее создаем экземпляр класса TeleBot для дальнейшей работы. Проще говоря, создаем бота:
bot = telebot.TeleBot("TOKEN", parse_mode="MARKDOWN")
parse_mode
- это способ форматирования - MarkDown
, MarkDownV2
или HTML
Добавим обработчик команды /start
Для проверки, сделаем так, чтобы бот приветствовал нас при запуске бота.
Для этого воспользуемся методом reply_to()
Для запуска воспользуемся методом polling() c параметром none_stop
в значении True
чтобы бот не выключался при ошибке.
# команда /start
@bot.message_handler(commands=['start'])
def start(message):
bot.reply_to(message, "приветствую тебя!")
bot.polling(none_stop=True)
Проверяем...
Работает! Продолжаем.
Теперь добавим под сообщение кнопки
Воспользуемся нашими types
'ами: InlineKeyboardMarkup(), types.InlineKeyboardButton() и методом add()
buttons = types.InlineKeyboardMarkup()
button = types.InlineKeyboardButton("Button", callback_data="Button")
buttons.add(button)
Через callback_data
потом будем отлавливать клик
Теперь присоединим кнопки к сообщению с помощью параметра reply_markup
.
bot.reply_to(message, "Приветствую тебя!", reply_markup=buttons)
Запускаем, проверяем..
Работает! Теперь надо отследить нажатие на кнопку.
Для этого надо создать обработчик
@bot.callback_query_handler(func=lambda c: True)
def callback(c):
pass
наш callback_data
приходит в c.data
.Проверим. Если в нем "button" то отправим сообщение через bot.send_message()
@bot.callback_query_handler(func=lambda c: True)
def callback(c):
if c.data == 'Button':
bot.send_message(c.message.chat.id, "Вы нажали на кнопку!")
Опять перезапускаем, проверяем...
Рабооотает!
Теперь создадим кнопки управления
С начала наметим наши кнопки:
Пусть будет так:
Сверху кнопки перелистывания, а внизу кнопки действия с сообщением, если требуется (например, купить товар, если это магазин)
left_button = types.InlineKeyboardButton("←", callback_data="None")
page_button = types.InlineKeyboardButton("1/4", callback_data="None")
right_button = types.InlineKeyboardButton("→", callback_data="None")
buy_button = types.InlineKeyboardButton("КУПИТЬ", callback_data="None")
buttons.add(left_button, page_button, right_button)
buttons.add(buy_button)
Окей, кнопки есть. Теперь займемся содержимым
Получение данных из Базы Данных
для начала создадим БД
(скорее всего у вас уже есть БД и вы можете пропустить этот пункт. Обратите внимание на колонку page)
Создаем базу данных
Для управления базой данных я использую SQLiteStudio
В программе добавляем БД
ыбираем/создаем файл db.db в папке с ботом
Ниже пишем для удобства название которое будет отображаться в программе
жмем ОК
Слева появится название то которое вы указали во втором поле выше.
Жмете на >
и внизу выбираете Tables
чтобы просмотреть таблицы
и видим ... ничего не видим. БД то пустая!
Жмем на Creat Table
После чего появится такая вкладка.
В поле 1 задаем имя таблицы - users
Далее создаем колонку (2)
В открывшемся окне в поле Column name
(1) пишем название столбца - id
Далее задаем тип данных (2) - INTEGER
- целое число (в телеграме все id это цифры)
Задаём Primary Key
(4) - это значит что данный столбец будет содержать только уникальные значения
Задаем not NULL
- (5) - не ничего - это значит что эта колонка обязательно должна быть заполнена.
Жмем ОК
Прекрасно! Мы создали колонку id
! Далее создадим еще одну колонку.
Создаем колонку (фото 15 2).
Название (фото 16 1):
page
Тип данных (фото 16 2):
INTEGER
-
Значение по умолчанию:
1
Устанавливаем галочку возле
Dafault
(фото 16 6)Жмем
Configure
(фото 16 1)В открывшемся окне пишем
1
Жмем
Apply
Жмем
ОК
Отлично! Теперь сохраним. Жмем на кнопку Commit structure changes
(фото 15 3) или жмем сочетание клавиш Ctrl
+ C
. Появится новое окно с кодом. Не пугайтесь, так надо, жмем ОК
Таблица users есть, теперь нужна таблица с информацией которую будем пагинировать
Жмем на Creat Table
(фото 14).Задаем имя (фото 15 1). В моем случае это store
- магазин. Создаем 4 колонки (фото 15 2).
Название (фото 16 1):
page
Тип данных (фото 16 2):
INTEGER
Задаем
Primary Key
(16 4)-
и
not NULL
(фото 16 5) Название (фото 16 1):
title
Тип данных (фото 16 2):
TEXT
Название (фото 16 1):
description
Тип данных (фото 16 2):
TEXT
Название (фото 16 1):
photo_path
Тип данных (фото 16 2):
TEXT
Не забываем сохранить (фото 15 3)
Должно получится 2 таблицы
Итак, у нас есть БД. Теперь для теста создадим пару записей в store
.
Жмем на вкладку Data (1), Далее на кнопку Insert rows (2), ниже появится строчка. Кликаем по синим ячейкам и заполняем их.
page — 1, 2, 3...
Title — тестирую название 1.
description — тестирую описание 2
photo_path — путь или URL к картинке.
Не забываем сохранить! Кнопка Commit
(3)
У меня получилось так:
Итак, БД есть, Данные есть, Теперь подключимся к БД в пайтоне!
Будем использовать библиотеку sqlite3
connect = sqlite3.connect("db.db") # не забудьте поставить свое название БД,
cursor = connect.cursor() # если оно у вас не такое!
с помощью cursor
'а будем обращаться к БД.
Для начала выведем первую страницу по команде /start
page_query = cursor.execute("SELECT `title`, `description`, `photo_path` FROM `store` WHERE `page` = 1;")
title, description, photo_path = page_query.fetchone()
в переменных title
, description
и photo_path хранится
соответственная информация из БД на странице 1 (!).
Теперь составим шаблон сообщения
# Название: *{title}*
# Описание: *{description}*
msg = f"Название: *{title}*\nОписание: *{description}*"
Отправим фото с описанием с помощью bot.send_photo(), и присобачим кнопки
bot.send_photo(message.chat.id, photo=photo_path, caption=msg, reply_markup=buttons)
итак, весь наш код сейчас выглядит так:
import telebot
from telebot import types
import sqlite3
bot = telebot.TeleBot("TOKEN", parse_mode="MARKDOWN")
# команда /start
@bot.message_handler(commands=['start'])
def start(message):
buttons = types.InlineKeyboardMarkup()
left_button = types.InlineKeyboardButton("←", callback_data="None")
page_button = types.InlineKeyboardButton("1/4", callback_data="None")
right_button = types.InlineKeyboardButton("→", callback_data="None")
buy_button = types.InlineKeyboardButton("КУПИТЬ", callback_data="None")
buttons.add(left_button, page_button, right_button)
buttons.add(buy_button)
connect = sqlite3.connect("db.db")
cursor = connect.cursor()
page_query = cursor.execute("SELECT `title`, `description`, `photo_path` FROM `store` WHERE `page` = '1';")
title, description, photo_path = page_query.fetchone()
msg = f"Название: *{title}*\nОписание: *{description}*"
bot.send_photo(message.chat.id, photo=photo_path, caption=msg)
@bot.callback_query_handler(func=lambda c: True)
def callback(c):
if c.data == 'None':
bot.send_message(c.message.chat.id, "Вы нажали на кнопку!")
bot.polling(none_stop=True)
Запускаем, проверяем ...
все работет, с БД все ок. Теперь присобачим кнопки
к bot.send_photo() на 27 строке добавим аргумент reply_markup с значением buttons:
bot.send_photo(message.chat.id, photo=photo_path, caption=msg, reply_markup=buttons)
проверяем..
Кнопки есть!
Сейчас раскажу саму суть работы кнопок.
При открытии какой-нибуть страницы в БД идет номер этой страницы (колонка page
).
А в колбэк кнопок мы просто будем засовывать страницу на которую надо перейти.
То есть Делаем функцию которая показывает страницу (у нас она уже есть - start
). Добавляем к ней атрибут page
- номер страницы которую надо вывести. В колбэк кнопки "→" записываем номер данной страницы + 1. А в колбэк кнопки "←" номер страницы - 1.
При нажатии кнопки обработчик будет забирать номер страницы которую нужно вывести и вызывает функцию показа страницы (start
) с параметром из колбэка.
Так-же в start надо добавить атрибут previous_message
- в нем будет передаваться предидущее сообщение чтобы его удалить.
Итак. Сейчас создадим переменные right и left для колбэка.
НО! Помним, что кнопка Влево
на первой странице кнопка должна перелистывать на последнюю страницу, а кнопка Вправо
на последней странице - на первую.
По этому надо узнать количество страниц. Узнаем их по количеству строк в таблице store:
pages_count_query = cursor.execute(f"SELECT COUNT(*) FROM `store`")
pages_count = int(pages_count_query.fetchone()[0])
Теперь создадим это переменные
left = page-1 if page != 1 else pages_count
right = page+1 if page != pages_count else 1
Теперь добавим значения в кнопки. Колбєк будет типа команды: to {страница}
left_button = types.InlineKeyboardButton("←", callback_data=f'to {left}')
page_button = types.InlineKeyboardButton(f"{str(page)}/{str(pages_count)}", callback_data='_')
right_button = types.InlineKeyboardButton("→", callback_data=f'to {right}')
buy_button = types.InlineKeyboardButton("КУПИТЬ", callback_data='buy')
Теперь изменим нашу функцию, добавим аргументы page
и previous_message
По-умолчаню page
= 1
def start(message, page=1, previous_message=None):
# code
pass
Еще надо понимать, что сообщения могут быть как и с фотографией, так и без. При чём фотография может быть как и на компьютере, так и в интернете (ссылка).
Надо под это подстроится
try:
try: photo = open(photo_path, 'rb')
except: photo = photo_path
msg = f"\[*{title}*]\nОписание: "
msg += f"*{description}*\n" if description != None else '_нет_\n'
bot.send_photo(message.chat.id, photo=photo, caption=msg, reply_markup=buttons)
except:
msg = f"\[*{title}*]\nОписание: "
msg += f"*{description}*\n" if description != None else '_нет_\n'
bot.send_message(message.chat.id, msg, reply_markup=buttons)
и на последок удаление сообщения:
try: bot.delete_message(message.chat.id, previous_message.id)
except: pass
АГГГА! Чуть не забыл! Надо же еще доставать информацию из БД определенной страницы. Исправим.
product_query = cursor.execute(f"SELECT `title`, `description`, `photo_path` FROM `store` WHERE `page` = '{page}';")
title, description, photo_path = product_query.fetchone()
Вот так нормально. Переходим к обработчику.
Итак. В обработчике все просто. Вспоминаем план - "При нажатии кнопки обработчик будет забирать номер страницы которую нужно вывести и вызывает функцию показа страницы (start
) с параметром из колбэка. "
То есть, проверяем, если есть в колбэке "to" то забираем то что после to и вызываем функцию старт с нужными параметрами.
# Обработчик callback
@bot.callback_query_handler(func=lambda c: True)
def callback(c):
if 'to' in c.data:
page = c.data.split(' ')[1]
start(c.message, page=page, previous_message=c.message)
Вот и пагинация готова.
Вот результат:
Вот полный код:
# Импорты
import telebot
import sqlite3
from telebot import types
# Создаем экземпляр бота (создаем бота)
bot = telebot.TeleBot("ТОКЕН", parse_mode="MARKDOWN")
# команда /start
@bot.message_handler(commands=['start'])
def start(message, page=1, previous_message=None):
connect = sqlite3.connect("habr_db.db")
cursor = connect.cursor()
pages_count_query = cursor.execute(f"SELECT COUNT(*) FROM `store`")
pages_count = int(pages_count_query.fetchone()[0])
product_query = cursor.execute(f"SELECT `title`, `description`, `photo_path` FROM `store` WHERE `page` = '{page}';")
title, description, photo_path = product_query.fetchone()
cursor.execute(f"UPDATE `users` SET `page` = {page} WHERE `users`.`id` = {message.chat.id};")
connect.commit()
buttons = types.InlineKeyboardMarkup()
left = page-1 if page != 1 else pages_count
right = page+1 if page != pages_count else 1
left_button = types.InlineKeyboardButton("←", callback_data=f'to {left}')
page_button = types.InlineKeyboardButton(f"{str(page)}/{str(pages_count)}", callback_data='_')
right_button = types.InlineKeyboardButton("→", callback_data=f'to {right}')
buy_button = types.InlineKeyboardButton("КУПИТЬ", callback_data='buy')
buttons.add(left_button, page_button, right_button)
buttons.add(buy_button)
try:
try: photo = open(photo_path, 'rb')
except: photo = photo_path
msg = f"Название: *{title}*\nОписание: "
msg += f"*{description}*\n" if description != None else '_нет_\n'
bot.send_photo(message.chat.id, photo=photo, caption=msg, reply_markup=buttons)
except:
msg = f"Название: *{title}*\nОписание: "
msg += f"*{description}*\n" if description != None else '_нет_\n'
bot.send_message(message.chat.id, msg, reply_markup=buttons)
try: bot.delete_message(message.chat.id, previous_message.id)
except: pass
# Обработчик callback
@bot.callback_query_handler(func=lambda c: True)
def callback(c):
if 'to' in c.data:
page = int(c.data.split(' ')[1])
start(c.message, page=page, previous_message=c.message)
# Запуск бота
bot.polling(none_stop=True)
Так-же хотел бы поблагодарить seeklay1337 за небольшую помощь в написании статьи
Если нашли ошибку - добро пожаловать в комментарии!
Комментарии (10)
alkosenk0
00.00.0000 00:00+2Все супер! Но что если не удалять сообщение, а его изменять? API телеграм позволяет исправлять сообщение по message_id. Кажется, что так было бы плавнее, без лишних мерцаний на экране. Да и если вдруг пропадет интернет и пройдет только первый запрос с удалением, то второго можно и не дождаться :)
RimMirK Автор
00.00.0000 00:00если вдруг пропадет интернет и пройдет только первый запрос с удалением, то второго можно и не дождаться
Лично у меня подобного не случалось, да и с начала я сообщение отправляю, а потом уже удаляю старое
novichikhin
00.00.0000 00:00+1Можно использовать фреймворк aiogram и библиотеку aiogram-dialog для реализации пагинации, упрощая себе жизнь.
RimMirK Автор
00.00.0000 00:00На некоторых хостигах почему-то блокируют айограм. + айограм более сложнее, на мой взгялд
rSedoy
00.00.0000 00:00И что это за такие хостинги? А по коду, не делай больше такой except без конкретики, да еще и с pass
RimMirK Автор
00.00.0000 00:00если не ошибаюсь, pythonAnyWhere.
не делай больше такой except без конкретики, да еще и с pass
А как лучше делать? Посоветуйте. Типа отлавливать ошибку удаления, а дальше что вместо
pass
?rSedoy
00.00.0000 00:00Вроде про это уже кучу раз было рассказано, ловить нужно конкретные исключения (пример, с открытием файла, там ты наверно FileNotFoundError ожидаешь?), а не все подряд, ну а если нужно всё таки ловить все (это не твой случай), то не замалчивать их, логируй их, да даже хотя бы print
WhiteApfel
00.00.0000 00:00+1А в колбэк кнопок мы просто будем засовывать страницу на которую надо перейти
Говно идея. Лучше записывать номер текущей страницы, а в логике кнопок уже добавлять или убавлять. Будет потом меньше гемора с тем, что страницы могут добавиться или пропасть. И, соответственно, искать не id=index+1, а первый элемент min(id>index)
red-cat-fat
Просто и со вкусом и подробно)
Для новичков самое то, одобряю