Обучение Python+aiogram+MySQL
September 25, 2023

Обучение #2

Язык программирования - Далее ЯП

Операционная система - Далее ОС

Редактор кода - Далее IDE

CMD - Далее консоль

Первый урок - *Кликабельно*

В этом уроке мы научимся:

Начало

Работой с базой данных

  1. Научимся записывать данные в базу
  2. Научимся читать данные из базы
  3. Научимся удалять данные с базы

Напишем telegram бота с логикой анкетирования

  1. Сможем получить имя
  2. Сможем получить пол
  3. Сможем получить возраст

Работой с кнопками

  1. Сделаем 2 кнопки (Заполнить анкету\Информация)
  2. Сделаем текстовое меню
  3. Сделаем выбор пола через текстовое меню

Начало

Для начала давайте создадим новый проект, создаём папку "bot_ankets"
В папке создадим 4 файла:
1. main.py - в него будем писать основной код бота
2. config.py - в него запишем токен бота, id админа, данные для подключения к базе данных
3. class_db.py - в него напишем вспомогательный класс для работы с базой данных
4. test.py - в нём я записываю все клавиатуры, если такое название вам не нравиться, вы можете назвать файл "keyboards.py", от названия ничего не зависит

Давайте заполним файл config.py:

token = 'Токен телеграм бота'
admin = 132 #ID админа (позже научу его получать)
host = 'localhost' #IP для подключения к базе данных (Ничего не менять, это локальный хостинг)
port = 8080 #Порт для подключения к базе данных (Ничего не меняем, мы его меняли при создании из первого урока)
user = 'root' #Пользователь для подключения к базе данных (Стандартный - root)
password = 'Password' #Пароль для подключения к базе данных (Замените на свой который вписывали на первом уроке)
database = 'test_basa' #Имя базы данных которую мы создали в первом уроке

И так мы заполнили конфиг, нажимаем сочетание клавиш ctrl+s для сохранения изменений

Далее давайте напишем вспомогательный класс для работы с базой данных в файл class_db.py:

import pymysql #Импортируем модуль pymysql который установили в первом уроке
from config import host, port, user, password, database #Импортируем из файла config.py данные для подключения к базе данных

class work_db:
	def __init__(self): #Функция для иницилизирования переменных в класс, запускать её не надо
		self.host = host
		self.port = port
		self.user = user
		self.password = password
		self.database = database

	def connect_db(self): #Функция для конекта к базе данных
		try:
			connection = pymysql.connect(
				host = self.host,
				port = self.port,
				user = self.user,
				password = self.password,
				database = self.database,
				cursorclass = pymysql.cursors.DictCursor
			)
			return connection #Если конект прошёл успешно, возвращаем переменную connection, для дальнейшей работы
		except Exception as eror:
			print(eror) #Если произошла ошибка, выводим её в консоль
			return False #Если произошла ошибка, возвращаем False

	def connect_close(self, connection): #Передаём переменную connection если в коде подключились, после каждого подключения к базе данных и выполнения какого-либо когда, нужно закрывать подключение
		try:
			connection.close() #Закрываем подключение к базе данных
			return True #Если успешно закрыли подключение к базе, возвращаем True
		except Exception as eror:
			print(eror) #Если произошла ошибка, выводим её в консоль
			return False #Если произошла ошибка, возвращаем False

	def create_table(self, connection, creat): #Функция для создания таблице в базе данных, в creat будем передавать текст запроса
		try:
			with connection.cursor() as cursor:
				cursor.execute(creat)
			connection.commit()
			return True
		except Exception as eror:
			print(eror) #Если произошла ошибка, выводим её в консоль
			return False #Если произошла ошибка, возвращаем False

	def edit_table(self, connection, edit): #Функция для редактирования данных в таблице, в edit будем передавать текст запроса
		try:
			with connection.cursor() as cursor:
				cursor.execute(edit)
			connection.commit()
			return True
		except Exception as eror:
			print(eror) #Если произошла ошибка, выводим её в консоль
			return False #Если произошла ошибка, возвращаем False

	def info_table(self, connection, info): #Функция для получения данных из таблицы, в info будем передавать текст запроса
		try:
			with connection.cursor() as cursor:
				cursor.execute(info)
			return cursor.fetchall()[0] #Возвращаем всё что нашли
		except Exception as eror:
			print(eror) #Если произошла ошибка, выводим её в консоль
			return False #Если произошла ошибка, возвращаем False

	def delete_from_table(self, connection, delete_): #Функция для редактирования данных в таблице, в delete_ будем передавать текст запроса
		try:
			with connection.cursor() as cursor:
				cursor.execute(delete_)
			connection.commit()
			return True
		except Exception as eror:
			print(eror) #Если произошла ошибка, выводим её в консоль
			return False #Если произошла ошибка, возвращаем False

И так класс для работы с базой данных написан, опять же сохраняем данные (нажимаем сочетание клавиш ctrl+s)

Давайте заполним файл test.py записав в него клавиатуру:

from aiogram.types import KeyboardButton, ReplyKeyboardMarkup #Импортируем классы для работы с текстовым меню\текстовыми кнопками

mainy = [['Заполнить анкету'], ['Информация']] #Создайм список кнопок для текстового меню в данном примере кнопки расположены в 2 строки
#Можно использовать такой список: [['Заполнить анкету', 'Информация']] Кнопки будут расположены в 1 строку

mark_menu = ReplyKeyboardMarkup(mainy, resize_keyboard=True) #Создаём клавиатуру, передав список представленный выше
#resize_keyboard=True нужен для того что-бы кнопки были маленькие, вы можете его на вписывать и посмотреть как это будет выглядеть

И так клавиатура создана, приступим к основному коду telegram бота, писать будем в файле main.py:

Запишем нужные нам импорты:

from aiogram.dispatcher.filters.state import State, StatesGroup
from aiogram import Bot, Dispatcher, executor, types
from aiogram.dispatcher import FSMContext
from aiogram.types import ReplyKeyboardRemove
from class_db import work_db #Импорируем класс для работы с базой данных из файла class_db.py
import test as bb #Импортируем файл test.py в переменную bb (мне так удобнее работать с ним)
import config as c #Импортируем файл config.py в переменную с (опять же мне так удобнее)

Создадим нужные нам экземпляры классов для работы:

bot = Bot(token=c.token) #Берём токен бота из файла config.py который импортировали ранее в переменную c
dp = Dispatcher(bot)
db_work = work_db(c.host, c.port, c.user, c.password, c.database) #Создаём экземпляр класса для работы с базой данных, передав все нужные параметры из конфига

Пишем обработчик текстовых сообщений:

@dp.message_handler(content_types=['text']) #Обрабатываем каждое сообщение от пользователей, добавляем content_types для обработки текста, в будущих уроках будет фото и видео
async def text(message: types.Message): #Асинхронная функция с название "text" которая принимает в переменную message, текст пользователя отправевшего сообщение
	pass

Давайте напишем логику обработки сообщений, начнём с кнопки "информация" и команду /start:

@dp.message_handler(content_types=['text']) #Обрабатываем каждое сообщение от пользователей, добавляем content_types для обработки текста, в будущих уроках будет фото и видео
async def text(message: types.Message): #Асинхронная функция с название "text" которая принимает в переменную message, текст пользователя отправевшего сообщение
	if message.text == '/start':
		await bot.send_message(message.chat.id, 'Привет, заполни анкету, или посмотри информацйию о мне', reply_markup=bb.mark_menu) #Обрабатываем команду /start и выводим клавиатуру из файлы test.py
	elif message.text == 'Информация':
		await bot.send_message(message.chat.id, f'Сюда пишем какую либо информацию, например я хочу вывести ID пользователя\nТвой ID: {message.chat.id}')

А теперь давайте напишем логику самого анкетирования

Перед началом объясню как работает if, elif, else

Допустим нам нужно проверить какое число пришло в функцию
И в зависимости от числа вернуть какой то текст

Для этого мы пишем саму функцию и запуск её с каким либо числом:

def info_number(number): #Данная функция принимает число в переменную number
    if number == 1: #Если полученное число равно одному, то возращаем слово Один
       return 'Один'
    elif number == 2: #Если же число равно двум, то возращаем слово Два
       return 'Два'
    else: #В ином случае говорим что такие числа мы не принимаем
       return 'Я не принимаю такие числа'

Ещё нам понадобиться такая вещь как машина состояний, мы её уже импортировали в начале

Давайте расскажу для чего нам машина состояний...

Она нам нужна для того что-бы работать с определённым пользователем, и определённой логикой
Это всё что нам требуется на этом этапе да и в принципе в программировании

Продолжим писать логику
Создаём класс для работы с машиной состояний:

class anketa(StatesGroup):
	name = State()
	floor = State()
	age = State()

Далее дописываем ожидание именно имени, у нас получился код:

@dp.message_handler(content_types=['text']) #Обрабатываем каждое сообщение от пользователей, добавляем content_types для обработки текста, в будущих уроках будет фото и видео
async def text(message: types.Message): #Асинхронная функция с название "text" которая принимает в переменную message, текст пользователя отправевшего сообщение
	if message.text == '/start':
		await bot.send_message(message.chat.id, 'Привет, заполни анкету, или посмотри информацйию о мне', reply_markup=bb.mark_menu) #Обрабатываем текст /start и выводим клавиатуру из файлы test.py
	elif message.text == 'Информация':
		await bot.send_message(message.chat.id, f'Сюда пишем какую либо информацию, например я хочу вывести ID пользователя\nТвой ID: {message.chat.id}')
	elif message.text == 'Заполнить анкету':
		await bot.send_message(message.chat.id, 'Введите своё имя:')
		await anketa.name.set()

В файле test.py добавляем клавиатуру с выбором пола:

main_floor = [['Мужской', 'Женский']]
menu_floor = ReplyKeyboardMarkup(main_floor, resize_keyboard=True)

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

@dp.message_handler(state=anketa.name)
async def anketa_name(message: types.Message, state: FSMContext): #Как видите у нас добавился FSMContext, он нам нужен как раз для обработки машины состояния
	#Записываем имя в кеш
	await state.update_data(name=name)
	#Далее просим ввести пол, с выбором пола из кнопок
	await bot.send_message(message.chat.id, 'Выберите свой пол:', reply_markup=bb.main_floor) #добавляем вывод кнопок с выбором пола

Пишем обработчик ввода пола:

@dp.message_handler(state=anketa.floor)
async def anketa_name(message: types.Message, state: FSMContext):
	#Записываем пол в кеш
	await state.update_data(floor=floor)
	#Далее просим ввести свой возраст
	await bot.send_message(message.chat.id, 'Выберите свой пол:', reply_markup=ReplyKeyboardRemove()) #убираем текстовую клавиатуру
	await anketa.age.set()

И наконец давайте обработаем возраст, и выведем данные которые ввёл пользователь, сначала пользователю, а затем админу по его ID из config.py

У нас получился такой код:

@dp.message_handler(state=anketa.age)
async def anketa_name(message: types.Message, state: FSMContext):
	#Получим все данные записанные в кеш
	data = await state.get_data()
	#Сортируем их по переменным
	name = data['name'] #В ковычках пишем name, потому что сами записывали в такую переменную, там может быть любая другая
	floor = data['floor'] #В ковычках пишем floor, потому что сами записывали в такую переменную, там может быть любая другая
	age = message.text #записываем ввод пользователя в переменную age
	await state.finish() #Завершаем работу с машиной состояний
	#Выведем анкету пользователю
	await bot.send_message(message.chat.id, f'Ваша анкета:\n\nИмя: {name}\nПол: {floor}\nВозраст: {age}\n\nСпасибо за уделение времени', reply_markup=bb.mark_menu) #Отправляем анкету, благодарим, и выдаём ему текстовоем меню
	#Отправим анкету администратору по его ID из config.py
	#Так же выведем ID пользователя и его юзернейм
	await bot.send_message(c.admin, f'Пользователь: {message.chat.id}\n@{message.from_user.username} заполнил анкету\nЕго данные:\n\nИмя: {name}\nПол: {floor}\nВозраст: {age}')


if __name__ == "__main__": #Если скрипт запущен с этого файла, запускаем executor
    executor.start_polling(dp, skip_updates=True) #skip_updates=True - нужен для того чтобы не обрабатывать сообщения которые были присланы пользователем в тот момент когда бот был выключен

Возможно у вас возникнет вопрос зачем я использую "\n" это для того что-бы перенести текст на 1 или более строк.

В итоге у нас получился такой код:

from aiogram.dispatcher.filters.state import State, StatesGroup
from aiogram import Bot, Dispatcher, executor, types
from aiogram.dispatcher import FSMContext
from aiogram.types import ReplyKeyboardRemove
from class_db import work_db #Импорируем класс для работы с базой данных из файла class_db.py
import test as bb #Импортируем файл test.py в переменную bb (мне так удобнее работать с ним)
import config as c #Импортируем файл config.py в переменную с (опять же мне так удобнее)



#Создаём класс бота и дистпетчера
bot = Bot(token=c.token) #Берём токен бота из файла config.py который импортировали ранее в переменную c
dp = Dispatcher(bot)
db_work = work_db(c.host, c.port, c.user, c.password, c.database) #Создаём экземпляр класса для работы с базой данных, передав все нужные параметры из конфига


class anketa(StatesGroup):
	name = State()
	floor = State()
	age = State()

	
@dp.message_handler(content_types=['text']) #Обрабатываем каждое сообщение от пользователей, добавляем content_types для обработки текста, в будущих уроках будет фото и видео
async def text(message: types.Message): #Асинхронная функция с название "text" которая принимает в переменную message, текст пользователя отправевшего сообщение
	if message.text == '/start':
		await bot.send_message(message.chat.id, 'Привет, заполни анкету, или посмотри информацйию о мне', reply_markup=bb.mark_menu) #Обрабатываем команду /start и выводим клавиатуру из файлы test.py
	elif message.text == 'Информация':
		await bot.send_message(message.chat.id, f'Сюда пишем какую либо информацию, например я хочу вывести ID пользователя\nТвой ID: {message.chat.id}')
	elif message.text == 'Заполнить анкету':
		await bot.send_message(message.chat.id, 'Введите своё имя:')
		await anketa.name.set()


@dp.message_handler(state=anketa.name)
async def anketa_name(message: types.Message, state: FSMContext): #Как видите у нас добавился FSMContext, он нам нужен как раз для обработки машины состояния
	#Записываем имя в кеш
	await state.update_data(name=message.text)
	#Далее просим ввести пол, с выбором пола из кнопок
	await bot.send_message(message.chat.id, 'Выберите свой пол:', reply_markup=bb.main_floor) #добавляем вывод кнопок с выбором пола
	await anketa.floor.set()


@dp.message_handler(state=anketa.floor)
async def anketa_name(message: types.Message, state: FSMContext):
	#Записываем пол в кеш
	await state.update_data(floor=message.text)
	#Далее просим ввести свой возраст
	await bot.send_message(message.chat.id, 'Выберите свой пол:', reply_markup=ReplyKeyboardRemove()) #убираем текстовую клавиатуру
	await anketa.age.set()


@dp.message_handler(state=anketa.age)
async def anketa_name(message: types.Message, state: FSMContext):
	#Получим все данные записанные в кеш
	data = await state.get_data()
	#Сортируем их по переменным
	name = data['name'] #В ковычках пишем name, потому что сами записывали в такую переменную, там может быть любая другая
	floor = data['floor'] #В ковычках пишем floor, потому что сами записывали в такую переменную, там может быть любая другая
	age = message.text #записываем ввод пользователя в переменную age
	await state.finish() #Завершаем работу с машиной состояний
	#Выведем анкету пользователю
	await bot.send_message(message.chat.id, f'Ваша анкета:\n\nИмя: {name}\nПол: {floor}\nВозраст: {age}\n\nСпасибо за уделение времени', reply_markup=bb.mark_menu) #Отправляем анкету, благодарим, и выдаём ему текстовоем меню
	#Отправим анкету администратору по его ID из config.py
	#Так же выведем ID пользователя и его юзернейм
	await bot.send_message(c.admin, f'Пользователь: {message.chat.id}\n@{message.from_user.username} заполнил анкету\nЕго данные:\n\nИмя: {name}\nПол: {floor}\nВозраст: {age}')


if __name__ == "__main__": #Если скрипт запущен с этого файла, запускаем executor
    executor.start_polling(dp, skip_updates=True) #skip_updates=True - нужен для того чтобы не обрабатывать сообщения которые были присланы пользователем в тот момент когда бот был выключен

Работу с базой данных я перенёс на третий урок, потому что информация для некоторых новая, и её достаточно много для новичка, поэтому ожидайте третий урок

В третьей части мы научимся работать с:

  1. Работой с базой данных
    1. Научимся записывать данные в базу
    2. Научимся читать данные из базы
    3. Научимся удалять данные с базы
  2. Работой с кнопками
    1. Сделаем инлайн кнопки (Расскажу и покажу что это и с чем это едят)
    2. Сделаем обработку таких кнопок, сделаем им не сложную логику

Так же советую посмотреть мою статью по форматированию сообщений - *КЛИКАБЕЛЬНО*

Но для них нам придётся переписать строку

bot = Bot(token=c.token)

На:

bot = Bot(token=c.token, parse_mode=types.ParseMode.HTML)

Как видим у нас добавился parse_mode, это как раз для обработки HTML тегов в сообщениях...

Домашнее задание:

1. Выдели сообщения (запрос имени, пола, возраста) в жирный шрифт
(по желанию можешь добавить наклонный текст)
2. Так же выдели в итоговой анкете, ID пользователя (само число)
в Текст для лёгкого копирования (Моноширинный)
(и у пользователя и у админа)

В обратной связи жду:
1. Скриншот кода где видно, как реализовано выделение текста
2. Скриншот с telegram бота, где видно заполнение анкеты + выделения текстов
3. Скриншот с telegram бота где видна конечная анкета (у пользователя и админа)
Прислать можно: @Xacker_Name_new

Спасибо за внимание

С вами был @Xacker_Name_new

Поддержка: @Bsc_Black_Secret_Club_bot