Сидя на freelance видел много раз задачи по сбору БД. Чаще всего просят собрать информацию о компаниях или специфические запросы на Google, Yandex картах.

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

В данной статье предлагаю разработать Telegram bot, который будет принимать название города (в котором будет производиться поиск) и запрос (по которому будет производиться поиск.

Например: Бар, Кофе, Ресторан и т.д.). Реализуем возможность делать donation, оплачивать услугу по сбору БД и отправлять клиентам на почту БД.

Используемые технологии:

  1. Telegram API;
  2. Yandex API;
  3. 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 от Сбербанк процесс выставление счет зависал, а при использовании Яндекс Кассы все хорошо. Не знаю с чем это связанно, но факт.

image
image

Заключение


Данный пост предназначен не только для того, чтобы показать возможности реализации mini-digital-business, но и для того чтобы расшевелить в тебе it предпринимателя, начать генерировать новые идеи где можно было бы применить данный функционал.
Что заработал, то и получил: ударник — хлеб, а лодырь — ничего.