Если погуглить, то решений пагинации не так то и много.

Есть библиотека telegram_bot_pagination, но там пагинация выглядит следующим образом:

фото 1
пагинация в библиотеке "telegram_bot_pagination"
фото 1 пагинация в библиотеке "telegram_bot_pagination"

Внизу кнопки, каждая из которых отвечает за определенную страницу

а хотелось бы что-то такое:

фото 2
Пример пагинации из этой статьи
фото 2 Пример пагинации из этой статьи

Внизу кнопки перелистывания страниц

фото 3
фото 3

Есть похожее решение, только там скрываются кнопки Вправо и Влево на последней и Первой страницах соответственно.

Но смысл? Пусть на первой странице кнопка Влево перелистывает на последнюю страницу, а кнопка Вправо на последней странице - на первую. Да и зачем юзеру кнопка Скрыть?


Для начала создадим бота

фото 4
фото 4



Находим БотаФазера, пишем ему /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)

Проверяем...

фото 5
фото 5

Работает! Продолжаем.

Теперь добавим под сообщение кнопки

Воспользуемся нашими 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)

Запускаем, проверяем..

фото 6
фото 6

Работает! Теперь надо отследить нажатие на кнопку.

Для этого надо создать обработчик

@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, "Вы нажали на кнопку!")

Опять перезапускаем, проверяем...

фото 7
фото 7

Рабооотает!

Теперь создадим кнопки управления

С начала наметим наши кнопки:
Пусть будет так:

фото 8
фото 8


Сверху кнопки перелистывания, а внизу кнопки действия с сообщением, если требуется (например, купить товар, если это магазин)

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

фото 9
фото 9



В программе добавляем БД

фото 10
фото 10



ыбираем/создаем файл db.db в папке с ботом

фото 11
фото 11



Ниже пишем для удобства название которое будет отображаться в программе

жмем ОК

фото 12
фото 12



Слева появится название то которое вы указали во втором поле выше.
Жмете на > и внизу выбираете Tables чтобы просмотреть таблицы


фото 13
фото 13



и видим ... ничего не видим. БД то пустая!

фото 14
фото 14



Жмем на Creat Table

фото 15
фото 15

После чего появится такая вкладка.
В поле 1 задаем имя таблицы - users

Далее создаем колонку (2)

фото 16
фото 16


В открывшемся окне в поле 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 таблицы

фото 17
Таблица users
фото 17 Таблица users
фото 18
Таблица store. Структура
фото 18 Таблица store. Структура

Итак, у нас есть БД. Теперь для теста создадим пару записей в store.

фото 19
фото 19

Жмем на вкладку Data (1), Далее на кнопку Insert rows (2), ниже появится строчка. Кликаем по синим ячейкам и заполняем их.

page — 1, 2, 3...
Title — тестирую название 1.
description — тестирую описание 2 
photo_path — путь или URL к картинке.
Не забываем сохранить! Кнопка Commit (3)

У меня получилось так:

фото 20
Таблица store. Данные
фото 20 Таблица store. Данные

Итак, БД есть, Данные есть, Теперь подключимся к БД в пайтоне!

Будем использовать библиотеку 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)

Запускаем, проверяем ...

фото 21
фото 21

все работет, с БД все ок. Теперь присобачим кнопки
к bot.send_photo() на 27 строке добавим аргумент reply_markup с значением buttons:

bot.send_photo(message.chat.id, photo=photo_path, caption=msg, reply_markup=buttons)

проверяем..

фото 22
фото 22

Кнопки есть!

Сейчас раскажу саму суть работы кнопок.
При открытии какой-нибуть страницы в БД идет номер этой страницы (колонка 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)


  1. red-cat-fat
    00.00.0000 00:00
    +2

    Просто и со вкусом и подробно)
    Для новичков самое то, одобряю


  1. red-cat-fat
    00.00.0000 00:00
    +2

    Просто и со вкусом и подробно)
    Для новичков самое то, одобряю


  1. alkosenk0
    00.00.0000 00:00
    +2

    Все супер! Но что если не удалять сообщение, а его изменять? API телеграм позволяет исправлять сообщение по message_id. Кажется, что так было бы плавнее, без лишних мерцаний на экране. Да и если вдруг пропадет интернет и пройдет только первый запрос с удалением, то второго можно и не дождаться :)


    1. RimMirK Автор
      00.00.0000 00:00

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

      Лично у меня подобного не случалось, да и с начала я сообщение отправляю, а потом уже удаляю старое


  1. novichikhin
    00.00.0000 00:00
    +1

    Можно использовать фреймворк aiogram и библиотеку aiogram-dialog для реализации пагинации, упрощая себе жизнь.


    1. RimMirK Автор
      00.00.0000 00:00

      На некоторых хостигах почему-то блокируют айограм. + айограм более сложнее, на мой взгялд


      1. rSedoy
        00.00.0000 00:00

        И что это за такие хостинги? А по коду, не делай больше такой except без конкретики, да еще и с pass


        1. RimMirK Автор
          00.00.0000 00:00

          если не ошибаюсь, pythonAnyWhere.

          не делай больше такой except без конкретики, да еще и с pass

          А как лучше делать? Посоветуйте. Типа отлавливать ошибку удаления, а дальше что вместо pass?


          1. rSedoy
            00.00.0000 00:00

            Вроде про это уже кучу раз было рассказано, ловить нужно конкретные исключения (пример, с открытием файла, там ты наверно FileNotFoundError ожидаешь?), а не все подряд, ну а если нужно всё таки ловить все (это не твой случай), то не замалчивать их, логируй их, да даже хотя бы print


            1. RimMirK Автор
              00.00.0000 00:00

              окей, буду знать. Спасибо за совет!


  1. WhiteApfel
    00.00.0000 00:00
    +1

    А в колбэк кнопок мы просто будем засовывать страницу на которую надо перейти

    Говно идея. Лучше записывать номер текущей страницы, а в логике кнопок уже добавлять или убавлять. Будет потом меньше гемора с тем, что страницы могут добавиться или пропасть. И, соответственно, искать не id=index+1, а первый элемент min(id>index)