Добавить в корзинуПозвонить
Найти в Дзене
Код Доступа

🚀 Пишем Telegram-бота для учёта расходов за вечер. Часть 2

В первой части мы с тобой сделали скелет бота: он умеет парсить команду /add и выводит расходы в консоль. Но есть проблема: стоит перезапустить бота — и все твои суши с маршрутками пропадают без следа 🫠 Сегодня это исправим. Прикрутим к боту настоящую базу данных SQLite, добавим статистику и кнопки, а ещё сделаем супер‑полезную команду /undo. 🧠 Что будем колдовать сегодня 🗃️ Шаг 1: Подключаем SQLite — никакой магии, просто работа Открываем наш expense_bot.py. Сначала добавляем импорт встроенного модуля в самом верху: python import sqlite3 Теперь создадим функцию, которая при запуске бота создаст файл базы данных expenses.db и таблицу для наших расходов, если их ещё нет. Добавь этот код перед командой start: python def init_db():
conn = sqlite3.connect('expenses.db')
cur = conn.cursor()
cur.execute('''
CREATE TABLE IF NOT EXISTS expenses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
amount REAL,
category T

В первой части мы с тобой сделали скелет бота: он умеет парсить команду /add и выводит расходы в консоль. Но есть проблема: стоит перезапустить бота — и все твои суши с маршрутками пропадают без следа 🫠 Сегодня это исправим. Прикрутим к боту настоящую базу данных SQLite, добавим статистику и кнопки, а ещё сделаем супер‑полезную команду /undo.

🧠 Что будем колдовать сегодня

  • База данных SQLite (встроена в Python, ничего устанавливать не нужно)
  • Команда /stats — итоги за сегодня и категории трат
  • Команда /undo — удалить последнюю запись (ой, не туда нажал!)
  • Команда /total — общая сумма за всё время
  • Инлайн‑клавиатуры — кнопки под сообщением
  • Автоматический сброс «за вечер» в полночь с помощью JobQueue

🗃️ Шаг 1: Подключаем SQLite — никакой магии, просто работа

Открываем наш expense_bot.py. Сначала добавляем импорт встроенного модуля в самом верху:

python

import sqlite3

Теперь создадим функцию, которая при запуске бота создаст файл базы данных expenses.db и таблицу для наших расходов, если их ещё нет. Добавь этот код перед командой start:

python

def init_db():
conn = sqlite3.connect('expenses.db')
cur = conn.cursor()
cur.execute('''
CREATE TABLE IF NOT EXISTS expenses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
amount REAL,
category TEXT,
date TEXT
)
''')
conn.commit()
conn.close()

Разберём по шагам:

  • sqlite3.connect('expenses.db') — создаёт файл базы данных (или открывает существующий). Храниться он будет в той же папке, где лежит бот.
  • CREATE TABLE IF NOT EXISTS expenses — создаём таблицу расходов с колонками: id (уникальный номер), user_id (чтобы разные люди не видели траты друг друга), amount (сумма), category (категория) и date (дата операции).
  • conn.commit() и conn.close() — сохраняем изменения и закрываем соединение с БД.

После этого в самом низу, перед app.run_polling(), добавляем вызов этой функции:

python

init_db()

Теперь при каждом запуске бот будет проверять, есть ли база, и создавать её при необходимости.

📝 Шаг 2: Переписываем команду /add на сохранение в базу

Сейчас у нас в /add есть строки:

python

print(f"Добавлен расход: {amount} руб. на '{category}'")
await update.message.reply_text(f"✅ Записал: {amount} руб. — {category}\nЗа вечер потрачено: ??? (скоро подсчитаем)")

Заменяем их на полноценное сохранение в БД. Вот так будет выглядеть обновлённая версия add:

python

async def add(update: Update, context: ContextTypes.DEFAULT_TYPE):
if not context.args:
await update.message.reply_text(
"Как писать:\n/add 150 такси\n/add 45 кофе"
)
return

try:
amount = float(context.args[0])
category = " ".join(context.args[1:]) if len(context.args) > 1 else "прочее"
user_id = update.effective_user.id

# Сохраняем в базу данных
conn = sqlite3.connect('expenses.db')
cur = conn.cursor()
cur.execute('''
INSERT INTO expenses (user_id, amount, category, date)
VALUES (?, ?, ?, datetime('now'))
''', (user_id, amount, category))
conn.commit()
conn.close()

# Считаем сумму трат за сегодня
conn = sqlite3.connect('expenses.db')
cur = conn.cursor()
cur.execute('''
SELECT SUM(amount) FROM expenses
WHERE user_id = ? AND date(date) = date('now')
''', (user_id,))
today_total = cur.fetchone()[0] or 0
conn.close()

await update.message.reply_text(
f"✅ Записал: {amount} руб. — {category}\n"
f"💰 За сегодня потрачено: {today_total:.2f} руб."
)
except ValueError:
await update.message.reply_text("Сумма должна быть числом! Например: /add 99 кино")

Что поменялось? Во-первых, добавилась запись в БД через INSERT. Мы используем ? как placeholder — это безопасно и защищает от SQL‑инъекций. Во-вторых, после добавления новый запрос считает сумму всех трат текущего пользователя за сегодня (date(date) = date('now')). В‑третьих, мы подставляем user_id из update.effective_user.id — теперь разные люди не увидят чужие расходы.

📊 Шаг 3: Создаём команду /stats

Пришло время добавить реальную статистику, а не просто консольные «отладочные сообщения». Вставляем новый обработчик:

python

async def stats(update: Update, context: ContextTypes.DEFAULT_TYPE):
user_id = update.effective_user.id
conn = sqlite3.connect('expenses.db')
cur = conn.cursor()

# Расходы за сегодня
cur.execute('''
SELECT SUM(amount) FROM expenses
WHERE user_id = ? AND date(date) = date('now')
''', (user_id,))
today_total = cur.fetchone()[0] or 0

# Расходы по категориям за сегодня
cur.execute('''
SELECT category, SUM(amount) FROM expenses
WHERE user_id = ? AND date(date) = date('now')
GROUP BY category
ORDER BY SUM(amount) DESC
''', (user_id,))
categories = cur.fetchall()

conn.close()

if not categories:
await update.message.reply_text("📭 Сегодня пока нет расходов. Добавь через /add!")
return

reply = f"📊 Статистика за сегодня:\n💰 Всего: {today_total:.2f} руб.\n\nПо категориям:\n"
for cat, total in categories:
reply += f"• {cat}: {total:.2f} руб.\n"

await update.message.reply_text(reply)

И, конечно, регистрируем новую команду в конце файла, где добавляются обработчики:

python

app.add_handler(CommandHandler("stats", stats))

Теперь пользователь может в любой момент вызвать /stats и увидеть, сколько уже успел потратить и на что именно.

🧶 Шаг 4: Команда /undo — отменяем последнюю операцию

Когда вечером понимаешь, что чипсы и пицца были лишними, можно быстро удалить последнюю запись. Создаём новую функцию:

python

async def undo(update: Update, context: ContextTypes.DEFAULT_TYPE):
user_id = update.effective_user.id
conn = sqlite3.connect('expenses.db')
cur = conn.cursor()

# Находим последнюю запись пользователя
cur.execute('''
SELECT id, amount, category FROM expenses
WHERE user_id = ?
ORDER BY id DESC LIMIT 1
''', (user_id,))
last = cur.fetchone()

if not last:
await update.message.reply_text("❌ Нет расходов, которые можно отменить.")
conn.close()
return

expense_id, amount, category = last

# Удаляем запись
cur.execute('DELETE FROM expenses WHERE id = ?', (expense_id,))
conn.commit()
conn.close()

await update.message.reply_text(f"🗑️ Отменил последний расход: {amount:.2f} руб. — {category}")

Регистрируем обработчик:

python

app.add_handler(CommandHandler("undo", undo))

Проверяем: /undo — и бот удалит самую свежую запись в базе.

📈 Шаг 5: Общая статистика /total

Иногда хочется посмотреть не только сегодняшнюю статистику, но и общую картину. Создаём команду /total:

python

async def total(update: Update, context: ContextTypes.DEFAULT_TYPE):
user_id = update.effective_user.id
conn = sqlite3.connect('expenses.db')
cur = conn.cursor()

cur.execute('''
SELECT SUM(amount) FROM expenses WHERE user_id = ?
''', (user_id,))
total_all = cur.fetchone()[0] or 0

cur.execute('''
SELECT COUNT(*) FROM expenses WHERE user_id = ?
''', (user_id,))
count_all = cur.fetchone()[0] or 0

conn.close()

await update.message.reply_text(
f"📈 Всего потрачено за всё время: {total_all:.2f} руб.\n"
f"📝 Количество записей: {count_all}"
)

Добавляем в регистрацию:

python

app.add_handler(CommandHandler("total", total))

🔘 Шаг 6: Инлайн‑клавиатура — красивые кнопки вместо слеша

Набор команд — это удобно, но представь: ты заходишь в бота, а тебе сразу показывается меню с кнопками. Добавим такую возможность.

Сначала создаём функцию, которая показывает меню, а затем — обработчик нажатия на кнопку.

python

from telegram import InlineKeyboardButton, InlineKeyboardMarkup

async def menu(update: Update, context: ContextTypes.DEFAULT_TYPE):
keyboard = [
[InlineKeyboardButton("➕ Добавить расход", callback_data="add")],
[InlineKeyboardButton("📊 Статистика за сегодня", callback_data="stats")],
[InlineKeyboardButton("📈 Полная статистика", callback_data="total")],
[InlineKeyboardButton("⏪ Отменить последний", callback_data="undo")]
]
reply_markup = InlineKeyboardMarkup(keyboard)
await update.message.reply_text("Выбери действие:", reply_markup=reply_markup)

async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
await query.answer()
data = query.data

if data == "stats":
# Переиспользуем нашу функцию stats, но для callback-запроса
await stats(update, context)
elif data == "total":
await total(update, context)
elif data == "undo":
await undo(update, context)
elif data == "add":
await query.edit_message_text("Напиши: /add сумма категория\nНапример: /add 250 продукты")

Регистрируем обработчики:

python

app.add_handler(CommandHandler("menu", menu))
app.add_handler(CallbackQueryHandler(button_handler))

🕛 Шаг 7: Автоматический сброс — JobQueue

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

python

async def reset_reminder(context: ContextTypes.DEFAULT_TYPE):
# Приветственное сообщение в выбранный чат
await context.bot.send_message(
chat_id=context.job.chat_id,
text="🌙 Полночь! Отчёт за вчера готов. Вызови /stats, чтобы посмотреть статистику!"
)

async def set_reset(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
# Планируем задачу на полночь каждый день
context.application.job_queue.run_daily(
reset_reminder,
time=datetime.time(hour=0, minute=0),
chat_id=chat_id
)
await update.message.reply_text("✅ Ежедневное напоминание на полночь установлено!")

Не забудь импортировать datetime в самом верху:

python

import datetime

Добавляем команду, которая запускает планировщик:

python

app.add_handler(CommandHandler("setreset", set_reset))

🧪 Шаг 8: Пробуем в деле

Запускаем бота заново:

bash

python expense_bot.py

Теперь у нас есть:

  • /add 100 кофе — расход сохраняется в базу и выводит итог за сегодня
  • /stats — показывает сегодняшнюю статистику по категориям
  • /total — общая сумма за всё время
  • /undo — отмена последней операции
  • /menu — удобное меню с кнопками
  • /setreset — автоматическое напоминание каждый вечер в полночь

https://./media/telegram-bot.jpg

📁 Финальная структура проекта

text

expense_bot.py # главный файл бота
expenses.db # база данных SQLite (создаётся автоматически)

💡 Что дальше?

База данных работает, статистика считается — бот уже реально полезен. Вот несколько идей для развития:

ИдеяСложностьЧто даётExcel-отчёт⭐⭐Экспорт всех трат в файл раз в месяцГрафики matplotlib⭐⭐⭐Визуализация: круговая диаграмма категорийВыбор даты для отчёта⭐⭐Отдельная статистика за любой день/неделюРедактирование категорий⭐⭐Возможность переименовать «прочее» в «развлечения»

🎯 Итоги второй части

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

Что делать прямо сейчас

  1. Скопируй готовый код в expense_bot.py и запусти
  2. Добавь несколько трат через /add
  3. Попробуй /stats, /undo и удобное меню /menu

Подпишись на канал «Код доступа», чтобы не пропустить новые фишки: графики расходов, Excel-отчёты и голосовой ввод!

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

До встречи в третьей части! А я пошёл проверять, сколько сегодня потратил на кофе... снова 500 рублей 😅☕