Сидя на freelance видел много раз задачи по сбору БД. Чаще всего просят собрать информацию о компаниях или специфические запросы на Google, Yandex картах.
Есть спрос, давайте создавать предложения, но обо всём по порядку.
В данной статье предлагаю разработать Telegram bot, который будет принимать название города (в котором будет производиться поиск) и запрос (по которому будет производиться поиск.
Например: Бар, Кофе, Ресторан и т.д.). Реализуем возможность делать donation, оплачивать услугу по сбору БД и отправлять клиентам на почту БД.
Используемые технологии:
Данные технологии или инструменты выбраны для удобства использования, возможности быстрой реализации и автоматизации задачи.
Эта программа не является коммерческой. Реализована для примера, т.к. правилами Яндекса запрещено сохранять данныеО правилах можно сказать то же, что и о законах: их многочисленность доказывает не столько соблюдение, сколько нарушение их. ©Дюбэ
Подготовка
Для удобства разделим проект на 3 файла. Создадим bot.py, yandex.py, send_email.py.
Реализация
- Импортируем библиотеки:
# -*- coding: utf-8 -*- import requests import xlwt, xlrd from xlutils.copy import copy as xlcopy
- Для обращение к Yandex API нужно получить ключ, по которому будут Вас идентифицировать. Ключи для использования maps api и location api разные.
Задаем api ключи:
apikey = '*******-***-****-****-************' apikey_location = '********-****-****-****-************'
- Получение общего кол-во найденных объектов:
def sum_taken_object(all_informations): try: found = str(all_informations['properties']['ResponseMetaData']['SearchResponse']['found']) except KeyError: found = '-' return found
- Получение всей информации:
В запросе requests.get параметр bbox указывает на координаты области поиска, а ll на центр области поиска. Можно указать только параметр ll без bbox, если нужны результаты только в центре города. О дополнительных возможностях можете почитать тут.def get_all_infomations(text, city): value_low_upp, point = get_location(city) low = value_low_upp[0].split(',') upp = value_low_upp[1].split(',') informations = requests.get('https://search-maps.yandex.ru/v1/?' 'apikey='+apikey+'&' 'text='+text+'&' 'lang=ru_RU&' 'll='+point[0]+'&' 'bbox='+low[1]+','+low[0]+'~'+upp[1]+','+upp[0]+'&' 'results=500') return informations.json()
Обратите внимание, что больше 500 объектов запрос не вернет.
Если будет интересна реализация сбора всех объектов превышающих кол-во 500, отпишитесь. Сделаю отдельный пост.
- Получение долготы и широты:
Возвращает долготу и ширину координаты области и центра.def get_location(city): location = requests.get('https://geocode-maps.yandex.ru/1.x/?' 'apikey='+apikey_location+'&' 'geocode='+city.title()+'&' 'format=json') loc = location.json() loc = loc['response']['GeoObjectCollection']['featureMember'] point = loc[0]['GeoObject']['Point']['pos'].split() value_lower = loc[0]['GeoObject']['boundedBy']['Envelope']['lowerCorner'].split() value_upper = loc[0]['GeoObject']['boundedBy']['Envelope']['upperCorner'].split() value_low_upp = [value_lower[1]+','+value_lower[0], value_upper[1]+','+value_upper[0]] return value_low_upp, point
- Получаем ограниченное кол-во объектов:
Возвращает словарь с информацией по 5 объектам. Если хотите изменить кол-во объектов, тогда измените значение двух переменных resul, skip в запросе requests.get.def get_information_limit(text,city): value_low_upp, point = get_location(city) low = value_low_upp[0].split(',') upp = value_low_upp[1].split(',') info = requests.get('https://search-maps.yandex.ru/v1/?' 'apikey='+apikey+'&' 'text='+text+'&' 'lang=ru_RU&' 'll='+point[0]+'&' 'bbox='+low[1]+','+low[0]+'~'+upp[1]+','+upp[0]+'&' 'results=5&' 'skip=5') information = info.json() information = information['features'] list = {} i = 1 for key in information: try: coordinates = str(key['geometry']['coordinates']) except KeyError: coordinates = '-' try: name = str(key['properties']['CompanyMetaData']['name']) except KeyError: name = '-' try: address = str(key['properties']['CompanyMetaData']['address']) except KeyError: address = '-' try: url = str(key['properties']['CompanyMetaData']['url']) except KeyError: url = '-' try: phones = key['properties']['CompanyMetaData']['Phones'] except KeyError: phones = '-' try: hours = str(key['properties']['CompanyMetaData']['Hours']['text']) except KeyError: hours = '-' for k in phones: try: phones = k['formatted'] except TypeError: pass list['object'+str(i)] = {'coordinates':coordinates,'name':name,'address':address,'url':url,'phones':phones,'hours':hours} i += 1 return list
- Запись базы данных в Excel файл:
def write_exl(text, city_name): try: name_excel_BD = creat_excel_file(name='{}_{}'.format(text, city_name)) read_book = xlrd.open_workbook(name_excel_BD) # Открываем исходный документ write_book = xlcopy(read_book) # Копируем таблицу в память, в неё мы ниже будем записывать write_sheet = write_book.get_sheet(0) # Будем записывать в первый лист # index = read_book.sheet_by_index(0).nrows # Номер последней строки url_maps = 'https://yandex.ru/maps/org/' info = get_all_infomations(text=text, city=city_name) for number, inf in enumerate(info['features']): try: name_object = inf['properties']['CompanyMetaData']['name'] except KeyError: name_object = '-' try: address = inf['properties']['CompanyMetaData']['address'] except KeyError: address = '-' try: time_work = inf['properties']['CompanyMetaData']['Hours']['text'] except KeyError: time_work = '-' try: id_organization = inf['properties']['CompanyMetaData']['id'] except KeyError: id_organization = '-' write_sheet.write(int(number), 0, city_name) # Город write_sheet.write(int(number), 1, name_object) # Название объекта write_sheet.write(int(number), 2, address) # Адрес write_sheet.write(int(number), 3, time_work) # Время работы write_sheet.write(int(number), 4, url_maps + str(id_organization)) # Url на карте write_book.save(name_excel_BD) # Сохраняем таблицу return True, name_excel_BD except TypeError or KeyError as err: return False
- Создание файла Excel:
def creat_excel_file(name): book = xlwt.Workbook('utf8') book.add_sheet('База_{}'.format(name)) book.save('BD_{}.xls'.format(name)) return 'BD_{}.xls'.format(name)
- Импортируем библиотеки:
#!/usr/bin/env python # coding: utf8 from smtplib import SMTP_SSL from email.mime.multipart import MIMEMultipart from email.mime.base import MIMEBase from email import encoders import os
- Функция для отправки писем с вложением:
Если будете реализовывать через Gmail, Вам нужно зайти в личный аккаунт на Google – Безопасность и в «Ненадежные приложения, у которых есть доступ к аккаунту» нужно отключить, иначе отправка писем будет блокироваться на стороне сервера Gmail.def send_mail(name_file, to_address): filepath = name_file address_from = "********@gmail.com" address_to = to_address password = '************' mail_adr = 'smtp.gmail.com' mail_port = 465 # Compose attachment part = MIMEBase('application', "octet-stream") part.set_payload(open(filepath, "rb").read()) encoders.encode_base64(part) part.add_header('Content-Disposition', "attachment", filename="%s" % os.path.basename(filepath)) # Compose message msg = MIMEMultipart() msg['From'] = address_from msg['To'] = address_to msg.attach(part) # Send mail smtp = SMTP_SSL(mail_adr) smtp.set_debuglevel(1) smtp.connect(host=mail_adr, port=mail_port) smtp.login(address_from, password) smtp.sendmail(address_from, address_to, msg.as_string()) smtp.quit()
- Импортируем библиотеки:
# -*- coding: utf-8 -*- import telebot from telebot.types import LabeledPrice import re import yandex from send_email import send_mail
- Получаем token и добавляем в переменную:
token = '*********:**********************************' bot = telebot.TeleBot(token)
- Создаем обработку команд:
Первый IF обрабатывает запрос /start, второй IF обрабатывает запрос /donation.@bot.message_handler(commands=['start', 'donation']) def send_welcom(message): if message.text == '/start': keyboard = telebot.types.InlineKeyboardMarkup() keyboard.row(telebot.types.InlineKeyboardButton('Получить базу', callback_data='bd_get')) bot.send_message(message.chat.id, 'Выберите действие: ', reply_markup=keyboard) if message.text == '/donation': bot.send_invoice(message.chat.id, title='Donation', description='Можешь отправить финансовую благодарность.', invoice_payload='donation', provider_token='*********:TEST:*******', currency='RUB', prices=[LabeledPrice(label='Donation', amount=10000)], start_parameter='pay_start', photo_url='https://cdn.imgbin.com/22/0/5/imgbin-donation-computer-icons-' 'fundraising-justgiving-charitable-organization-donation-' '5Yehm9UecF2cRWrqtms4e6emn.jpg', photo_height=512, # !=0/None or picture won't be shown photo_width=512, photo_size=512, is_flexible=False)
В параметре invoice_payload задаем название, по которому будем определяться подтверждение оплаты.
В bot.send_invoice нужно указать token используемого провайдера платежей. Как его получить, написано здесь.
Для разработки советую использовать тестовый token провайдера. При его использовании функционал сохраняется, но деньги не снимаются. В дальнейшем Вам потребуется только заменить на реальный token.
Тестовый: 123:TEST:XXXX
Реальный: 123:LIVE:XXXX
- Данный декоратор необходим для подтверждения наличия заказа:
Т.к. у нас digital услуга, она всегда в наличии по этому, параметр ok всегда True. Без данного подтверждения оплата не будет проходить у клиента.@bot.pre_checkout_query_handler(func=lambda query: True) def checkout(message): bot.answer_pre_checkout_query(message.id, ok=True, error_message="Инопланетяне пытались украсть CVV вашей карты, " "но мы успешно защитили ваши учетные данные. " "Попробуй расплатиться через несколько минут, " "нам нужен небольшой отдых.")
Данный декоратор полезен если у Вас, например, магазин обуви. Клиент заходит оформить заказ, нажимает оплатить и в этот момент Вам на сервер приходит запрос, который обрабатывается этой функцией. Если на складе имеется данный товар, то параметр ok=True иначе задаете значение False и клиент видит всплывающее окно с текстом, который указан в параметре error_message.
- Декоратор подтверждает оплату заказа:
@bot.message_handler(content_types=['successful_payment']) def got_payment(message): if message.json['successful_payment']['invoice_payload'].split(',')[0] == 'buy': email = message.json['successful_payment']['order_info']['email'] bot.send_message(message.chat.id, 'Ураааа! Спасибо за оплату на сумму: `{} {}`.\n' 'Вам на почту (`{}`) отправленно письмо с базой данных.\n\n' 'По всем возникшим вопросам обращайтесь @имя_админа' .format(message.successful_payment.total_amount / 100, message.successful_payment.currency, email), parse_mode='Markdown') request_text = message.json['successful_payment']['invoice_payload'].split(',')[1] city = message.json['successful_payment']['invoice_payload'].split(',')[2] write_in_BD, name_excel_BD = yandex.write_exl(text=request_text, city_name=city) if write_in_BD == True: send_mail(name_file=name_excel_BD, to_address=email) elif message.json['successful_payment']['invoice_payload'] == 'donation': bot.send_video(message.chat.id, 'https://media0.giphy.com/media/QAsBwSjx9zVKoGp9nr/giphy.gif')
- Декоратор обрабатывает нажатые кнопки InlineKeyboardMarkup:
@bot.callback_query_handler(func=lambda call: True) def callback_key(message): if message.data == 'bd_get': input_city(message) elif re.search(r'bd_yes/',message.data): location = re.sub('bd_yes/','',message.data) bot.answer_callback_query(message.id, text=location, show_alert=False) input_text(message, city=location) elif message.data == 'bd_no': input_city(message) elif re.search(r'pay', message.data): info_get_bd_limit = re.split(r'/',message.data) sent_text = info_get_bd_limit[1] sent_city = info_get_bd_limit[2] found = info_get_bd_limit[3] bot.answer_callback_query(message.id, text='Оплата', show_alert=False) pay(message, text=sent_text, city=sent_city, found=found)
- Функция отправляет счет на оплату:
Параметр need_email необходим, так как нужно запросить у клиента почту, на которую будем отправлять БД.def pay(message, text, city, found): bot.send_invoice(message.from_user.id, title='База данных', description='Оплата базы данных по запросу.\n' 'Текст: '+text+'\nГород: '+city+'\nКол-во объектов: '+found, invoice_payload='buy,{},{}'.format(text, city), provider_token='*********:TEST:*******', currency='RUB', prices=[LabeledPrice(label='База данных', amount=20000)], start_parameter='pay_start', photo_url='https://encrypted-tbn0.gstatic.com/images?q=tbn:' 'ANd9GcRVUs3eGt4U9YSXZrsbOkJoNEdpcYUdq0vEzM-ci_oIxEWs1FK0', photo_height=300, photo_width=300, photo_size=300, need_email=True, is_flexible=False)
- Функция запрашивает название города, по которому будет производиться поиск:
def input_city(message): bot.send_message(message.from_user.id, 'Введите город:') bot.register_next_step_handler_by_chat_id(message.from_user.id, get_city) bot.answer_callback_query(message.id, text='Введите город', show_alert=False)
- Функция отправляет клиенту геолокацию, для уточнения найденного города:
def get_city(message): get_location, get_location_point = yandex.get_location(message.text) keyboard = telebot.types.InlineKeyboardMarkup() keyboard.row(telebot.types.InlineKeyboardButton('Да', callback_data='bd_yes/'+message.text), telebot.types.InlineKeyboardButton('Нет', callback_data='bd_no')) bot.send_location(message.chat.id, get_location_point[1], get_location_point[0], reply_markup=keyboard)
- Функция запрашивает ввод объекта, который хочет искать клиент:
def input_text(message, city=None): global location_city location_city = city info_message_id = bot.send_message(message.from_user.id, 'Введите объект (например бар): ') bot.register_next_step_handler_by_chat_id(message.from_user.id, get_bd_limit)
- Функция отправляет ограниченное кол-во информации о найденных объектах и предлагает оплатить полную БД:
def get_bd_limit(message): information = yandex.get_information_limit(message.text, location_city) if information: found = yandex.sum_taken_object(yandex.get_all_infomations(message.text, location_city)) bot.send_message(message.from_user.id, 'Бот работает в бесплатном режиме.\n' 'Будет выведено только 5 первых попавшихся результатов.') i = 1 text = '' for key, value in information.items(): name = value['name'] address = value['address'] url = value['url'] phones = value['phones'] hours = value['hours'] text = text + '' + str(i) + ') Название: '+name+'\nАдрес: '+address+'\nСайт: '+url+ '\nТелефон: '+phones+'\nВремя работы: '+hours+'\n----------\n' i += 1 bot.send_message(message.from_user.id, text, disable_web_page_preview=True) keyboard = telebot.types.InlineKeyboardMarkup() keyboard.row(telebot.types.InlineKeyboardButton('Оплатить 200 руб.', callback_data='pay/' + message.text+'/'+location_city+'/'+found)) bot.send_message(message.from_user.id, 'Всего найдено объектов по вашему запросу: ' + found+'' '\n\nВсю базу вы можете получить на почту.', reply_markup=keyboard) else: bot.send_message(message.from_user.id, 'Ничего не найдено по данному запросу!')
- В конце кода добавляем по рекомендациям Telegram:
while True: try: bot.polling(none_stop=True) except Exception as e: time.sleep(15)
Сейчас Telegram поддерживает 8 платежных систем. Для российского рынка самые ходовые это Яндекс Касса и Сбербанк. Советую использовать Tranzzo, т.к. с помощью него можно сохранять данные карты, использовать отпечаток пальца для оплат в течение 5 часов после подтверждения паролем.
При использовании тестового token от Сбербанк процесс выставление счет зависал, а при использовании Яндекс Кассы все хорошо. Не знаю с чем это связанно, но факт.
Заключение
Данный пост предназначен не только для того, чтобы показать возможности реализации mini-digital-business, но и для того чтобы расшевелить в тебе it предпринимателя, начать генерировать новые идеи где можно было бы применить данный функционал.
Что заработал, то и получил: ударник — хлеб, а лодырь — ничего.
empenoso
А какое практическое применение возможно?
y_manager
Оплата прямо в Телеграме без перенаправки. Любое применение.