Рубрики
Без рубрики

Создание Чат-бота с использованием Telegram и Python (Часть 2): Добавление серверной части базы данных SQLite

В этом уроке мы добавим бэкэнд базы данных SQLite в нашего бота Telegram и позволим ему запоминать информацию для конкретных пользователей на неопределенный срок. Мы создадим простой список дел, который позволит пользователям добавлять новые элементы или удалять существующие.

Автор оригинала: Gareth Dwyer.

В Части 1 этого урока мы создали базовый чат-бот Telegram с нуля, используя Python. Наш бот был не слишком умен и просто повторял все, что ему отправляли, обратно пользователю. Созданный нами бот является хорошей основой для широкого спектра возможных ботов, поскольку мы можем принимать входные данные, обрабатывать их и возвращать результат — основу классических вычислений. Однако главным ограничением нашей предыдущей лодки было то, что она не могла хранить какую-либо информацию. У него не было долговременной памяти.

В этом уроке мы добавим бэкэнд базы данных SQLite к нашему боту и позволим ему запоминать информацию для конкретных пользователей бесконечно. Мы создадим простой список дел, который позволит пользователям добавлять новые элементы или удалять существующие.

В дополнение к базовым знаниям Python, необходимым для части 1, было бы неплохо, если бы вы немного познакомились с реляционными базами данных для этого урока. В идеале вы уже должны быть знакомы с такими понятиями, как таблицы, строки, столбцы, и написать по крайней мере базовую инструкцию SELECT * FROM SQL. Но если вы этого не сделали, вы, вероятно, все равно сможете не отставать.

Обзор

В этом уроке мы рассмотрим следующее:

  • Создание вспомогательного скрипта базы данных для добавления данных в базу данных SQLite и их повторного извлечения.
  • Добавление пользовательской клавиатуры Telegram, чтобы наш пользователь мог выбирать параметры, а не печатать целое сообщение.
  • Добавление концепции владения элементами, чтобы несколько пользователей могли использовать нашего бота.

К концу этого урока вы узнаете, как использовать некоторые из более продвинутых функций API Telegram (в частности, функцию пользовательской клавиатуры) и как добавить базу данных SQLite в свои чат-боты.

Создание помощника по базе данных

Мы будем следовать хорошей практике кодирования и держать наш код, связанный с базой данных, отдельно от основной логики нашего бота. Любой код, который касается базы данных, будет ограничен новым сценарием, dbhelper.py и мы импортируем этот скрипт в основной скрипт бота.

Моделирование проблемы

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

Список дел состоит из нескольких пунктов с текстом описаниями , таких как “Купить продукты” или “Закончить написание учебника по чат-боту”. На данный момент мы будем моделировать каждый элемент, просто сохраняя его описание в таблице в базе данных. Позже мы увидим, что это радикальное упрощение проблемы, но этого будет достаточно, чтобы мы начали.

Наш Чат-бот должен будет иметь возможность добавить новый элемент в базу данных элементов, которые он запоминает для пользователя, а также должен будет удалить элемент, как только пользователь пометит этот элемент как выполненный. Кроме того, он должен быть в состоянии получить все элементы, чтобы отобразить их пользователю. Наконец, он должен иметь возможность создать таблицу базы данных на случай, если нам понадобится создать новую базу данных (например, при перемещении нашей лодки на новую машину). Вся эта функциональность должна содержаться в вашем dbhelper.py скрипт, так как все это напрямую взаимодействует с базой данных.

В dbhelper.py код

Создайте новый файл с именем dbhelper.py в том же каталоге, что и ваш скрипт чат-бота, и добавьте следующий код:

import sqlite3


class DBHelper:
    def __init__(self, dbname="todo.sqlite"):
        self.dbname = dbname
        self.conn = sqlite3.connect(dbname)

    def setup(self):
        stmt = "CREATE TABLE IF NOT EXISTS items (description text)"
        self.conn.execute(stmt)
        self.conn.commit()

    def add_item(self, item_text):
        stmt = "INSERT INTO items (description) VALUES (?)"
        args = (item_text, )
        self.conn.execute(stmt, args)
        self.conn.commit()

    def delete_item(self, item_text):
        stmt = "DELETE FROM items WHERE description = (?)"
        args = (item_text, )
        self.conn.execute(stmt, args)
        self.conn.commit()

    def get_items(self):
        stmt = "SELECT description FROM items"
        return [x[0] for x in self.conn.execute(stmt)]

В этом коде у нас есть пять методов:

  • ____in it__() принимает имя базы данных (по умолчанию храните данные в файле с именем todo.sqlite ) и создает соединение с базой данных.
  • setup() создает новую таблицу с именем items в нашей базе данных. Эта таблица содержит один столбец (называемый описание )
  • add_item() берет текст для элемента и вставляет его в нашу таблицу базы данных.
  • delete_item() принимает текст для элемента и удаляет его из базы данных
  • get_items() возвращает список всех элементов в нашей базе данных. Мы используем понимание списка, чтобы взять первый элемент каждого элемента, так как SQLite всегда будет возвращать данные в формате кортежа, даже если есть только один столбец, поэтому в этом примере каждый элемент, который мы извлекаем из базы данных, будет похож на ("купить продукты") (кортеж), который понимание списка преобразует в "купить продукты" (простая строка).

Этот класс DBHelper , который мы только что создали, может использоваться нашим Чат-ботом для добавления, удаления и отображения элементов. Давайте изменим код Чат-бота, чтобы использовать эту новую функциональность.

Изменение кода бота

Мы начнем с того, на чем остановились в части 1 этого урока, где у нас был сценарий под названием echobot.py который отражал каждое сообщение, которое было отправлено ему. Чтобы сохранить различные части этой серии отдельно, скопируйте echobot.py в новый файл с именем todobot.py , и вместо этого работайте с новым файлом.

Первое изменение, которое нам нужно внести в код, – это импортировать класс DBHelper и инициализировать его экземпляр.

Добавьте следующие две строки в верхней части todobot.py скрипт:

from dbhelper import DBHelper

db = DBHelper()

С помощью приведенного выше кода мы можем получить доступ к методам получения, добавления и удаления элементов через переменную db .

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

def handle_updates(updates):
    for update in updates["result"]:
        try:
            text = update["message"]["text"]
        	chat = update["message"]["chat"]["id"]
            items = db.get_items()
            if text in items:
                db.delete_item(text)
                items = db.get_items()
            else:
                db.add_item(text)
                items = db.get_items()
            message = "\n".join(items)
            send_message(message, chat)
        except KeyError:
            pass

Это не слишком отличается от того, что у нас было раньше. Снова мы просматриваем каждое обновление и захватываем компоненты text и chat , чтобы мы могли просмотреть текст полученного сообщения и ответить пользователю, который его отправил.

Теперь мы также включаем строку для загрузки всех элементов из нашей базы данных и хранения их в переменной items . Затем мы проверяем, является ли сообщение, отправленное нам пользователем, уже одним из элементов, которые мы храним. Если пользователь отправит нам дубликат элемента, мы примем это как сигнал к удалению элемента (т. Е. пометим его как выполненный), но если элемент новый, мы добавим его в базу данных. В любом случае, мы отправим пользователю сообщение, в котором каждый из сохраненных элементов будет отображаться в новой строке.

Просить наших пользователей ввести элементы, которые они хотят удалить, не очень удобно, но как только мы увидим, что это работает, мы добавим некоторые улучшения. Обратите внимание, что мы уже начинаем выходить за рамки создания чат-ботов вместо традиционных мобильных приложений: с помощью чат-ботов все взаимодействия должны быть текстовыми, в то время как с традиционным мобильным приложением мы могли бы добавить кнопку “X” на каждый элемент, который пользователь может просто коснуться, чтобы удалить этот элемент.

Нам также нужно где-то вызвать метод setup() базы данных. Хотя нам нужно сделать это только один раз, и это может быть сделано в отдельном скрипте, мы вызовем его в основной функции нашего бота, чтобы он вызывался каждый раз, когда мы запускаем бота. Потому что, если у вас есть IF NOT EXISTS часть в нашем CREATE TABLE операторе, мы все равно создадим таблицу только один раз.

Добавьте следующую строку кода в начало функции main() :

 db.setup()

Тестирование нашей первой попытки составить список дел

Чтобы убедиться, что все работает должным образом, запустите todobot.py скрипт в вашем терминале и отправьте сообщение боту. Он должен эхом передать вам сообщение. Это похоже на то, что у нас было раньше, но под капотом происходит гораздо больше. Теперь любое отправленное вами сообщение сохраняется в базе данных, и то, что выглядит как эхо, на самом деле является вашим списком дел, который содержит только один элемент.

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

чат-бот telegram

Здесь мы видим, что мы добавляем “Купить яйца” в список, и весь список (состоящий только из “Купить яйца”) возвращается. Затем мы добавляем “Закончить писать”, и оба элемента возвращаются. Когда мы снова отправляем “Купить яйца”, вместо добавления дубликата товара бот удаляет существующий.

Добавление пользовательской клавиатуры Telegram

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

Способ добиться этого в Telegram – использовать пользовательскую клавиатуру. По умолчанию, когда вы общаетесь с пользователем или ботом в Telegram, вам предоставляется раскладка клавиатуры вашего телефона по умолчанию (или ничего, если у вас есть физическая клавиатура на вашем устройстве). В рамках вызова SendMessage API мы можем передать пользовательскую клавиатуру, которая предоставит пользователю клавиатуру, содержащую определенные клавиши с определенными метками, которые мы можем указать. Когда пользователь указывает, что он хочет пометить элементы как выполненные, мы создадим клавиатуру из всех существующих элементов и позволим пользователю нажимать на те, которые он хочет удалить.

Синтаксис для создания клавиатур Telegram может выглядеть немного сложным. Каждая клавиша представлена строкой, а каждая строка | на клавиатуре представлена списком клавиш. Клавиатура состоит из списка строк клавиш, поэтому, если бы мы хотели перестроить грубую qwerty-клавиатуру, мы получили бы структуру, которая выглядит следующим образом: [[“q”, “w”, “e”, “…”], [“a”, “s”, “d”, “…”], [“z”, “x”, “c”]] . К счастью, клавиатура, которую мы хотим построить, очень проста — она должна содержать только одну клавишу в строке, и эта клавиша должна быть одним из существующих элементов в списке.

Добавьте функцию build_keyboard() в todobot.py который возьмет список элементов и построит клавиатуру, чтобы пользователь мог легко удалить элементы.

def build_keyboard(items):
    keyboard = [[item] for item in items]
    reply_markup = {"keyboard":keyboard, "one_time_keyboard": True}
    return json.dumps(reply_markup)

Эта функция создает список элементов, превращая каждый элемент в список, чтобы указать, что он должен быть целой строкой клавиатуры. Затем мы создаем словарь, который содержит клавиатуру в качестве значения клавиши “клавиатура” и указывает, что эта клавиатура должна исчезнуть, как только пользователь сделает один выбор (в отличие от обычной клавиатуры, где вам, возможно, придется нажать много клавиш, чтобы построить слово, для этой клавиатуры пользователь будет выбирать только один элемент за раз). Наконец, мы преобразуем словарь Python в строку JSON, так как именно этого формата ожидает от нас API Telegram.

Нам также нужно научить нашу функцию send_message() включать пользовательскую клавиатуру, когда мы этого хотим. Мы добавим клавиатуру в качестве необязательного параметра в эту функцию, и если она включена, мы передадим клавиатуру вместе с остальной частью вызова API. Измените функцию send_message () , чтобы она выглядела следующим образом:

def send_message(text, chat_id, reply_markup=None):
    text = urllib.parse.quote_plus(text)
    url = URL + "sendMessage?text={}&chat_id={}&parse_mode=Markdown".format(text, chat_id)
    if reply_markup:
        url += "&reply_markup={}".format(reply_markup)
    get_url(url)

Помните, что аргумент reply_markup , который мы передаем в Telegram, – это не только клавиатура, но и объект, который включает клавиатуру вместе с другими значениями, такими как "one_time_keyboard": True . Поскольку мы построили весь объект в нашей build_keyboard() и закодировали его как JSON, мы можем просто передать его в Telegram в нашей send_message() функции всякий раз, когда это необходимо.

Выбор времени отправки клавиатуры

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

Команды Telegram по соглашению начинаются с косой черты, поэтому мы попросим наших пользователей отправить сообщение /готово , когда они захотят проверить элементы. Хотя мы могли бы использовать любую строку и обрабатывать ее как команду, следуя соглашению и начиная с прямой косой черты, это означает, что клиент Telegram пользователя автоматически выполнит команду или позволит пользователю нажать на команду из предыдущего сообщения (что будет полезно, когда мы добавим последние штрихи к нашему боту в конце этого поста).

Теперь мы изменим функцию handle_updates () , чтобы отправлять пользовательскую клавиатуру пользователю всякий раз, когда он указывает, что хочет пометить элементы как выполненные. Мы также отправим клавиатуру после того, как пользователь только что отметил элемент, поэтому несколько элементов можно легко удалить подряд. Обновленная функция должна выглядеть следующим образом.

def handle_updates(updates):
    for update in updates["result"]:
        text = update["message"]["text"]
        chat = update["message"]["chat"]["id"]
        items = db.get_items()
        if text == "/done":
            keyboard = build_keyboard(items)
            send_message("Select an item to delete", chat, keyboard)
        elif text in items:
            db.delete_item(text)
            items = db.get_items()
            keyboard = build_keyboard(items)
            send_message("Select an item to delete", chat, keyboard)
        else:
            db.add_item(text)
            items = db.get_items()
            message = "\n".join(items)
            send_message(message, chat)

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

Во-Вторых, Чтобы Сделать Разговор

Добавление права собственности на элементы

Наш список дел все еще сырой, но он почти пригоден для использования. Последняя важная особенность, которую мы должны добавить, – это идея “владельцев” различных списков дел. В настоящее время наш бот поддерживает только один список дел в базе данных, поэтому, если несколько пользователей будут использовать нашего бота, все они будут делиться одним гигантским списком и мешать друг другу. Это было бы интересно (если вы хотите немного повеселиться, попробуйте лодку в ее нынешнем виде с друзьями), но не очень практично.

Мы хотим изменить бота таким образом, чтобы вместо того, чтобы каждый раз возвращать каждый элемент в базе данных, он возвращал только элементы, связанные с определенным chat_id . Для этого нам нужно сохранить chat_id владельца вместе с каждым элементом в базе данных. Поэтому нам нужно добавить еще один столбец в нашу базу данных, а также логику базы данных и логику приложения, чтобы учесть эти изменения.

Обновление dbhelper.py файл

Первые изменения, которые мы внесем, находятся в dbhelper.py файл. Нам нужно обновить:

  • Метод setup() для добавления столбца владелец при настройке новой базы данных
  • Метод add_item() принимает chat_id (который мы используем для идентификации владельцев) в качестве дополнительного аргумента и добавляет его в базу данных вместе с элементом
  • Метод delete_item () , чтобы он удалял только элементы, соответствующие и , принадлежащие указанному владельцу
  • Метод get_items() возвращает только элементы, принадлежащие указанному владельцу

Четыре обновленных метода для нашего класса DBHelper можно увидеть ниже.

def setup(self):
    print("creating table")
    stmt = "CREATE TABLE IF NOT EXISTS items (description text, owner text)"
    self.conn.execute(stmt)
    self.conn.commit()

def add_item(self, item_text, owner):
    stmt = "INSERT INTO items (description, owner) VALUES (?, ?)"
    args = (item_text, owner)
    self.conn.execute(stmt, args)
    self.conn.commit()

def delete_item(self, item_text, owner):
    stmt = "DELETE FROM items WHERE description = (?) AND owner = (?)"
    args = (item_text, owner )
    self.conn.execute(stmt, args)
    self.conn.commit()

def get_items(self, owner):
    stmt = "SELECT description FROM items WHERE owner = (?)"
    args = (owner, )
    return [x[0] for x in self.conn.execute(stmt, args)]

Обновление todobot.файл py

Изменения здесь проще из-за абстракции, предоставляемой DBHelper . Изменения, которые мы должны внести, находятся в функции handle_updates () , и все, что нам нужно сделать, это передать chat_id вместе с DBHelper всякий раз, когда мы вызываем один из методов, которые мы только что обновили. Новый код для handle_updates() выглядит следующим образом:

def handle_updates(updates):
    for update in updates["result"]:
        text = update["message"]["text"]
        chat = update["message"]["chat"]["id"]
        items = db.get_items(chat)  ##
        if text == "/done":
            keyboard = build_keyboard(items)
            send_message("Select an item to delete", chat, keyboard)
        elif text in items:
            db.delete_item(text, chat)  ##
            items = db.get_items(chat)  ##
            keyboard = build_keyboard(items)
            send_message("Select an item to delete", chat, keyboard)
        else:
            db.add_item(text, chat)  ##
            items = db.get_items(chat)  ##
            message = "\n".join(items)
            send_message(message, chat)

Строки, в которых мы теперь должны передать это вместе с DBHelper , были обозначены ## . Теперь бот должен работать с несколькими пользователями, назначая каждому пользователю новый список. Удалите файл todo.sqlite и снова запустите бота, чтобы воссоздать базу данных с новыми изменениями.

Последние штрихи

Теперь у нас есть MVP (минимальный жизнеспособный продукт), чтобы мы могли делать и то, и другое. Чтобы закончить, мы приведем в порядок пару небольших вопросов. Одна из проблем заключается в том, что наше тело интерпретирует каждое отправленное ему сообщение как элемент, но на самом деле пользователи могут отправлять другие сообщения, такие как команды бота. Другая проблема заключается в том, что наша база данных будет становиться медленнее по мере ее роста, потому что у нас нет индексов базы данных.

Проглатывание команд и добавление приветственного текста

Когда пользователь впервые начинает наш разговор с нашим ботом, он должен отправить команду /start , и в настоящее время наш бот добавляет это в качестве первого пункта в свой личный список дел. Наш бот должен игнорировать все сообщения , которые начинаются с / , так как это команды Telegram. Однако мы все еще хотим распознать команду /done , поэтому мы добавим два новых логических блока после перехвата сообщений /done , но до перехвата элементов для обработки команд /|.

Добавьте следующие блоки elif в функцию handle_updates() непосредственно перед текстом elif в строке .

elif text == "/start":
    send_message("Welcome to your personal To Do list. Send any text to me and I'll store it as an item. Send /done to remove items", chat)
elif text.startswith("/"):
    continue

Добавление индексов базы данных

Выбор места размещения индексов баз данных-это классический компромисс между пространством и временем в информатике. Если мы добавим больше индексов, наша база данных часто будет работать быстрее, но займет значительно больше места для хранения. Поскольку мы имеем дело с очень маленькими данными, мы добавим индекс в оба наших столбца. Добавление индекса в столбец owner означает, что метод get_items() будет быстрее, так как он будет быстро получать все элементы от конкретного владельца. Добавление индекса в столбец items означает, что метод delete_item() будет работать быстрее, так как ему не придется искать все элементы пользователя, чтобы найти один из них с соответствующим текстом.

Измените setup() метод Debhelper , чтобы он выглядел следующим образом:

def setup(self):
    tblstmt = "CREATE TABLE IF NOT EXISTS items (description text, owner text)"
    itemidx = "CREATE INDEX IF NOT EXISTS itemIndex ON items (description ASC)" 
    ownidx = "CREATE INDEX IF NOT EXISTS ownIndex ON items (owner ASC)"
    self.conn.execute(tblstmt)
    self.conn.execute(itemidx)
    self.conn.execute(ownidx)
    self.conn.commit()

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

Это подводит нас к концу части 2 этого урока. Окончательный код для лодки и помощника по базе данных можно найти на GitHub по адресу https://github.com/sixhobbits/python-telegram-tutorial . Следующая и заключительная часть этой серии приняла форму Codementor Office Hours , где я продемонстрировал, как развернуть бота на VPS. Вы можете посмотреть запись здесь .