Привет! Ранее я рассказал, как я создал Телеграм-бота, который пишет хокку и подбирает пикчу в тему. В этой статье расскажу, как я научился фильтровать изображения по размеру и про работу с vk_api. Прошлая статья вышла немного скомканной, поэтому в этой статье заново рассмотрим весь принцип работы.

Цели проекта

Строго говоря, бот не пишет хокку. Он всего лишь составляет трёхстишие из уже имеющихся строк, вставляя их рандомно. Таким образом, мы получаем не совсем осмысленное произведение, хотя если посмотреть на переводы оригинальных хокку, то наши отличаются не сильно. Буду рад любым идеям, как нам реализовать правильное строение хокку по слогам (Строение хокку).

Для меня самым важным тут является не сам принцип построения хокку (хотя и хотелось бы улучшить качество самих стихов), а работа с API и requests. Поэтому, этот бот больше нужен для моего обучения, развития и развлечения (все таки бот мемный). Перед тем как начать, хочу напомнить, что исходный код вы можете увидеть на GitHub, а результат работы посмотреть в Телеграм-канале и группе ВК

Как составить "хокку"?

Стихи хокку или хайку это традиционные японские трехстишия, составленные по определённой структуре. Предвидя споры в комментариях (хокку или хайку), оставлю ссылку. Тут вы можете сполна познакомиться с японской поэзией.

Мы же, чтобы хоть немного приблизиться к стилю, будем просто собирать трехстишия, без какого либо закона. Получается сами хокку не будут иметь ни структуры, ни смысла и генерировать мемные записи с мемными авторами. Разве не забавно?

Для этого я заранее собрал базу из традиционных японских хокку и имён авторов, откуда бот будет брать инфу. После всех фильтрации и сбора популярных японских имён (надеюсь это не расизм) мы получаем такие документы:

Количество вариантов

Чтобы посчитать количество вариантов нужно обратиться к комбинаторике. Наш алгоритм каждую итерацию берёт случайную строку. Так возможны варианты, когда у нас все 3 строки повторяются.

Одинокий сверчок.

Одинокий сверчок.

Одинокий сверчок.

- Дао Дао Дао 1111 г. н.э.

Теперь нужно немного напрячься, чтобы вспомнить, что такому варианту соответствуют размещения, в нашем случае ещё и с повторениями.

Получается, для нас это:

6,8 миллиардов различных вариантов. Если бот будет отправлять сообщения раз в 8-16 часов, то в среднем такого количества нам хватит на 9 млн лет. :)

Телеграм канал

Наконец, когда понятно, что и как должно работать, пришло время написать нашего бота. Изначально проект был задуман как Телеграм бот, поэтому начинаем с создания бота в BotFather. Обращаемся к классу TeleBot в библиотеке pyTelegramBotApi и заодно сохраняем имя канала.

bot = telebot.TeleBot(token = BOT_TOKEN)
CHANNEL_NAME = '@hokky_t'

Теперь нам нужно создать сами хокку, которые мы будем отправлять в канал. Для этого мы записываем каждую строку в список, откуда будем брать элементы для хокку. С именами авторов поступаем точно так же.

def hokky_bot():
    f = open('hokky.txt', 'r', encoding='UTF-8')  # Открываем файл с хокку
    all_hokky = f.read().split('\n')  # Записываем каждую строчку в отдульный элемент списка
    f.close()

    f = open('names.txt', 'r', encoding='UTF-8')  # То же самое для файла с именами
    all_names = f.read().split('\n')
    f.close()
    j = 0
    print('Power on!')   
    while j < 10000:
        a = randint(0,1)  # генерируем случайное число для вставки н.э или до н.э.
        if a == 1:
            era ='до н.э.'
        else: 
            era = 'н.э'
        i =0 
        name = [1, 2, 3]
        text = [1, 2, 3]
        while i<=2: 
            name[i] = all_names[randint(1, len(all_names)-1)]  # Формируем списки из 3 строчек хокку и 3-х имён
            text[i] = all_hokky[randint(1, len(all_hokky)-1)] 
            i += 1
        message = (f'{text[0]}\n{text[1]}\n{text[2]}\n\n     - {name[0].title()} {name[1].title()} {name[2].title()}, {randint(0, 2022)} г. {era}')
        j += 1
        search = text[randint(0,2)] 
        print(f'Японская живопись {search}')
        picture(message, search)
        time.sleep(randint(28800, 57600))

Из количества использования функции randint() уже можно понять, насколько качественно наш алгоритм создаёт стихи. Про предпоследние три строки расскажу далее, а пока наш код уже умеет создавать это:

На этом текстовая часть закончена и далее никак не дорабатывалась. Теперь можно подумать о визуальной составляющей. Изначально идея была в добавлении текста на фон какого-нибудь изображения. Но текст на картинке из интернета выглядел совершенно нечитаемым, и создание обводки или тени не помогло.

search = text[randint(0,2)] 
print(f'Японская живопись {search}')
picture(message, search)

После всех вариантов, оптимальным оказался тот, где мы ищем картинки по запросу 'Японская живопись + строка из хокку'

Мы передаём сам текст сообщения и поисковой запрос в функцию picture().

def picture(message, search):
    # Код для вставки своего хокку в изображение из request_photo
    # im = requests.get(request_photo('японcкая живопись', search))  
    # out = open("img.jpg", "wb")
    # out.write(im.content)
    # out.close()
    # image = Image.open('img.jpg')

    # # Создаем объект со шрифтом
    # font = ImageFont.truetype('font.name', size= int(image.width/15))
    # draw_text = ImageDraw.Draw(image)
    # draw_text.text(
    #     (int(image.width/50), int(image.height/4)),
    #     message,
    #     # Добавляем шрифт к изображению
    #     font=font,
    #     fill='#d60000') # Цвет текста
    url = request_photo(f'Японская живопись {search}')  # Функция поиска изображения
    bot.send_photo(CHANNEL_NAME, photo = url, caption = message)  # Отправляем в тг
    vk_post(url=url, message=message)  # Отправляем пост в ВК

Здесь мы вызываем функцию request_photo(). Она возвращает ссылку на случайное изображение из поискового запроса Яндекс.

Функция vk_post() является некоторым спойлером, о котором позже.

def request_photo(message):
    req = requests.get("https://yandex.ru/images/search?text="+message)
    ph_links = list(filter(lambda x: '.jpg' in x, re.findall('''(?<=["'])[^"']+''', req.text)))
    ph_list = []
    for i in range(1, 10):
        if len(ph_links[i]) > 5:
            if ph_links[i][0:4] == "http":
                size = ph_size(ph_links[i])[0]
                print(size)
                if size > 500:
                    ph_list.append(ph_links[i])
                    print(ph_list)

    return ph_list[randint(0, len(ph_list) - 1)]

Тут всё просто. Сначала мы получаем код страницы по запросу в Яндекс картинках. Затем мы составляем список из всех объектов html документа, оканчивающиеся на .jpg, после чего, через цикл фильтруем только те, которые начинаются на http. И свежеотобранные ссылки уже отправляем в функцию ph_links(), которая в свою очередь возвращает размер изображения. Мы получаем ширину картинки, после чего в новый список добавляем только те, ширина которых больше 500 пикселей. Таким образом мы отсеиваем "мыльные" картинки. И в конце функция возвращает рандомную картинку из полученного списка.

Функция ph_size отправляет запрос на url выбранной нами картинки, после чего возвращает параметр p.image.size. Введение такого фильтра очень замедлило работу бота. Также пришлось уменьшить выборку ссылок с поисковой страницы (мы берём только 10) из-за вывода системой капчи.

def ph_size(url):
    resume_header = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:66.0) Gecko/20100101 Firefox/66.0",
    "Accept-Encoding": "*",
    "Connection": "keep-alive", 
    'Range': 'bytes=0-2000000'}  
    data = requests.get(url, stream = True, headers = resume_header).content
    p = ImageFile.Parser()
    p.feed(data)   
    if p.image:
        return p.image.size 
    # (1400, 1536) 
    else: 
        return (0, 0)

Ещё одна проблема, с которой пришлось столкнуться это ошибка получения сертификата:

[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:997)

Как справиться с этой ошибкой, я так и не нашел, но она волшебным образом уходит при включении VPN. Итак, после починки деталей с Телеграм-каналом покончено. Бот отправляет нам хокку с картинкой в тему хокку.

Пример картинки по запросу "Японская живопись ночью прилёг уснуть."
Пример картинки по запросу "Японская живопись ночью прилёг уснуть."

Такой результат меня очень устраивает. Такой Телеграм-канал уже не стыдно показать друзьям и вместе посмеяться. Но как насчёт заставить бота вести одновременно несколько соцсетей. Начнём с ВК.

Группа в ВК

Самым простым способом публиковать записи в сообществе является API VK. На питоне доступна библиотека vk_api для создания api запросов. С отправкой сообщений всё просто, можно обращаться к методу wall.post, указав айди сообщества со знаком минус. Главное, чтобы пользователь был админом, либо же использовать встроенный токен из настроек сообщества. Делается это так:

vk_session = vk_api.VkApi('LOGIN', 'PASSWORD')
vk_session.auth()
vk = vk_session.get_api()
vk.wall.post(message=message, owner_id = '-213199160') 

С отправкой фотографий всё несколько труднее. Нужно сначала загрузить фото в альбом, потом получив photo id указать его в параметре attachments. В документации ВК подробно описано, как пользоваться этим методом. То есть нам для загрузки в альбом нужно сначала сохранить его в локальную папку. В итоге получаем такую функцию:

def vk_post(url, message):
    vk_session = vk_api.VkApi('LOGIN', 'PASSWORD')
    vk_upload = vk_api.upload.VkUpload(vk_session)
    vk_session.auth()  # Входим в аккаунт
    vk = vk_session.get_api()  # Возвращает VkApiMethod(self)

    im = requests.get(url=url)  # Скачиваем изображение
    f = open("img.jpg", "wb")
    f.write(im.content)
    f.close()

    with open ('img.jpg', 'rb') as f:
        ph = vk_upload.photo(photos=f, album_id=284394723)  # Загружаем фото в альбом
        ph_id = ph[0]['id']  # Получаем id фотографии
# Отправляем пост на стену группы
    print(vk.wall.post(message=message, owner_id = '-213199160', attachments= f'photo223988241_{ph_id}', copyright= 'https://t.me/hokky_t'))

Ура! Теперь наш бот отправляет одинаковые записи в Телеграм-канал и группу ВК. Довольно простой на первый взгляд проект дал мне немало новых знаний и умений, ещё и удовольствия получил массу. Есть некоторые идеи для новых фич, как например озвучка текста, или добавление фото не из поиска, а создание их с помощью нейросети ruDalle. В общем, есть куда стремиться.

Публикация в в
Публикация в в

На этом в принципе всё. Жду в комментариях ваши идеи для внедрения в проект, а также замечания.

Ещё раз напомню, вот ссылки на Телеграм канал и группу ВК, а также GitHub. Подпишитесь, мне будет очень приятно!

Всем пока, до новых статей.

Комментарии (3)


  1. iig
    14.05.2022 11:09

    Как то это не на python написано ;)

    f = open('hokky.txt', 'r', encoding='UTF-8')  # Открываем файл с хокку
        all_hokky = f.read().split('\n')  # Записываем каждую строчку в отдульный элемент списка
        f.close()
    all_hokky = open('hokky.txt', 'r', encoding='UTF-8').readlines()

    j = 0  
        while j < 10000:
            j += 1

    Почему не for?

    name = [1, 2, 3]
    while i<=2: 
        name[i] = all_names[randint(1, len(all_names)-1)] 

    Если собираетесть считать до 3 - лучше использовать i<3. Непонятно, зачем массив сначала заполнять, и потом сразу заполнять правильно. Непонятно, почему игнорируется первая строчка.

    name = []
    for i in range(3):
      name.append(случайное значение)

    f'{text[0]}\n{text[1]}\n{text[2]}\n

    join лучше:

    f'{"\n".join(text)}

    И я очень не уверен, что древние японские поэты как-то связывали своё творчество с Р.Х.


    1. mogilyoy Автор
      15.05.2022 07:12

      Спасибо, обязательно исправлю! По поводу формата и Р.Х., в качестве референса был мем с конфуцием.


      1. randomsimplenumber
        15.05.2022 09:47
        -1

        name = [random_value() for i in range (3)]

        Думаю, так будет ещё более по питоновски.