https://habr.com/ru/post/495594/- Python
- Maps API
- API
- Бизнес-модели
Сидя на freelance видел много раз задачи по сбору БД. Чаще всего просят собрать информацию о компаниях или специфические запросы на Google, Yandex картах.
Есть спрос, давайте создавать предложения, но обо всём по порядку.
В данной статье предлагаю разработать Telegram bot, который будет принимать название города (в котором будет производиться поиск) и запрос (по которому будет производиться поиск.
Например: Бар, Кофе, Ресторан и т.д.). Реализуем возможность делать donation, оплачивать услугу по сбору БД и отправлять клиентам на почту БД.
Используемые технологии:
- Telegram API;
- Yandex API;
- Payments API.
Данные технологии или инструменты выбраны для удобства использования, возможности быстрой реализации и автоматизации задачи.
Эта программа не является коммерческой. Реализована для примера, т.к. правилами Яндекса запрещено сохранять данныеО правилах можно сказать то же, что и о законах: их многочисленность доказывает не столько соблюдение, сколько нарушение их. ©Дюбэ
Подготовка
Для удобства разделим проект на 3 файла. Создадим
bot.py,
yandex.py,
send_email.py.
Реализация
Yandex.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
- Получение всей информации:
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()
В запросе requests.get параметр bbox указывает на координаты области поиска, а ll на центр области поиска. Можно указать только параметр ll без bbox, если нужны результаты только в центре города. О дополнительных возможностях можете почитать тут.
Обратите внимание, что больше 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
Возвращает долготу и ширину координаты области и центра.
- Получаем ограниченное кол-во объектов:
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
Возвращает словарь с информацией по 5 объектам. Если хотите изменить кол-во объектов, тогда измените значение двух переменных resul, skip в запросе requests.get.
- Запись базы данных в 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)
Send_email.py
- Импортируем библиотеки:
#!/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
- Функция для отправки писем с вложением:
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()
Если будете реализовывать через Gmail, Вам нужно зайти в личный аккаунт на Google – Безопасность и в «Ненадежные приложения, у которых есть доступ к аккаунту» нужно отключить, иначе отправка писем будет блокироваться на стороне сервера Gmail.
Bot.py
- Импортируем библиотеки:
# -*- 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)
- Создаем обработку команд:
@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)
Первый IF обрабатывает запрос /start, второй IF обрабатывает запрос /donation.
В параметре invoice_payload задаем название, по которому будем определяться подтверждение оплаты.
В bot.send_invoice нужно указать token используемого провайдера платежей. Как его получить, написано здесь.
Для разработки советую использовать тестовый token провайдера. При его использовании функционал сохраняется, но деньги не снимаются. В дальнейшем Вам потребуется только заменить на реальный token.
Тестовый: 123:TEST:XXXX
Реальный: 123:LIVE:XXXX
- Данный декоратор необходим для подтверждения наличия заказа:
@bot.pre_checkout_query_handler(func=lambda query: True)
def checkout(message):
bot.answer_pre_checkout_query(message.id, ok=True,
error_message="Инопланетяне пытались украсть CVV вашей карты, "
"но мы успешно защитили ваши учетные данные. "
"Попробуй расплатиться через несколько минут, "
"нам нужен небольшой отдых.")
Т.к. у нас digital услуга, она всегда в наличии по этому, параметр ok всегда True. Без данного подтверждения оплата не будет проходить у клиента.
Данный декоратор полезен если у Вас, например, магазин обуви. Клиент заходит оформить заказ, нажимает оплатить и в этот момент Вам на сервер приходит запрос, который обрабатывается этой функцией. Если на складе имеется данный товар, то параметр 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)
- Функция отправляет счет на оплату:
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)
Параметр need_email необходим, так как нужно запросить у клиента почту, на которую будем отправлять БД.
- Функция запрашивает название города, по которому будет производиться поиск:
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 предпринимателя, начать генерировать новые идеи где можно было бы применить данный функционал.
Что заработал, то и получил: ударник — хлеб, а лодырь — ничего.