Всем привет! В данной статье я поделюсь своей реализацией бота для telegram, который может переводить статьи из интернета в mp3-файлы. Для этого я буду использовать python 3.6 и соответствующие библиотеки. Итак, приступим.

Для начала надо зарегистрировать своего бота в telegram. На хабре есть статья, в которой это подробно описано. Далее надо установить pip, на всякий случай прикрепляю ссылку на его установку. После установки менеджера пакетов устанавливаем библиотеки, которые помогут осуществить задуманное, выполняем в терминале:

pip install pyttsx3
pip install langdetect
pip install pydub
pip install bs4
pip install telebot
pip install PyTelegramBotAPI

Теперь надо создать три основных файла:

bot.py

parser.py

voice.py

В них будет реализована логика работы приложения. bot.py отвечает непосредственно за работу бота, основные функции всех модулей вызываются в нем. В parser.py реализован парсинг, того что отправлено боту, в voice.py реализован функционал перевода данных после парсинга в аудиоформат. Смысл таков, боту приходит ссылка на статью из интернета, если она удовлетворяет условию(url-адрес ведет на контент, который написан с использованием тега <article>, а не содержащий просто набор текста), текст статьи парсится с помощью bs4 и средствами библиотеки pyttsx3 записывается в аудиофайл, который в дальнейшем конвертируется в mp3 и отправляется пользователю обратно сообщением. Все просто. Понеслась...

bot.py

Я использую декоратор message_handler, чтобы отвечать юзеру на вызов команды /start

import telebot
from voice import get_mp3_file, get_file_name
from parser import get_article_text, get_article_language, get_link


bot = telebot.TeleBot('TOKEN')

@bot.message_handler(commands=['start'])
def forward_message(message):
    bot.send_message(message.from_user.id, "Привет, я перевожу статьи в аудиофайлы,"
                                           "пришли мне ссылку на статью, "
                                           "а я сброшу тебе mp3 файл.")

и на сообщения

is_running = False

@bot.message_handler(content_types=['text'])
def forward_message(message):
    global is_running  # Глобальная переменная для проверки в работе бот или нет
    if not is_running:
        link = get_link(message.text)
        if link:  # Проверка, что боту отправили ссылку, а не что-то другое
            is_running = True
            article_text = get_article_text(link)
            article_language = get_article_language(article_text)
            if article_language:  # Проверка, что статья была распарсена и ее язык получен         
                bot.send_message(message.from_user.id, "Да, эта статья подходит.")
                bot.send_message(message.from_user.id, f"Язык статьи - {article_language[0]}.")
                bot.send_message(message.from_user.id, "Отправляю аудиофайл...")
                file_name = get_file_name(link)
                get_mp3_file(file_name, article_text, article_language[1])
                bot.send_audio(message.from_user.id, audio=open(file_name, 'rb'))
            else:
                bot.send_message(message.from_user.id, "Данная статья не подходит,"
                                                       "попробуй прислать другую...")
            is_running = False
        else:
            bot.send_message(message.from_user.id, "Нет, это не ссылка, попробуй " 
                                                   "прислать ссылку на статью.")
    else:
        bot.send_message(message.from_user.id, "Я занят предыдущим запросом, " 
                                               "подожди немного...")

Глобальная переменная is_running и ее проверка нужна для того, чтобы не вызывать повторно основные функции бота до тех пор, пока они не закончат работу над предыдущим запросом. То есть, боту необходимо отправить аудиофайл или же дать ответ юзеру, что, что-то пошло не так и только потом принимать новый запрос на обработку. В переменной link я проверяю, что боту пришел действительно url-адрес, а в article_language, что статья была удачно распарсена и ее язык получен.

if link:  # Проверка, что боту отправлена ссылка, а не что-то другое
            is_running = True
            article_text = get_article_text(link)
            article_language = get_article_language(article_text)       

parser.py

import requests
from bs4 import BeautifulSoup
from langdetect import detect
import re


def get_link(message_text):
  	# Ищем ссылку в отправленном боту сообщении
    link_arr = re.findall(r'^https?:\/\/?[\w-]{1,32}'
                          r'\.[\w-]{1,32}[^\s@]*$', message_text)
    if len(link_arr) > 0:
        link = link_arr[0]
        return link
    return False

Чтобы проверить, что пользователь действительно отправил ссылку, я использую регулярное выражение в функции get_link. Если функция отработала и вернула url статьи, я иду дальше и получаю её текст, если вернулся False, бот посылает посылает сообщение: "Нет, это не ссылка, попробуй прислать ссылку на статью."

def get_article_text(link):
    try:
        # Получаем ответ от сервера, на который ведет ссылка
        response = requests.get(link)
    except requests.exceptions.ConnectionError:
        return False
    # Инициализируем парсер  
    parser = BeautifulSoup(response.content, 'html.parser')
    try:
      	# Парсим статью по тегу <article>
        article_text = parser.select_one('article').get_text(separator='. ')
    except AttributeError:
        return False
    return article_text

Здесь я проверяю код ответа, использую для этого соответствующее исключение, если боту был отправлен несуществующий url. При вызове requests.exceptions.ConnectionError возвращается False, который в дальнейшем не пройдет проверку в get_article_language.

Парсинг статьи осуществляется библиотекой BeautifulSoup4 по тегу <article>, с получением всего входящего в тег текста, с разделением через точку с пробелом. Это нужно для того, чтобы разделить слитные части текста, которые могут образоваться при парсинге, в итоге аудиофайл будет прочитан программой с более верной интонацией. get_article_text возвращает строку с содержимым статьи, либо False.

def get_article_language(article_text):
    try:
        language = detect(article_text)  # Определяем язык статьи
    except TypeError:
        return False
    if language == 'en':
        return ['EN', ['en_GB']]
    if language == 'ru':
        return ['RU', ['ru_RU']]
    return False

Язык статьи я определяю, используя библиотеку langdetect. Я выбрал в своей реализации всего лишь 2 языка: русский и английский, хотя можно было бы и больше. В функции возвращается либо массив, 0-ой элемент которого идет в отправку сообщения юзеру об языке статьи, а 1-й элемент используется в pyttsx3 для выбора языка чтеца, либо False, тогда бот посылает сообщение: "Данная статья не подходит, попробуй прислать другую…".

voice.py

import pyttsx3
from pydub import AudioSegment
import re


def engine_settings(engine, article_language):
    voices = engine.getProperty('voices')
    engine.setProperty('rate', 185)  # Выставляем скорость чтения голоса
    for voice in voices:
        if voice.languages == article_language and                 voice.gender == 'VoiceGenderMale':
            return engine.setProperty('voice', voice.id)  # Выбираем подходящий голос


def get_mp3_file(file_name, article_text, article_language):
    engine = pyttsx3.init()
    engine_settings(engine, article_language)  # Применение настроек голоса
    engine.save_to_file(article_text, file_name)  # Сохранение текста статьи в аудиофайл
    engine.runAndWait()
    convert_file_to_mp3(file_name)  # Конвертация в mp3 формат


def convert_file_to_mp3(file_name):
    converter = AudioSegment
    converter_file = converter.from_file(file_name)
    converter_file.export(file_name, format="mp3")


def get_file_name(link):
    # Название файла - ссылка на статью
    file_name = re.split(r'^https?:\/\/?', link)[1]
    for symbols_in_file_name in ['/', '.', '-']:
      # Замена символов в названии файла на '_', чтобы сохранить файл в OS
        file_name = file_name.replace(symbols_in_file_name, '_')
    file_name = file_name+'.mp3'  # Сохраняем файл изначально в mp3 формате
    return file_name

Для создания аудиофайла я использую библиотеку pyttsx3 как указано было ранее, а для конвертации в mp3 библиотеку pydub. Сначала я задаю настройки в engine_settings, выставляю скорость чтения, в зависимости от языка статьи использую русско-говорящего или англо-говорящего чтеца мужского пола и выбираю его голос.

В get_mp3_file я применяю настройки из engine_settings, сохраняю полученный текст статьи после парсинга в аудиофайл(в get_file_name указываю mp3, но по факту pyttsx3 дает другой формат и в обычных проигрывателях, полученный файл не воспроизводится, поэтому в дальнейшем его надо конвертировать с помощью AudioSegment). После выполнения engine.runAndWait и дальнейшей конвертации в mp3, файл сохранится в текущей директории проекта.

Теперь бот может отправить аудиофайл юзеру. Прикрепляю видео работы бота.

Запускаем

python bot.py

Конечно, у меня получился неидеальный парсинг, причем завязанный на тег <article> и голос в pyttsx3 в некоторых моментах может быть неслушабельным, но задумка по моему мнению неплохая и к тому же работающая. Спасибо за внимание.