Всем привет!
15.02.2023. О том как я делал бота, который файлы с яндекс диска показывает, для лично-производственных целей.
Не маленькое предисловие (хотите – пропустите, оно не про бот)
Сначала думал писать. Потом взглянув с высоты полета - не писать. Ну а после всё-таки решил написать свой опыт ТГ-ботостроения. Парочку моментов, которые нигде не встречаются, может кому и пригодятся. Всё что будет далее — это так сказать проба пера. Хабр давно просматриваю и читаю, а иногда что-то и использую.
Я по образованию инженер-гидротехник - речные порты, причальные стенки, набережные, водозаборы, плотины и ГЭС — вот это всё я учился строить из арматуры, бетона, щебня и песка. А так в производстве начинал с ПГС (промышленно-гражданское строительство, а не песчано-гравийная смесь) в РБ, сейчас работаю в строительстве нефте-газового комплекса в РФ. Это для справки, что б не думали, что я программист). Давно уже работаю в производственно-техническом отделе – ПТО-шник я :-)
В 2022 начал глядеть что там с ЯП делается, а там вроде Python подает надежды, почитал поглядел всякие сравнения ну и выбрал его. Привлекала по отзывам работа с данными, нейросетями и генетическими алгоритмами, для оживления своей проги для рулетки - для чего ж еще. Начал Лутца читать, других книг накачал, что-то полистал. Стал смотреть, изучать инструментарий - версии питона, его юпитерноутбуки, анаконду, VSC, PyCharm. Вот кто бы еще расписал по полкам как этим PyCharm пользоваться, приёмы работы с ним какие-нибудь, нет же в инете (или не нашёл), копипастят друг у друга одно и тоже. GUI пробовал, штатные не впечатлили, как и QT, может не распробовал. Нашел Delphi4Python, полез смотреть что там в стане их делается, попробовал Александрию - понравилось, что есть порт в питон, даже как-то работает, изучаю по мере возможности. Нашел их бесплатный PyScripter, понравился, на нем и сижу пока. Нет у меня таких больших сложностей, его хватает.
Набрасываю по работе скрипты-помогаторы: то архивчик какой сделать, то переименовать что-нибудь в количестве 200-300файлов. И тут в 2023г узнаю про телеграмм и его ботов. По работе в основном вацап был для групповых чатов, потому и не пересекался. Фух, хватит, наверное, погнали про бота).
Задача
Имеется огромный массив нормативно-технической документации: ГОСТы, СП, ОР, РД, СТО, СНиП, СанПиН и пр. и пр. и пр. За время работы много чего такого накопляется у любого ПТО-шника. Много кто таскают для этого внешние диски.
Еще за время ведения объекта идет накопление/создание исполнительной и не только - различной документации: проект, переписка, АВК, АОСР, др. акты, договора, согласования, приказы, разрешительная, допускная. И вот подумалось – кладем это все в облако, а доступ дает бот по запросу, а документ может потребоваться много кому из ИТР, даже когда находишься в поле или отпуске (а то беги к компу присылай инфу, найти как всегда никто ничего не может))) Вот к боту их и посылать всех, пусть он отдувается). Я ж ленивый, вот и приходится изучать программирование что б комп за себя заставлять работать)).
В целом в мире сейчас идет движуха по BIM технологиям в строительстве. Такой бот будет примерно где-то около такой темы. Планы на будущее: отправляешь боту акт входного контроля, а он оформляет сам журнал верификации…эх, мечты :-)
Идеи и реализация
Бот решил пока сделать для нормативки, обкатается, потом можно будет подумать и про производственные потребности. Файлы будут в облаке по папкам. Бот должен в ответ на запрос присылать нормативку. Файлы в основной своей массе будут pdf, как универсальный формат не зависящий от кодировок и пр.
Функциональность
-
По запросу бот выдает инфу по нормативке. В облако по папкам раскладываю НТД, бот перебирает папку и если находит, то даёт ответ. В начале он у меня скачивал файл из облака на диск, отсылал пользователю, потом удалял файл, дабы не захламлять диск. И тут узкое место – может быть прислан не нужный пользователю файл. Но если бот деплоить (размещать на VPS/VDS) то диска так может и не хватить, поэтому впоследствии переделал на выдачу ссылок на файл
-
Папку поиска можно переключать командой. Команды сделал как русскими так и английскими, как заглавными так и строчными. Например: /gost (или /гост или /ГОСТ) – переключит поиск на папку с ГОСТами; /vsn – на папку с ВСН и т.д.
-
Команды /start, /about и /help – в принципе все стандартно.
-
Есть команда /sms – для отправки сообщения разработчику, т.е. мне).
-
Есть команда /status – на неё бот даёт ответ работает ли он и какая папка поиска текущая.
-
Боту можно отправить файл с НТД. Когда он не находит нормативку – так и пишет, если мол пришлёте, то после проверки потом уже будет. Принимает он pdf, doc форматы и складывает в облако в отдельную папку. Проверять буду вручную, а то мало ли понаприсылают картинок со взрослых сайтов.
-
Ведет отчет, в который пишет id пользователя и что запрашивали, смс-ки или присылали файлы.
-
Отдельно сделаны сервисные команды, нужны для управления ботом. Сделаны буквенно-цифирным кодом что б никто не догадался. По такой команде бот может прислать отчет (типа /покажи отчет) либо выгрузить его в облако (типа /отчет в облако). Есть еще мысля сделать полную остановку бота, но пока такую команду убрал – бъёт ошибки.
Реализация:
Регистрируем бота в ТГ, получаем токен – это как везде.
В яндексе через ID получаем токен для своего облачного диска – поищите, тоже все расписано.
Открываю чей-то пример реализации эхобота и погнал.
Токены храню в отдельном файле cfg_token.py, в нем 2 строчки с токенами:
telebot_token ='тут абракадабра от ТГ'
## токен ЯД
ya_dsk_token = 'тут абракадабра от ЯД'
Сначала использовал модуль PyTelegramBotAPI (Telebot), потом переделал на Aiogram – когда много пользователей, лучше, что б была асинхронность, вроде так. Еще нужен модуль от яндекса.
pip install aiogram
pip install yadisk
Блок импорта, все стандартно:
from aiogram import Bot, types
from aiogram.dispatcher import Dispatcher
from aiogram.utils import executor
import cfg_token
import yadisk
import glob, os
import sys
import time
from datetime import datetime
import sqlite3 as sl
Возможно лишние есть, пока делаешь по 100500раз попробуешь, то одно, то другое.
Начальные установки:
bot = Bot(token=cfg_token.telebot_token)
dp = Dispatcher(bot)
codirovk = 'utf-8'
# токен яндекс диска
y = yadisk.YaDisk(token=cfg_token.ya_dsk_token)
# загружаемый файл должен содержать в своем имени
format_name_files =['ГОСТ', 'Гост', 'гост', 'GOST', 'Gost', 'gost', 'SP', 'sp',
'СП', 'сп', 'VSN', 'vsn', 'ВСН', 'всн', 'STO', 'sto', 'СТО', 'сто']
# загружаемый файл должен иметь расширение
format_ext_files = ['.pdf', '.doc', '.docx', '.rtf']
search_dir = 'GOST' #папка по умолчанию стартовая для поиска
Кодировку пришлось указывать явно для текстового файла отчета, поэтому наличествует переменная codirovk. А то в одном файле даже азиатская ероглифическая начала иногда прорываться.
##----записать данные в рапорт---------------
def report_to_txt(str15):
try:
with open('Report.txt', 'a', encoding=codirovk) as file4:
file4.write(str15)
except Exception as e:
print('Ошибка: '+e)
Когда начал делать, то тренировался на папке ГОСТ. В облако там создавалась спец папка для загрузки файлов и отчетов, потом поправлю когда надо будет. Отработка команд как и в других примерах. Здесь их сократил, там у меня побольше написано, для растолкования
@dp.message_handler(commands=['start', 'старт']) ## команда /start
async def process_start_command(message: types.Message):
set_base_bot(message.from_user.id, 'GOST')
await bot.send_message(message.from_user.id, "Прива! Я бот-помошник! Ищу НТД и выдаю их Вам")
await bot.send_message(message.from_user.id, "Текущая папка для поиска НТД: "+get_base_bot(message.from_user.id)+'. Её можно переключить командой (см. /help)')
await bot.send_message(message.from_user.id, "Введите запрос на НТД (можно только номер или часть наименования):")
@dp.message_handler(commands=['help', 'хелп']) ## команда /help
async def process_help_command(message: types.Message):
# тут не выставляем папку поиска, берем ее из базы
await bot.send_message(message.from_user.id, "Введите запрос на НТД (можно только номер или часть наименования) и отправьте мне, а я поищу где-то и если найду, то отправлю Вам файл, по 1шт за раз.")
await bot.send_message(message.from_user.id, "В данный момент включен поиск в папке: "+get_base_bot(message.from_user.id))
Команда для переключения папки поиска, остальные сделаны аналогично:
@dp.message_handler(commands=['GOST', 'gost', 'ГОСТ', 'гост']) ## команда /GOST
async def process_gost_command(message: types.Message):
set_base_bot(message.from_user.id, 'GOST')
await bot.send_message(message.from_user.id, "Установлена текущая папка для поиска НТД: "+get_base_bot(message.from_user.id))
Команда для сообщения разработчику, использует ранее приведенную функцию report_to_txt:
@dp.message_handler(commands=['sms', 'смс']) ## команда /sms сообщение разработчику
async def process_sms_command(message: types.Message):
report_to_txt('nПользователь id'+str(message.from_user.id)+' отправил сообщение: '+message.text)
await bot.send_message(message.from_user.id, "Сообщение разработчику отправлено")
Запрос от пользователя. То, что запросил пользователь, может быть не одно, показываю до 7 найденных документов и сколько их всего, а то введут запрос = ‘5’ и выдаст 400шт документов. Поэтому алгоритм настроен на выдачу 1-го точного результата.
Долго мучался с сокращенным вариантом ссылки на файл и где-то в уголке интернета периферическим зрением заметил промелькнувший вариант. Вот много кто про это спрашивает, а прямого ответа нет. ВОТ ОН: y.publish(file) + y.get_meta(file).public_url (Кто ж знал что public_url надо сзади писать). Обрабатываю так:
@dp.message_handler(content_types=['text']) ## получаем сообщение от юзера
async def get_text_messages(message: types.Message):
search_dir = get_base_bot(message.from_user.id)
report_to_txt('nПользователь id'+str(message.from_user.id)+' сделал запрос на поиск в папке ' + search_dir +': '+message.text)
await bot.send_message(message.from_user.id, 'Запускаю процесс поиска в папке ' + search_dir +' : '+message.text)
if y.check_token():
# ищем в папке документ содержащий запрос
if not y.is_dir('/'+search_dir):
await bot.send_message(message.from_user.id, 'Папка ' + search_dir +' не обнаружена. Шо-то поломалось. Извините.')
else:
Spis = []
for item in y.listdir(search_dir):
if message.text in item['name']:
if len(Spis) < 7:
await bot.send_message(message.from_user.id, 'Обнаружен документ: '+item['name'])
Spis.append(item['name'])
if len(Spis) == 0:
await bot.send_message(message.from_user.id, 'Извините, пока такого документа не нашлось.')
await bot.send_message(message.from_user.id, 'Но если Вы мне его сюда скинете, после проверки я его добавлю.')
if len(Spis) == 1:
# ------------ВАР2 - даем ссылку на файл-------------------
y.publish('/'+search_dir+'/'+Spis[0]) # делаем публичный файл
# шлем ссылку
await bot.send_message(message.from_user.id, y.get_meta('/'+search_dir+'/'+Spis[0]).public_url)
# ------------ВАР1 - грузим файл в телегу через свой диск
# await bot.send_message(message.from_user.id, 'Загружаю. Ждите...')
# Скачивает на свой диск
# y.download('/'+search_dir+'/'+Spis[0], Spis[0])
# Отправляем в телегу
# f = open(Spis[0],"rb")
# await bot.send_document(message.from_user.id,f)
# f.close()
if len(Spis) > 1:
await bot.send_message(message.from_user.id, 'Найдено документов: '+str(len(Spis)) + '. Уточните запрос:')
else:
await bot.send_message(message.from_user.id, 'Извините по каким-то причинам диск не доступен. Попробуйте в другой раз')
# ВАР1 - когда скачиваем файл из облака для телеги на свой диск
# remove_files() - тут функция удаления загруженного файла
Загруженный файл от пользователя обрабатываю так:
@dp.message_handler(content_types=['document']) # получаем файл от юзера
async def handle_file(message):
try:
pr1=0
pr2=0
# проверяем, содержит ли имя файла нужное название
for item in format_name_files:
if item in message.document.file_name:
pr1=+1
# проверяем, нужного ли формата файл
for item in format_ext_files:
if item in message.document.file_name:
pr2=+1
# если файл такой как надо, то качаем
if (pr1 > 0) and (pr2 > 0):
file_id = message.document.file_id
file = await bot.get_file(file_id)
file_path = file.file_path
# ------------Вариант1 загрузки файлов на диск-------------------
## await bot.download_file(file_path, os.path.join('Download', message.document.file_name))
# ------------Вариант загрузки файлов в яндекс-облако------------
# путь к загружаемым в облако файлам от пользователей
src = '/GOST/Download/'+ message.document.file_name
print(src)
# грузим в облако файл от пользователя
if y.is_file(src): # если такой файл есть то яндекс даст ошибку, поэтому: вот
src = '/GOST/Download/Double-'+datetime.now().strftime("%d.%m.%Y-%H.%M.%S")+'-'+ message.document.file_name
y.upload(await bot.download_file(file_path), src)
await bot.send_message(message.from_user.id, 'Загрузил. Спасибо. После проверки добавлю в свою базу:)')
# сделать запись после загрузки в файл Report.txt
report_to_txt('nПользователь id='+str(message.from_user.id)+' прислал файл: '+message.document.file_name)
else:
await bot.send_message(message.from_user.id, 'Простите, но присланный Вами файл не содержит в имени тип НТД (ГОСТ, СП, ВСН и т.д.) и/или не подходит по формату, нужен .pdf или .doc')
except Exception as e:
print('Ошибка: '+e)
await bot.send_message(message.from_user.id, 'Я наверное не смогу загрузить, шо-то сломалось и выдает ошибку: '+e)
Практически бот готов, поллинг добавляем:
if __name__ == '__main__':
executor.start_polling(dp)
## executor.start_polling(dp, skip_updates=True)
##Параметр skip_updates=True позволяет пропустить накопившиеся входящие сообщения, если они нам не важны
Но как Вы могли заметить по коду, кое-что там еще незнакомое мелькает:
get_base_bot(message.from_user.id)
set_base_bot(message.from_user.id, 'GOST')
А возникла следующая проблема: если Ваня выберет папку ГОСТ, и пока будет вбивать запрос, то Катя в это время переключит папку поиска на СП и Ваня получит результат по папке СП. Прикольно. «Шо делать, шо делать? — Шо‑то надо. Мда.»
В мыслях 2 варианта: через файл типа txt и пр., либо через базу данных. Выбрал через встроенный в питон sqlite3. В маленькой DB будут храниться id и выбранная папка. Для работы нужны 2 функции одна устанавливает, другая читает. На самом деле они почти одинаковые и есть мысли сделать одну. Когда наименование папки будет пустое (=’’) – то get, а если указано, то set. Может потом поправлю. Программу же можно улучшать бесконечно.
В итоге вот эти функции:
# запись данных о юзере - установка папки поиска для юзера, шоб друг другу не сбивали
def set_base_bot(user_id, name_dir):
con = sl.connect('databasebot.db')
with con:
cur = con.cursor()
cur.execute("CREATE TABLE IF NOT EXISTS user_seadir(id INTEGER NOT NULL PRIMARY KEY, seadir TEXT)")
con.commit()
with con:
cur = con.cursor()
cur.execute("SELECT seadir FROM user_seadir WHERE id = " + str(user_id))
dat = cur.fetchone()
if dat is None:
cur.execute('INSERT INTO user_seadir (id, seadir) values(?, ?)', (user_id, name_dir))
con.commit()
else:
cur.execute('UPDATE user_seadir SET seadir = ? WHERE id = ?', (name_dir, user_id))
con.commit()
con.close()
# получение данных о юзере - запрос папки поиска для юзера
def get_base_bot(user_id):
con = sl.connect('databasebot.db')
with con:
cur = con.cursor()
cur.execute("CREATE TABLE IF NOT EXISTS user_seadir(id INTEGER NOT NULL PRIMARY KEY, seadir TEXT)")
cur.commit()
with con:
cur = con.cursor()
cur.execute("SELECT seadir FROM user_seadir WHERE id = " + str(user_id))
dat = cur.fetchone()
if dat is not None:
return dat[0]
else:
return 'GOST'
cur.execute('INSERT INTO user_seadir (id, seadir) values(?, ?)', (user_id, 'GOST'))
con.commit()
con.close()
По этим функциям еще разбираться надо, где что как коммитить, не идет ли перезапись файла, в общем вопросов по нюансам много, а об них мало кто пишет, и информация в интернете разнится.
Ну а сейчас Вам будет смешно. Get как надо работает, Set ошибку бьет (выше то уже поправленная):
def set_base_bot(user_id, name_dir):
…
cur.execute('UPDATE user_seadir SET seadir = ? WHERE id = ?', (user_id, name_dir))
Попоискал я какого хутора не пашет, все оказалось просто – питон тоже не знает, что мне надо :-)
UPD. Пока делал статью, свёл 2 вышеприведенные функции в одну:
def sget_base_bot(user_id, name_dir):
con = sl.connect('databasebot.db')
cur = con.cursor()
with con:
cur.execute("CREATE TABLE IF NOT EXISTS user_seadir(id INTEGER NOT NULL PRIMARY KEY, seadir TEXT)")
with con:
cur.execute("SELECT seadir FROM user_seadir WHERE id = " + str(user_id))
dat = cur.fetchone()
if name_dir == '': # блок запроса установленной папки
if dat is not None:
return dat[0]
else:
return 'GOST'
cur.execute('INSERT INTO user_seadir (id, seadir) values(?, ?)', (user_id, 'GOST'))
else: # блок установки папки поиска
if dat is None:
cur.execute('INSERT INTO user_seadir (id, seadir) values(?, ?)', (user_id, name_dir))
else:
cur.execute('UPDATE user_seadir SET seadir = ? WHERE id = ?', (name_dir, user_id))
return name_dir
cur.close()
con.close()
Эта функция стала двойной – set и get в одном флаконе: если строка указана name_dir ,то работает как set – установка папки, если прислано ‘’ (пустая строка) – выдача инфы по установленной папке. Сами папки поиска указаны в ответе на команду /help и есть в меню. Со временем ассортимент будет расширяться.
end UPD
При первом появлении в боте пользователя, ему папку включаю ГОСТ. А дальше он сам выберет, help и меню в помощь.
Сделал также и меню для бота. Тоже опишу, а то сумбур в инете. Сделал файл с командами txt, даже не файл – просто напишите текст для копипасты хоть в блокноте, хоть в ворде. Вот мой:
Далее идем как везде пишут, доходим до сообщения и далее по пунктам 1-нажать кнопку, 2- появится приглашение, 3-копипастим наш список команд и жмем на отсылку. Усё.
Возможно, моя беда была в многострочном сообщении. Но в конце концов менюшка получилась:
Структура
Структура бота в папке счас такая:
1.__pycache__ - папка
2.bot.pyw
3.cfg_token.py
4.databasebot.db
5.Report.txt
Папка п.1 создается самим Python. В ней он складывает байткодовый файл cfg_token.py. Её лучше никому не показывать, внутри можно токены вытянуть.
Файлы п.4 и п.5 делает программа сама себе – если удалить, вновь сделает).
Сам бот это п.2 - бот и п.3 - токены.
А так на яндекс диске:
Расширение бота сменил с .py на .pyw для не отображения окна консоли (работаю в Win).
Фух, вроде всё. А нет, про
Немного о хостинге
Итак варианты
1) на своем компе, запустил, инет есть – бот работает. Пока так мой работает, т.е. пока я на работе.
2) Ну в инете есть варианты с VPS/VDS – в основной своей массе платные. Делая бесплатного бота, я не готов к тратам. Вроде на сейчас PythonAnywhere остался более менее как вариант.
3) С двумя подвариантами – смартфон, он же по сути комп, все время в сети, все время работает, чем не сервер)). Из минусов – возможно садит батарею, но что её только не садит:
-
Pydroid3 – работает), но иногда, а то и часто прерывается. У этой программы крутые модули/пакеты платные, но с аиограмм и яндексом повезло);
-
UserLAnd с Ubunty CLI – работает еще круче, питон надо ставить (встроенный версии 2.7 вроде), пакеты тоже, еще mc (это что-то вроде нортона командера под dos) для забрать файлы бота из папки загрузок. Там в корне сделал папку bot, в неё и сложил файлы бота.
Команды которые использовал:
cd – переход в корень,
ls - просмотр каталога,
cd bot – переход в папку,
python3 bot.py – запускаю бота (расширение с pyw возвращаю на py)
Еще можно помудрить с сервисами, что б само запускалось и работало – не дошли руки. А еще мысля старый телефон под сервер для этого))
(Это мне выдало, когда на компе и смарте запустил бота, одновременная работа)
(выключаю утром бота на убунту, будет запущен на компе)
-
термукс работать отказался вообще в этом плане, может я не программист или не линуксоид, не победил я его.
Планы
-
Асинхронность встроенной базы данных – насколько актуально пока не знаю. Может и забить можно). В pip-ах там вроде кто-то сварганил, но, если не ошибаюсь это было для постгрес.
-
Делал сервисную команду остановки бота (типа /stop bot) через sys.exit() выдает кучу ошибок. Изучил много способов прерывания скрипта, пробовал – бьёт ошибки. Как понял, связано это с асинхронностью бота, возможно aiogram этому причина. Где-то выкладывали вариант бота со списком задач и постепенной остановке процессов бота и красивым, безошибочным остановом. Но, это пока не для меня, сложновато будет. Если не заморачиваться, то может и пойдет. Изучаю пока этот вопрос.
-
Выпросить у яндекса под бот отдельный диск и там поселить, наделать папок, насыпать нормативки. Для этого только токен в боте поменять. Upd сделано.
-
Не нравится мне поиск по папке, есть мысля завести отдельную таблицу в БД под ссылки на файлы + сервисную команду на индексацию надо будет придумать. Если файлов каких добавлю в облако, то и индексацию скомандую боту сделать. Либо вести такую таблицу по мере запросов: вначале в БД поиск, если нет, то поиск на диске.
-
Ну и шлифовать код, до идеала)
Заключение
Создан инструмент для доступа и наполнения базы НТД через мессенджер.
Вроде все
П.с.1. Конструктивная критика приветствуется категорически.
П.с.2. Эксперименты с ботом не закончил, возможны сбои в работе в процессе, а так велкам кому надо – Normativkabot.
П.с.3. Если вдруг ТГ накроется, думаю не сложно будет GUI набросать с подобным функционалом и запустить по сарафанному радио.
Автор: Алексей Кравец