Русский
Русский
English
Статистика
Реклама

Pinterest

Telegram бот на Firebase

26.04.2021 14:21:23 | Автор: admin

В основном, про Firebase рассказывают в контексте создания приложений под IOS или Android. Однако, данный инструмент можно использовать и в других областях разработки, например при создании Telegram ботов. В этой статье хочу рассказать и показать насколько Firebase простой и удобный инструмент (а ещё и бесплатный, при разумных размерах проекта).


Motivation

Ровно посередине апреля я очнулся от работы и вдруг вспомнил, что у меня ещё не написан диплом, а сдавать его через месяц - полтора. У меня была заброшенная научная статья на достаточно скучную тему, делать на её базе диплом мне уж совсем не хотелось. Нужен был глоток свежего воздуха - какой-нибудь новый проект.

В середине февраля я с ребятами из веб студии обсуждал идею создания приложения по подбору квартир с рекомендательной системой, которая анализировала бы изображения интерьеров и подстраивалась под предпочтения пользователя. Так как мой диплом должен быть на тему Computer Vision, то я решил развить эту тему. Да, и было придумало прикольное название - Flinder (Flats Tinder).

Если погружаться в детали, то для создания рекомендательной системы, работающей с изображениями, необходимо построить пайплайн конструирования эмбедингов на базе изображений. Эмбединг - это вектор, содержащий в себе информацию о изображении и являющийся результатом работы нейронной сети.

В частности, меня вдохновила одна научная статья про DeViSE: A Deep Visual-Semantic Embedding Model. Мне было интересно попробовать такие эмбединги.

В чём суть?

Если кратко, то авторы статьи обучили нейронную сеть предсказывать не конкретные классы изображений, по типу "кошка", "собака", а векторные представления названий классов. Это те самые векторные представления, для которых "King - Man + Woman = Queen".

Такой подход, объединяет визуальное представление нейросети об изображении со структурой языка. Это приводит, например, к тому что модель может предсказать класс, которого не было в датасете, но который семантически близок к классу из датасета.

Какого бота я делал?

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

Итак, телеграм бот:

  • Присылает пользователю изображение и просит его оценить

  • Получает оценку от пользователя

  • Сохраняет оценку пользователя в базу данных

  • *киллер фича* - удаляет изображение из диалога, если оно не понравилось пользователю

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

Да, также важно раздобыть контент, который пользователи будут оценивать. Немного заморочившись я скачал сразу 20.000 изображений с интерьерами с Pinterest. Это были и запросы как скандинавский интерьер квартиры так и готический интерьер дома. Старался собрать как можно более разнообразный (репрезентативный) набор изображений.

Изображения добывал с помощью библиотечки pinterest-image-scraper (Там есть баги и она не супер удобная, но мне её хватило).

Firebase

Меня немного смущал момент отправки изображений телеграм ботом. Получившаяся база изображений в 20.000 штук весила примерно 1.5 гигабайта и мучаться с переносом её на сервер мне уж совсем не хотелось.

Тут я подумал о том, что было бы классно выложить все фотографии на какой-нибудь облачный сервис, и дальше в телеграмм боте использовать только ссылки на изображения, а не сами изображения. А ссылки на изображения (или идентификаторы изображений) и оценки пользователей можно хранить в Firebase Realtime Database.

Перед этим я быстро проверил, поддерживает ли библиотека pyTelegramBotAPI отправку изображений ссылками. Метод send_photo спокойно работал с ссылками на изображения, так что я решил дальше использовать этот подход.

Прежде я работал только с Firebase Realtime Database, но про удобство Firebase Storage был наслышан.

Инициализация проекта в Firebase

Итак, чтобы начать работать с Firebase вам необходимо зарегистрироваться на этом сервисе и создать там проект. После чего у вас откроется вкладка Project Overview.

Project OverviewProject Overview

Для того чтобы получить доступ к функциям Firebase из кода необходимо скачать ключи доступа к проекту. Сделать это можно нажав на значок шестерёнки в верхнем левом углу, справа от надписи Project Overview, и выбрать пункт Project Settings. Затем, на открывшемся экране нужно выбрать Service Accounts и нажать Generate new private key.

После чего, выбрав в левой вкладке меню Realtime Database и Storage можно создать соотвествующие базы данных. В проект на питоне подключить эти базы данных можно следующим способом.

import firebase_adminfrom firebase_admin import credentialsfrom firebase_admin import dbfrom firebase_admin import storagecred = credentials.Certificate("/path/to/secret/key.json")default_app = firebase_admin.initialize_app(cred, {  'databaseURL': 'https://realtime-db-name',    'storageBucket' : 'storage-bucket-local-name'})bucket = storage.bucket()

Где правые части внутри выражения initialize_app есть условные ссылки на названия ваших баз данных внутри проекта в Firebase. После инициализации у вас будут доступны две базы данных

  • db - объект Realtime Database. Данные хранятся в виде одного JSON дерева. В случае работы с питоном - это по сути объект dict.

  • bucket - объект Storage, по сути, обёртка над Google Storage, позволяющая по API загружать и скачивать объекты.

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

Firebase Realtime Database

Обожаю эту базу данных и готов петь ей дифирамбы. Она очень удобная, быстрая, надёжная, а главное - никакого SQL! Это JSON based Database. Но хватит похвалы, давайте посмотрим, как с ней работать.

Например, у нас есть несколько пользователей, которые хранятся в users_database.

users_databse = {"1274981264" : {"username" : "user_1","last_activity" : 1619212557},"4254785764" : {"username" : "user_2","last_activity" : 1603212638}}

Добавить их в в Realtime Database мы можем так:

db.reference("/users_databse/").set(users_databse)

Также мы можем добавить и следующего пользователя таким вот образом:

user_3_id = "2148172489"user_3 = {"username" : "user_3","last_activity" : 1603212638}db.reference("/users_database/" + user_3_id).set(user_3)

Этот код добавит user_3 в users_database

Получить данные можно так.

user_3 = db.reference("/users_database/" + user_3_id).get()users_databse = db.reference("/users_databse/").get()

Это вернет объекты формата Python dict

Стоит отметить, что массивы в Realtime database хранятся в следующем виде.

a = ["one", "two", "three"]firebase_a = {"0" : "one","1" : "two","2" : "three"}

То есть также в формате json

И ещё один нюанс, Realtime Database не хранит объекты None и [] То есть код

db.reference("/users_database/" + user_3_id).set(None)

Приведёт к ошибке

А код

db.reference("/users_database/" + user_3_id).set([])

Удалит данные user_3

Также стоит добавить, что если внутри вашего объекта в питоне есть какое-либо поле, значение которого есть None или [], то в объекте, загруженном в Realtime Database этих полей не будет. То есть:

user_4 = {"username" : "user_4","last_activity" : 4570211234,  "interactions" : []}# Но user_4_in_fb = {"username" : "user_4","last_activity" : 4570211234}

На самом деле, методами get() и set() всё не ограничивается. По ссылке вы можете посмотреть документацию по firebase_admin.db

Firebase Storage

Вернёмся к Firebase Storage. Допустим, у нас на локальном диске хранится изображение по пути image_path Следующий код добавит это изображение в Storage.

def add_image_to_storage(image_path):    with open(image_path, "rb") as f:        image_data = f.read()    image_id = str(uuid.uuid4())        blob = bucket.blob(image_id + ".jpg")        blob.upload_from_string(        image_data,        content_type='image/jpg'    )

Где image_id - уникальный идентификатор изображения.

С получением доступа к изображению всё чуточку сложнее. blob имеет формат

blob.__dict__
blob.__dict__ = {'name': 'one.jpg', '_properties': {'kind': 'storage#object',  'id': 'flinder-interiors/one.jpg/1619134548019743',  'selfLink': 'https://www.googleapis.com/storage/v1/b/flinder-interiors/o/one.jpg',  'mediaLink': 'https://storage.googleapis.com/download/storage/v1/b/flinder-interiors/o/one.jpg?generation=1619134548019743&alt=media',  'name': 'one.jpg',  'bucket': 'flinder-interiors',  'generation': '1619134548019743',  'metageneration': '1',  'contentType': 'image/jpg',  'storageClass': 'REGIONAL',  'size': '78626',  'md5Hash': 'OyY/IkYwU3R1PlYxeay5Jg==',  'crc32c': 'VfM6iA==',  'etag': 'CJ+U0JyCk/ACEAE=',  'timeCreated': '2021-04-22T23:35:48.020Z',  'updated': '2021-04-22T23:35:48.020Z',  'timeStorageClassUpdated': '2021-04-22T23:35:48.020Z'}, '_changes': set(), '_chunk_size': None, '_bucket': <Bucket: flinder-interiors>, '_acl': <google.cloud.storage.acl.ObjectACL at 0x7feb294ff410>, '_encryption_key': None}

Где есть selfLink и mediaLink, однако доступ к изображению по этим ссылкам - ограничен и доступен только при наличии определенных прав доступа, которые настраиваются в консоли Firebase.

В своём проекте я постарался сделать всё максимально просто и поэтому воспользовался методом blob.generate_signed_url(...). Этот метод генерирует ссылку, которая имеет определённое время жизни. Время жизни ссылки является параметром метода.

Следующий метод генерирует ссылку, живущую 10 минут.

def get_image_link_from_id(image_id):    blob = bucket.blob(image_id + ".jpg")    time_now = int(time.time() // 1)    ttl = 600    return blob.generate_signed_url(time_now + ttl)

Telegram Bot

Не буду вдаваться в подробности написания телеграм ботов, так как на эту тему статей много (простой туториал, супер подробная статья). Пройдусь только по основным моментам.

Как выглядит бот?

В своём проекте на pyTelegramBotAPI я использовал InlineKeyboardButton, состоящую из эмоджи и callback_query_handler, обрабатывающий нажатия на кнопки.

keyboard = types.InlineKeyboardMarkup(row_width = 3)nott = types.InlineKeyboardButton(text="no_emoji", callback_data='no')bad = types.InlineKeyboardButton(text="bad_emoji", callback_data='bad')yes = types.InlineKeyboardButton(text="yes_emoji", callback_data='yes')keyboard.add(nott, bad, yes)
Небольшой баг хабра

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

Реакции пользователей я храню в Realtime Database. Добавляю их туда следующим образом.

def push_user_reaction(chat_id, image_id, reaction):  db_path = "/users/" + str(chat_id)+ "/interactions/"+ str(image_id)db.reference(db_path).set(reaction)

База данных Firebase Realtime Database имеет следующий вид

users - база данных пользователей.

Для каждого пользователя в разделе interactions мы храним взаимодействия пользователя с изображениями. last_image_id и last_message_id - элементы логики работы телеграмм бота. Что-то типо конечного автомата.

Да, и идентификаторы пользователей в базе данных - это telegram id пользователей (chat_id для библиотеки telebot).

usersusersinteractionsinteractions

interiors_images - база идентификаторов изображений. В ней хранятся метаданные изображений, а также дублируются реакции пользователей.

interiors_imagesinteriors_images

Ну и images_uuids в Realtime Database - это просто массив с уникальными идентификаторами изображений. Своего рода костыль, чтобы было откуда выбирать идентификаторы изображений.

Костыль

Собственно говоря сам костыль.

IMAGES_UUIDS = Nonedef obtain_images_uuids():    global IMAGES_UUIDS    IMAGES_UUIDS = db.reference("/images_uuids/data").get()obtain_images_uuids()def get_random_image_id():    image_id = np.random.choice(IMAGES_UUIDS)    return image_id

Да, на первом этапе, изображения, отправляемые пользователю, выбираются случайным образом.

Firebase Storage выглядит таким вот образом. Можно заметить, что названия изображений в Storage есть просто идентификаторы изображений + их расширение.

Также, огромным плюсом облачных баз данных является то, что можно хранить состояния взаимодействия пользователя с ботом. И если вдруг сервер решит перезапустить код бота, то это состояние сохранится. (Хотя если пользоваться обычной базой данных состояние тоже сохранится, так что это такой, притянутый за уши плюс)

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

Деплой на сервер

Последнее время я стал адептом докера, поэтому и этот свой проект я размещал на сервер с помощью докера. Пройдусь по основным моментам, которые я использовал в проекте.

Dockerfile

FROM python:busterCOPY requirements.txt /tmp/RUN pip install -r /tmp/requirements.txtRUN mkdir /srcWORKDIR /srcCOPY ./src .CMD python3 /src/code/bot.py
requirements.txt
pyTelegramBotAPIfirebase-admingoogle-cloud-storagenumpy

Где src - это место монтирования docker volume, который я создал до этого командой

docker volume create \            --opt type=none \            --opt o=bind \            --opt device=/home/ubuntu/Flinder/src \            --name flinder_volume

После чего собрал образ и запустил контейнер следующим образом, где флаг-v монтирует созданные ранее flinder_volume в директорию src внутри докер контейнера.

docker run -d \--network=host \--name flinder_bot \--restart always \-v "flinder_volume:/src" devoak/flinder:1.0

Ну и полезное замечание, что у команды docker run можно указать прекрасный параметр --restart always, который обеспечит постоянную работу бота на сервере.

До этого я делал через systemctl, что было сложнее и менее удобно.

Заключение и капелька пиара

Flinder - именно так называется мой проект (Flats Tinder)Flinder - именно так называется мой проект (Flats Tinder)

Надеюсь, моя статья была полезна. Хочу привнести в сообщество программистов такую идею, что работать с Firebase - это очень просто, приятно и удобно, а главное - бесплатно, при разумных размерах проекта.

Более того, использование Firebase не ограничивается Телеграм ботами, недавно я сделал целый промышленный парсер инстаграмма на основе Firebase Realtime Database, о чём я тоже планирую написать статью.

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

По поводу других проектов, оставлю свой телеграм канал. Периодически пишу туда про проекты, которыми занимаюсь (а их много, есть даже нейросети для театра). Более того, если вам вдруг нужен сайт или приложение с продвинутым бэкендом, то милости прошу, моя веб студия. Занимаемся проектами любой сложности.

Подробнее..

Перевод Одна строка, которая ускорила клонирование в 100 раз

09.11.2020 16:20:11 | Автор: admin
Наша группа по оптимизации производительности нашла маленькое изменение, которое оказало большое влияние на скорость сборки по всем конвейерам. Мы обнаружили, что установка параметра refspec во время git fetch ускоряет шаг клонирования в 100 раз.

Группа Engineering Productivity отвечает за поддержку инженеров, которые создают и развёртывают программное обеспечение в Pinterest. Наша команда поддерживает ряд инфраструктурных сервисов и часто работает над крупными проектами перенос всего программного обеспечения на Bazel, создание платформы непрерывной доставки под названием Hermez. Они же поддерживают монорепозитории, куда ежедневно присылают по несколько сотен коммитов, и это ещё не все их задачи.

Мы направляем большие усилия на то, чтобы разработка и доставка программного обеспечения в Pinterest происходили быстро и безболезненно. Недавно жизнь ещё раз показала, какое большое влияние может оказать даже самая мелкая деталь. Мы нашли такую маленькую деталь в Git, которая значительно сократила время сборки в наших конвейерах непрерывной интеграции. Чтобы понять, как это маленькое изменение оказало такое большое влияние, нужно поделиться некоторой информацией о наших монорепозиториях и конвейерах.

Монорепозитории и конвейеры


У нас в Pinterest шесть основных репозиториев: Pinboard, Optimus, Cosmos, Magnus, iOS и Android. Всё это монорепозитории с большим наборов сервисов, специфичных для языка. Pinboard самый крупный монорепозиторий, который поддерживается с момента основания компании. В нём более 350тыс. коммитов и размер 20ГБ при полном клонировании.

Клонирование монорепозитория с большим объёмом кода и длительной историей занимает много времени, а в наших конвейерах непрерывной интеграции приходится делать это очень часто в течение дня. Для одного только Pinboard в рабочие дни мы делаем больше 60тыс. git pull. Большинство скриптов конфигурации конвейера Jenkins (написанных на Groovy) начинаются с этапа Checkout, где мы клонируем репозиторий, который на более поздних этапах будет построен и протестирован. Вот как выглядит типичная стадия Checkout:



Если использовать Git CLI напрямую:



```
Даже при неполном/поверхностном (shallow) клонировании, не извлекая никаких тегов и только для последних 50 коммитов, операция всё равно выполнялась не так быстро, как могла бы. Всё потому, что мы не устанавливали параметр refspec. Обратите внимание, что отсутствие этого параметра означает команду на извлечение всех refspec'ов: +refs/heads/*:refs/remotes/origin/*. В случае с Pinboard происходит обработка более 2500 ветвей.

Просто добавив опцию refspec и указав, какие ссылки нас интересуют (в нашем случае только из мастера), можно ограничить область обработки нужной ветвью и сэкономить много времени. Вот как это выглядит в нашем конвейере:



Простое изменение одной строки сократило время клонирования в 100 раз и в результате значительно сократило время сборки. Время клонирования крупнейшего репозитория Pinboard сократилось с 40 минут до 30 секунд. Это показывает, что иногда даже самые маленькие усилия имеют очень большое значение.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru