Ссылка на 1 часть, где мы говорили о тренировках:

https://habr.com/ru/articles/750656/

Теперь настало время поговорить о второй составляющей чат-бота - дневник питания (он же калькулятор калорий).

Что по БД?

В отличие от БД с физическими упражнениями здесь есть из чего выбрать. Существует куча баз продуктов питания с доступом по API, к примеру:

Остановиться было решено на... Nutritionix, так как она обладает одной интересной фишкой - распознавание всех продуктов из одного запроса. То есть, мы можем просто послать на сервис строку вида "3 вареных яйца и банка пива", а сервис выудит все перечисленные продукты их количество/вес/объем и отправит в ответе информацию по каждой позиции. Например:

При этом если мы не укажем конкретный вес/объем продукта, то сервис просто возьмет стандартное значение: 1 вареное яйцо - 50 грамм или 1 кусочек хлеба - 29 грамм. Кому интересно - можете затестить данный функционал сервиса по ссылке ниже:

https://www.nutritionix.com/natural-demo

Кстати, подобный функционал у Nutritionix есть и для тренировок - ввел объем выполненного упражнения (например, пробежал 30 минут) и получил количество потраченных калорий, но, сейчас не об этом.

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

Но, как вы заметили, сервис принимает запросы только на английском языке. Как это нас остановит? Никак. Что же тогда делать? Переводить...

Трудности (нет) перевода

Перевести простейшее предложение вида "2 помидора, ложка оливкового масла и зубчик чеснока" не будет сложной задачей для любого популярного переводчика, поэтому давайте воспользуемся функционалом одного из них. Для этого давайте выберем какой-нибудь онлайн-переводчик и Python-библиотеку под него, например googletrans для Google Переводчика.

Шаг 1. Устанавливаем библиотеку

pip install googletrans

Шаг 2. Переводим

from googletrans import Translator

def translate_from_rus_to_eng(text):
    translator = Translator()
    translated = translator.translate(text, src='ru', dest='en')
    return translated.text

Ключи к успеху

Итак, пользователь что-то ввел, мы это перевели, а теперь настало время отправить запрос к Nutritionix. Однако, для начала нам нужно получить парочку ключей для взаимодействия с сервисом. Для этого переходим по данной ссылке, регистрируемся и копируем Application ID и Application Key.

Код крутится - бот мутится

У нас есть д̶в̶а̶ ̶п̶а̶к̶е̶т̶и̶к̶а̶ ̶т̶р̶а̶в̶ы̶,̶ ̶с̶е̶м̶ь̶д̶е̶с̶я̶т̶ ̶п̶я̶т̶ь̶ ̶а̶м̶п̶у̶л̶ ̶м̶е̶с̶к̶а̶л̶и̶н̶а̶,̶ ̶5̶ ̶п̶а̶к̶е̶т̶и̶к̶о̶в̶ ̶д̶и̶э̶т̶и̶л̶а̶м̶и̶д̶а̶ ̶л̶и̶з̶е̶р̶г̶и̶н̶о̶в̶о̶й̶ ̶к̶и̶с̶л̶о̶т̶ы̶ ̶и̶л̶и̶ ̶Л̶С̶Д̶,̶ ̶с̶о̶л̶о̶н̶к̶а̶,̶ ̶н̶а̶п̶о̶л̶о̶в̶и̶н̶у̶ ̶н̶а̶п̶о̶л̶н̶е̶н̶н̶а̶я̶ ̶к̶о̶к̶а̶и̶н̶о̶м̶,̶ ̶и̶ ̶ц̶е̶л̶о̶е̶ ̶м̶о̶р̶е̶ ̶р̶а̶з̶н̶о̶ц̶в̶е̶т̶н̶ы̶х̶ ̶а̶м̶ф̶е̶т̶а̶м̶и̶н̶о̶в̶,̶ ̶б̶а̶р̶б̶и̶т̶у̶р̶а̶т̶о̶в̶ ̶и̶ ̶т̶р̶а̶н̶к̶в̶и̶л̶и̶з̶а̶т̶о̶р̶о̶в̶,̶ ̶а̶ ̶т̶а̶к̶ ̶ж̶е̶ ̶л̶и̶т̶р̶ ̶т̶е̶к̶и̶л̶ы̶,̶ ̶л̶и̶т̶р̶ ̶р̶о̶м̶а̶,̶ ̶я̶щ̶и̶к̶ ̶«̶Б̶а̶д̶в̶а̶й̶з̶е̶р̶а̶»̶,̶ ̶п̶и̶н̶т̶а̶ ̶ч̶и̶с̶т̶о̶г̶о̶ ̶э̶ф̶и̶р̶а̶,̶ ̶и̶ ̶1̶2̶ ̶п̶у̶з̶ы̶р̶ь̶к̶о̶в̶ ̶а̶м̶и̶л̶н̶и̶т̶р̶и̶т̶а̶ 2 ключа для API и переведенный запрос, так что - давайте кодить.

Все запросы (POST) будем посылать на следующий URL, сохранив его в переменную:

natural_url = https://trackapi.nutritionix.com/v2/natural/nutrients

  1. В начале подключаем библиотеку requests и собираем заголовки из Content-Type, Application ID и Application Key:

import requests

# Заголовки
headers = {
    "Content-Type": "application/json",
    "x-app-id": '672c6c14',
    "x-app-key": '6f4ba779b23cefe6adf152de7860fc87'
}
  1. Собираем тело запроса, который включает переведенный запрос и параметра timezone (оставим по дефолту US/Eastern, пока это неважно):

# Тело запроса
    body = {
        "query": query,
        "timezone": "US/Eastern"
    }
  1. Отсылаем POST-запрос на сервер Nutritionix, куда включаем URL, заголовки и тело запроса, а его ответ сохраняем в переменную response:

# Выполнение POST-запроса
  response = requests.post(natural_url, json=body, headers=headers)
  1. Проверяем что запрос удался и вернул код 200 (OK), переводим его в JSON и получаем значение по ключу 'foods', где как раз и лежит список словарей с информацией по каждому продукту:

if response.status_code == 200:
  data = response.json()
  foods = data["foods"]

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

class NutritionixFood:
    def __init__(self, food:dict) -> None:
        self.food_name = food.get('food_name')
        self.brand_name = food.get('brand_name')
        self.serving_qty = food.get('serving_qty')
        self.serving_weight_grams = food.get('serving_weight_grams')
        self.nf_calories = food.get('nf_calories')
        self.nf_total_fat = food.get('nf_total_fat')
        self.nf_saturated_fat = food.get('nf_saturated_fat')
        self.nf_cholesterol = food.get('nf_cholesterol')
        self.nf_total_carbohydrate = food.get('nf_total_carbohydrate')
        self.nf_dietary_fiber = food.get('nf_dietary_fiber')
        self.nf_sugars = food.get('nf_sugars')
        self.nf_protein = food.get('nf_protein')
        self.nf_potassium = food.get('nf_potassium')
        self.nf_p = food.get('nf_p')
        self.full_nutrients = food.get('full_nutrients')
        self.photo_url = food.get('photo', {}).get('highres')
        self.barcode = food.get('upc')
  1. Осталось только воспользоваться генератором списка и передать каждый словарь из списка словарей в конструктор класса NutritionixFood. В итоге мы получим список объектов данного класса.

result = [NutritionixFood(food) for food in foods]

Готово! Весь код можете просмотреть на гитхаб:

https://github.com/Molot999/Nutritionix

Теперь осталось только объединить это с библиотекой telebot:

  1. Запросить ввод текста с перечислением съеденного

  2. Перевести текст на английский

  3. Отправить текст через API Nutritionix

  4. Получить ответ сервера

  5. "Конвертировать" ответ сервера в список объектов класса NutritionixFood

  6. Вывести список продуктов пользователю, переведя названия продуктов с английского на русский:

Дьявол кроется в деталях

Как я говорил выше, кроме основных составляющих пищи, которые в основном нас и интересуют (КБЖУ), данный сервис предоставляет еще кучу других. Некоторые из них, например сахар или калий, хранятся в атрибутах класса в готовом виде (nf_sugars и nf_potassium соответственно), но основная часть содержится в атрибуте full_nutrients со списком словарей, каждый из которых имеет следующие ключи:

  • ID нутриента

  • Его количество

Например, для запроса "3 boiled eggs" мы получим следующее:

[
    {"attr_id": 203, "value": 18.87},
    {"attr_id": 204, "value": 15.915},
    {"attr_id": 205, "value": 1.68},
    {"attr_id": 207, "value": 1.62},
    {"attr_id": 208, "value": 232.5},
    {"attr_id": 221, "value": 0},
    {"attr_id": 255, "value": 111.93},
    {"attr_id": 262, "value": 0},
    {"attr_id": 263, "value": 0},
    {"attr_id": 268, "value": 973.5},
    {"attr_id": 269, "value": 1.68},
    {"attr_id": 291, "value": 0},
    {"attr_id": 301, "value": 75},
    {"attr_id": 303, "value": 1.785},
    {"attr_id": 304, "value": 15},
    {"attr_id": 305, "value": 258},
    {"attr_id": 306, "value": 189},
    {"attr_id": 307, "value": 186},
    {"attr_id": 309, "value": 1.575},
    {"attr_id": 312, "value": 0.0195},
    {"attr_id": 313, "value": 7.2},
    {"attr_id": 315, "value": 0.039},
    {"attr_id": 317, "value": 46.2},
    {"attr_id": 318, "value": 780},
    {"attr_id": 319, "value": 222},
    {"attr_id": 320, "value": 223.5},
    {"attr_id": 321, "value": 16.5},
    {"attr_id": 322, "value": 0},
    {"attr_id": 323, "value": 1.545},
    {"attr_id": 324, "value": 130.5},
    {"attr_id": 326, "value": 3.3},
    {"attr_id": 328, "value": 3.3},
    {"attr_id": 334, "value": 15},
    {"attr_id": 337, "value": 0},
    {"attr_id": 338, "value": 529.5},
    {"attr_id": 401, "value": 0},
    {"attr_id": 404, "value": 0.099},
    {"attr_id": 405, "value": 0.7695},
    {"attr_id": 406, "value": 0.096},
    {"attr_id": 410, "value": 2.097},
    {"attr_id": 415, "value": 0.1815},
    {"attr_id": 417, "value": 66},
    {"attr_id": 418, "value": 1.665},
    {"attr_id": 421, "value": 440.7},
    {"attr_id": 430, "value": 0.45},
    {"attr_id": 431, "value": 0},
    {"attr_id": 432, "value": 66},
    {"attr_id": 435, "value": 66},
    {"attr_id": 454, "value": 0.9},
    {"attr_id": 501, "value": 0.2295},
    {"attr_id": 502, "value": 0.906},
    {"attr_id": 503, "value": 1.029},
    {"attr_id": 504, "value": 1.6125},
    {"attr_id": 505, "value": 1.356},
    {"attr_id": 506, "value": 0.588},
    {"attr_id": 507, "value": 0.438},
    {"attr_id": 508, "value": 1.002},
    {"attr_id": 509, "value": 0.7695},
    {"attr_id": 510, "value": 1.1505},
    {"attr_id": 511, "value": 1.1325},
    {"attr_id": 512, "value": 0.447},
    {"attr_id": 513, "value": 1.05},
    {"attr_id": 514, "value": 1.896},
    {"attr_id": 515, "value": 2.466},
    {"attr_id": 516, "value": 0.6345},
    {"attr_id": 517, "value": 0.7515},
    {"attr_id": 518, "value": 1.404},
    {"attr_id": 601, "value": 559.5},
    {"attr_id": 606, "value": 4.9005},
    {"attr_id": 607, "value": 0},
    {"attr_id": 608, "value": 0},
    {"attr_id": 609, "value": 0.0045},
    {"attr_id": 610, "value": 0.0045},
    {"attr_id": 611, "value": 0.0045},
    {"attr_id": 612, "value": 0.0525},
    {"attr_id": 613, "value": 3.5235},
    {"attr_id": 614, "value": 1.242},
    {"attr_id": 617, "value": 5.5875},
    {"attr_id": 618, "value": 1.782},
    {"attr_id": 619, "value": 0.0525},
    {"attr_id": 620, "value": 0.2235},
    {"attr_id": 621, "value": 0.057},
    {"attr_id": 626, "value": 0.465},
    {"attr_id": 627, "value": 0},
    {"attr_id": 628, "value": 0.045},
    {"attr_id": 629, "value": 0.0075},
    {"attr_id": 630, "value": 0.0045},
    {"attr_id": 631, "value": 0},
    {"attr_id": 645, "value": 6.1155},
    {"attr_id": 646, "value": 2.121},
]

Чтобы сопоставить attr_id с реальным "веществом" необходимо обратиться к справочной таблице:

Здесь нас интересуют лишь самые основные колонки: A (ID), D (Название) и E (единица измерения). Для простоты взаимодействия можно скопировать данную таблицу в Excel, а уже из него спарсить все это дело в таблицу БД. На всякий случай оставил тег USDA (может когда-то пригодится) и добавил колонку ru_name, в которую потом можно будет "запихнуть" русское название нутриента, прогнав колонку name, к примеру, через тот же самый googletrans.

Кстати, в этой же справочной таблице на 2 листе есть ссылка на документ от FDA (Агентство Министерства здравоохранения и социальных служб США), где прописаны нормы потребления нутриентов. Здесь в основном нас интересует колонка 3 (взрослые и дети >= 4 лет),ну а кого-то 6 (беременные и кормящие женщины).

Вернемся к коду и создадим класс, представляющий каждый нутриент:

class NutritionixNutrient:
    def __init__(self, usda_tag, name, unit, ru_name) -> None:
        self.usda_tag = usda_tag
        self.name = name
        self.ru_name = ru_name
        self.unit = unit
        self.value = None

И начинаем проходиться по тому самому списку словарей

[
    {"attr_id": 203, "value": 18.87},
    {"attr_id": 204, "value": 15.915},
    {"attr_id": 205, "value": 1.68},
    {"attr_id": 207, "value": 1.62},
    {"attr_id": 208, "value": 232.5},
  ...
]

Подобным образом:

# Получаем список словарей из объекта NutritionixFood
full_nutrients = food.get('full_nutrients')
# Создаем пустой список для объектов NutritionixNutrient
food_nutrients = []
# Проходимся по каждому словарю из списка словарей
for nutrient in full_nutrients:
  # Получаем из БД инфо о нутриенте по attr_id и пихаем ее в конструктор класса NutritionixNutrient
  food_nutrient = NutritionixNutrient(db.get_nutritionix_nutrient_info(nutrient_info.get('attr_id')))
  # Отдельно устанавливаем количество нутриента
  food_nutrient.value = nutrient_info.get('value')
  # Добавляем объект класса NutritionixNutrient в список
  food_nutrients.append(food_nutrient)
  
# А тут проводим какие-либо манипуляции с food_nutrients
Краткая схема формирования объекта класса NutritionixNutrient
Краткая схема формирования объекта класса NutritionixNutrient

В итоге получим список объектов класса NutritionixNutrient, каждый из которых содержит информацию о конкретном нутриенте из продукта. А имея информацию обо всех нутриентах, потребленных в течение определенного периода уже можно сделать выводы о его диете (много соли, мало витамина B и т.д.) и дать соответствующие рекомендации, опираясь на вышеописанные нормы FDA или национальные.

Затестить функционал по добавлению продуктов питания можете в данном чат-боте совершенно бесплатно.

Кстати, а почему бы не облегчить жизнь пользователю и дать ему возможность просто записать голосовое сообщение со всем съеденным? Поговорим об это в части №3...

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


  1. 3gada
    29.07.2023 11:40
    +2

    • при заполнении данных профиля, на этапах выбора образа жизни и цели отсутствуют заголовки сообщений, поясняющие это;

    • заполнил данные профиля, в том числе и указал дату рождения, на основе которой бот посчитал мой возраст, однако в калькуляторе бот снова спрашивает мой возраст.


    1. Molot999 Автор
      29.07.2023 11:40

      Окей, будем править