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

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

Что по БД?

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

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

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

Кстати, подобный функционал у 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 пакетиков диэтиламида лизергиновой кислоты или ЛСД, солонка, наполовину наполненная кокаином, и целое море разноцветных амфетаминов, барбитуратов и транквилизаторов, а так же литр текилы, литр рома, ящик «Бадвайзера», пинта чистого эфира, и 12 пузырьков амилнитрита 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' }

2. Собираем тело запроса, который включает переведенный запрос и параметра timezone (оставим по дефолту US/Eastern, пока это неважно):

# Тело запроса body = { "query": query, "timezone": "US/Eastern" }

3. Отсылаем POST-запрос на сервер Nutritionix, куда включаем URL, заголовки и тело запроса, а его ответ сохраняем в переменную response:

# Выполнение POST-запроса response = requests.post(natural_url, json=body, headers=headers)

4. Проверяем что запрос удался и вернул код 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')

5. Осталось только воспользоваться генератором списка и передать каждый словарь из списка словарей в конструктор класса NutritionixFood. В итоге мы получим список объектов данного класса.

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

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

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

  • Запросить ввод текста с перечислением съеденного
  • Перевести текст на английский
  • Отправить текст через API Nutritionix
  • Получить ответ сервера
  • «Конвертировать» ответ сервера в список объектов класса NutritionixFood
  • Вывести список продуктов пользователю, переведя названия продуктов с английского на русский:

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

Как я говорил выше, кроме основных составляющих пищи, которые в основном нас и интересуют (КБЖУ), данный сервис предоставляет еще кучу других. Некоторые из них, например сахар или калий, хранятся в атрибутах класса в готовом виде (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, каждый из которых содержит информацию о конкретном нутриенте из продукта. А имея информацию обо всех нутриентах, потребленных в течение определенного периода уже можно сделать выводы о его диете (много соли, мало витамина B и т. д.) и дать соответствующие рекомендации, опираясь на вышеописанные нормы FDA или национальные.

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

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

Начать дискуссию