Если погуглить, то решений пагинации не так то и много.
Есть библиотека 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
Просто и со вкусом и подробно)
Для новичков самое то, одобряю