CoderCastrov logo
CoderCastrov
Парсер

Как создать приложение для рекомендации коктейлей с нуля

Как создать приложение для рекомендации коктейлей с нуля
просмотров
10 мин чтение
#Парсер

Парсинг ингредиентов коктейлей, разработка простого алгоритма схожести коктейлей и создание приложения для рекомендации коктейлей с использованием Streamlit

Cocktail

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

  1. Получение данных о коктейлях

  2. Разработка алгоритма схожести коктейлей

  3. Создание пользовательского интерфейса (веб-приложение)

1. Получение данных о коктейлях

Сайт TheCocktailDB.com предлагает бесплатное API, которое можно использовать для получения данных о коктейлях. Мы будем использовать метод API "List all cocktails by first letter". Сначала создадим список URL-адресов:

import pandas as pd
import numpy as np
import string

def get_url_list():

    '''получает URL-адрес для каждой возможной первой буквы названия коктейля - a, b, c... 1, 2 и т. д. и сохраняет их в списке'''

    url_list = []
    main_url = 'https://www.thecocktaildb.com/api/json/v1/1/search.php?f='
    for i in string.printable:
        url_list.append(main_url+i)
    return url_list

Затем мы получаем ингредиенты и инструкции для приготовления коктейлей из этих URL-адресов:

import requests

def scrape_cocktail_list():

  '''парсит список URL-адресов для получения информации о коктейлях'''

    cocktail_list = []
    url_list = get_url_list()
    for i in url_list:
        try:
            r = requests.get(i, verify=False)
            cocktail_list.append(r.json())
        except:
            pass
    return cocktail_list

Ниже показаны первые два элемента возвращенного списка:

cocktail_list[0:2]
[{'drinks': None},
 {'drinks': [{'dateModified': '2016-10-05 12:36:28',
    'idDrink': '15346',
    'strAlcoholic': 'Alcoholic',
    'strCategory': 'Cocktail',
    'strCreativeCommonsConfirmed': 'No',
    'strDrink': '155 Belmont',
    'strDrinkAlternate': None,
    'strDrinkThumb': 'https://www.thecocktaildb.com/images/media/drink/yqvvqs1475667388.jpg',
    'strGlass': 'White wine glass',
    'strIBA': None,
    'strImageAttribution': None,
    'strImageSource': None,
    'strIngredient1': 'Темный ром',
    'strIngredient10': None,
    'strIngredient11': None,
    'strIngredient12': None,
    'strIngredient13': None,
    'strIngredient14': None,
    'strIngredient15': None,
    'strIngredient2': 'Светлый ром',
    'strIngredient3': 'Водка',
    'strIngredient4': 'Апельсиновый сок',
    'strIngredient5': None,
    'strIngredient6': None,
    'strIngredient7': None,
    'strIngredient8': None,
    'strIngredient9': None,
    'strInstructions': 'Смешайте с льдом. Подавайте в бокале для белого вина. Украсьте морковью.',
    'strInstructionsDE': 'Смешайте с льдом. Подавайте в бокале для белого вина. Украсьте морковью.',
    'strInstructionsES': None,
    'strInstructionsFR': None,
    'strInstructionsIT': 'Смешайте с льдом. Подавайте в бокале для белого вина. Украсьте морковью.',
    'strInstructionsZH-HANS': None,
    'strInstructionsZH-HANT': None,
    'strMeasure1': '1 шот ',
    'strMeasure10': None,
    'strMeasure11': None,
    'strMeasure12': None,
    'strMeasure13': None,
    'strMeasure14': None,
    'strMeasure15': None,
    'strMeasure2': '2 шота ',
    'strMeasure3': '1 шот ',
    'strMeasure4': '1 шот ',
    'strMeasure5': None,
    'strMeasure6': None,
    'strMeasure7': None,
    'strMeasure8': None,
    'strMeasure9': None,
    'strTags': None,
    'strVideo': None},
   {'dateModified': '2016-07-18 22:27:04',
    'idDrink': '15395',
    'strAlcoholic': 'Alcoholic',
    'strCategory': 'Shot',
    'strCreativeCommonsConfirmed': 'No',
    'strDrink': '1-900-FUK-MEUP',
    'strDrinkAlternate': None,
    'strDrinkThumb': 'https://www.thecocktaildb.com/images/media/drink/uxywyw1468877224.jpg',
    'strGlass': 'Old-fashioned glass',
    'strIBA': None,
    'strImageAttribution': None,
    'strImageSource': None,
    'strIngredient1': 'Absolut Kurant',
    'strIngredient10': None,
    'strIngredient11': None,
    'strIngredient12': None,
    'strIngredient13': None,
    'strIngredient14': None,
    'strIngredient15': None,
    'strIngredient2': 'Grand Marnier',
    'strIngredient3': 'Chambord raspberry liqueur',
    'strIngredient4': 'Midori melon liqueur',
    'strIngredient5': 'Malibu rum',
    'strIngredient6': 'Amaretto',
    'strIngredient7': 'Cranberry juice',
    'strIngredient8': 'Pineapple juice',
    'strIngredient9': None,
    'strInstructions': 'Встряхните ингредиенты в шейкере с кубиками льда. Процедите в рокс-стакан.',
    'strInstructionsDE': 'Встряхните ингредиенты в шейкере с кубиками льда. Процедите в рокс-стакан.',
    'strInstructionsES': None,
    'strInstructionsFR': None,
    'strInstructionsIT': 'Встряхните ингредиенты в шейкере с кубиками льда. Процедите в рокс-стакан.',
    'strInstructionsZH-HANS': None,
    'strInstructionsZH-HANT': None,
    'strMeasure1': '1/2 унции ',
    'strMeasure10': None,
    'strMeasure11': None,
    'strMeasure12': None,
    'strMeasure13': None,
    'strMeasure14': None,
    'strMeasure15': None,
    'strMeasure2': '1/4 унции ',
    'strMeasure3': '1/4 унции ',
    'strMeasure4': '1/4 унции ',
    'strMeasure5': '1/4 унции ',
    'strMeasure6': '1/4 унции ',
    'strMeasure7': '1/2 унции ',
    'strMeasure8': '1/4 унции ',
    'strMeasure9': None,
    'strTags': None,
    'strVideo': None},
   {'dateModified': '2016-02-03 14:51:57',
    'idDrink': '15423',
    'strAlcoholic': 'Alcoholic',
    'strCategory': 'Beer',
    'strCreativeCommonsConfirmed': 'No',
    'strDrink': '110 in the shade',
    'strDrinkAlternate': None,
    'strDrinkThumb': 'https://www.thecocktaildb.com/images/media/drink/xxyywq1454511117.jpg',
    'strGlass': 'Beer Glass',
    'strIBA': None,
    'strImageAttribution': None,
    'strImageSource': None,
    'strIngredient1': 'Лагер',
    'strIngredient10': None,
    'strIngredient11': None,
    'strIngredient12': None,
    'strIngredient13': None,
    'strIngredient14': None,
    'strIngredient15': None,
    'strIngredient2': 'Текила',
    'strIngredient3': None,
    'strIngredient4': None,
    'strIngredient5': None,
    'strIngredient6': None,
    'strIngredient7': None,
    'strIngredient8': None,
    'strIngredient9': None,
    'strInstructions': 'Бросьте шутера в стакан. Наполните пивом',
    'strInstructionsDE': 'Бросьте шутера в стакан. Наполните пивом',
    'strInstructionsES': None,
    'strInstructionsFR': None,
    'strInstructionsIT': 'Бросьте шутера в стакан. Наполните пивом',
    'strInstructionsZH-HANS': None,
    'strInstructionsZH-HANT': None,
    'strMeasure1': '16 унций ',
    'strMeasure10': None,
    'strMeasure11': None,
    'strMeasure12': None,
    'strMeasure13': None,
    'strMeasure14': None,
    'strMeasure15': None,
    'strMeasure2': '1.5 унции ',
    'strMeasure3': None,
    'strMeasure4': None,
    'strMeasure5': None,
    'strMeasure6': None,
    'strMeasure7': None,
    'strMeasure8': None,
    'strMeasure9': None,
    'strTags': None,
    'strVideo': None},
   {'dateModified': '2016-07-18 22:28:43',
    'idDrink': '14588',
    'strAlcoholic': 'Alcoholic',
    'strCategory': 'Shake',
    'strCreativeCommonsConfirmed': 'No',
    'strDrink': '151 Florida Bushwacker',
    'strDrinkAlternate': None,
    'strDrinkThumb': 'https://www.thecocktaildb.com/images/media/drink/rvwrvv1468877323.jpg',
    'strGlass': 'Beer mug',
    'strIBA': None,
    'strImageAttribution': None,
    'strImageSource': None,
    'strIngredient1': 'Malibu rum',
    'strIngredient10': None,
    'strIngredient11': None,
    'strIngredient12': None,
    'strIngredient13': None,
    'strIngredient14': None,
    'strIngredient15': None,
    'strIngredient2': 'Светлый ром',
    'strIngredient3': '151 proof rum',
    'strIngredient4': 'Темный крем-де-какао',
    'strIngredient5': 'Cointreau',
    'strIngredient6': 'Молоко',
    'strIngredient7': 'Кокосовый ликер',
    'strIngredient8': 'Ванильное мороженое',
    'strIngredient9': None,
    'strInstructions': 'Смешайте все ингредиенты. Взбейте до гладкости. Украсьте шоколадной стружкой, если хотите.',
    'strInstructionsDE': 'Смешайте все ингредиенты. Взбейте до гладкости. Украсьте шоколадной стружкой, если хотите.',
    'strInstructionsES': None,
    'strInstructionsFR': None,
    'strInstructionsIT': 'Смешайте все ингредиенты. Взбейте до гладкости. Украсьте шоколадной стружкой, если хотите.',
    'strInstructionsZH-HANS': None,
    'strInstructionsZH-HANT': None,
    'strMeasure1': '1/2 унции ',
    'strMeasure10': None,
    'strMeasure11': None,
    'strMeasure12': None,
    'strMeasure13': None,
    'strMeasure14': None,
    'strMeasure15': None,
    'strMeasure2': '1/2 унции ',
    'strMeasure3': '1/2 унции Bacardi ',
    'strMeasure4': '1 унция ',
    'strMeasure5': '1 унция ',
    'strMeasure6': '3 унции ',
    'strMeasure7': '1 унция ',
    'strMeasure8': '1 чашка ',
    'strMeasure9': None,
    'strTags': None,
    'strVideo': None}]}]

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

Для алгоритма рекомендации и пользовательского интерфейса нам понадобятся только название коктейля (strDrink), ингредиенты (strIngredient1, strIngredient2 и т. д.) и их количество (strMeasure1, strMeasure2 и т. д.), а также инструкции по приготовлению коктейля на английском языке (strInstructions).

Необходимо выполнить некоторую предварительную обработку данных, чтобы:

  • Объединить ингредиенты для каждого коктейля в одну строку (вместо наличия одного столбца на каждый ингредиент). Это поможет настроить алгоритм схожести.
  • Сопоставить ингредиенты каждого коктейля с их соответствующими количествами, а затем также объединить пары "ингредиент - количество" в одну строку для каждого коктейля. Это потребуется для веб-приложения.
  • Создать конечную структуру данных только с необходимой информацией для каждого коктейля.

Для предварительной обработки данных используется следующий код:

def get_ingredients(cocktail_list, col_name)

  '''объединяет несколько столбцов с именами столбцов col_name1, col_name2 и т. д. в одну строку'''

    ingredient = cocktail_list[col_name + '1']
    i = 2
    ingredient_list = ""
    while ingredient:
        ingredient_list = ingredient + ", " + ingredient_list
        ingredient = cocktail_list[col_name + str(i)]
        i = i+1
    return ingredient_list
# создание столбцов, показывающих пары "ингредиент - количество"

for i in range(len(cocktail_list)):
    try:
        for j in range(len(cocktail_list[i]['drinks'])):
            for k in range(1,16):
                if cocktail_list[i]['drinks'][j]['strIngredient'+str(k)] is not None:
                    try:
                        cocktail_list[i]['drinks'][j]['ingredient_and_quantity'+str(k)] = cocktail_list[i]['drinks'][j]['strIngredient'+str(k)] + " - " + cocktail_list[i]['drinks'][j]['strMeasure'+str(k)]
                    except:
                        cocktail_list[i]['drinks'][j]['ingredient_and_quantity'+str(k)] = cocktail_list[i]['drinks'][j]['strIngredient'+str(k)] 
                else:
                    cocktail_list[i]['drinks'][j]['ingredient_and_quantity'+str(k)] = None
    except:
        pass
def cocktail_data_clean(cocktail_list):

   '''создает список словарей, один для каждого коктейля, с ключами: drink, ingredients, ingredients_and_quantities, instructions'''
   
    cocktails_info = []
    for i in range(len(cocktail_list)):
        try:
            for j in range(len(cocktail_list[i]['drinks'])):
                cocktail = {}
                cocktail['drink'] = cocktail_list[i]['drinks'][j]['strDrink']
                cocktail['ingredients'] = get_ingredients(cocktail_list[i]['drinks'][j],'strIngredient')
                cocktail['ingredients_and_quantities'] = get_ingredients(cocktail_list[i]['drinks'][j],'ingredient_and_quantity')
                cocktail['instructions'] = cocktail_list[i]['drinks'][j]['strInstructions']
                cocktails_info.append(cocktail)
        except:
            pass   #игнорировать коктейли, для которых нет данных
    return cocktails_info

2. Разработка алгоритма схожести коктейлей

После парсинга данных и их приведения к удобному формату, пришло время разработать алгоритм рекомендаций. Простым методом является рекомендация пользователю коктейлей с похожими ингредиентами. Поскольку мы уже создали строку со всеми ингредиентами для каждого коктейля, нам нужно только сравнить эти строки и найти наиболее похожие. Мы можем использовать комбинацию векторизатора tf-idf и косинусного сходства полученных векторов.

На высоком уровне, векторизатор tf-idf используется для присвоения "оценки" каждому слову в документе (в нашем случае, слово = ингредиент и документ = все ингредиенты коктейля) в коллекции документов (полный список ингредиентов всех коктейлей), давая более высокую "оценку" словам, которые часто встречаются внутри этого документа, но наказывая слова, которые часто встречаются в целом, т.е. в коллекции документов. Например, если многие коктейли содержат сахар в качестве ингредиента, векторизатор tf-idf даст сахару низкую "оценку".

После применения векторизатора tf-idf получается вектор чисел для каждого коктейля. Косинусное сходство можно использовать для измерения схожести каждой пары векторов. Чем больше (т.е. ближе к 1) значение косинусного сходства, тем более похожи векторы (их угол ближе к 0). Ниже приведена реализация алгоритма схожести:

from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity

cocktail_df = pd.DataFrame(cocktails_info)

def similar_cocktail(cocktail_df, chosen_cocktail):

    # предварительная обработка строк
    cocktail_df['ingredients'] = cocktail_df['ingredients'].str.lower().str.replace('[^\w\s]','')

    # применение векторизатора tf-idf к ингредиентам
    vectorizer = TfidfVectorizer()
    X = vectorizer.fit_transform(cocktail_df['ingredients'])
    arr = X.toarray()

    # создание таблицы попарного косинусного сходства
    similarity_table = pd.DataFrame(cosine_similarity(arr), columns=cocktail_df['drink'], index=cocktail_df['drink'])
    
    for column in similarity_table.columns:
            similarity_table[column] = np.where(similarity_table[column] >= 1, 0, similarity_table[column])
        
    # выбор коктейля с наибольшим косинусным сходством в качестве наиболее похожего на выбранный пользователем коктейль
    similar_cocktail= similarity_table.idxmax()
    new_cocktail = similar_cocktail[chosen_cocktail]

    return new_cocktail

3. Создание пользовательского интерфейса (веб-приложение)

Последний шаг - создание пользовательского интерфейса, который позволит пользователям получать рекомендации. Мы можем создать веб-приложение с использованием Streamlit, дружественного к пользователю фреймворка для создания веб-приложений на Python. Основные элементы приложения Streamlit:

  • Файл .py для каждой страницы, которую мы хотим показать (здесь называется файл recommend_page.py)
  • Файл app.py для запуска приложения

Нам также понадобится вспомогательный файл .py, который содержит код парсинга и алгоритм схожести (здесь называется scrape_and_model.py)

Собирая код из предыдущих шагов, это файл scrape_and_model.py:

import pandas as pd
import numpy as np
import requests
import string

from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity

def get_url_list():

    ''' получает URL для каждой потенциальной первой буквы названия коктейля - a, b, c.. 1, 2 и т. д. и сохраняет их в списке'''

    url_list = []
    main_url = 'https://www.thecocktaildb.com/api/json/v1/1/search.php?f='

    for i in string.printable:
        url_list.append(main_url+i)

    return url_list


def scrape_cocktail_list():

    '''парсит список URL-адресов для информации о коктейле'''

    cocktail_list = []
    url_list = get_url_list()

    for i in url_list:
        try:
            r = requests.get(i, verify=False)
            cocktail_list.append(r.json())
        except:
            pass
    return cocktail_list


cocktail_list = scrape_cocktail_list()

for i in range(len(cocktail_list)):
    try:
        for j in range(len(cocktail_list[i]['drinks'])):
            for k in range(1,16):
                if cocktail_list[i]['drinks'][j]['strIngredient'+str(k)] is not None:
                    try:
                        cocktail_list[i]['drinks'][j]['ingredient_and_quantity'+str(k)] = cocktail_list[i]['drinks'][j]['strIngredient'+str(k)] + " - " + cocktail_list[i]['drinks'][j]['strMeasure'+str(k)]
                    except:
                        cocktail_list[i]['drinks'][j]['ingredient_and_quantity'+str(k)] = cocktail_list[i]['drinks'][j]['strIngredient'+str(k)] 
                else:
                    cocktail_list[i]['drinks'][j]['ingredient_and_quantity'+str(k)] = None
    except:
        pass



def get_ingredients(cocktail_list, col_name):

  '''объединяет несколько столбцов с именами столбцов col_name1, col_name2 и т. д. в одну строку'''

    ingredient = cocktail_list[col_name + '1']
    i = 2
    ingredient_list = ""

    while ingredient:
        ingredient_list = ingredient + ", " + ingredient_list
        ingredient = cocktail_list[col_name + str(i)]
        i = i+1
    return ingredient_list


def cocktail_data_clean(cocktail_list):

    '''создает список словарей, один для каждого коктейля, с колонками: drink, ingredients, ingredients_and_quantities, instructions'''
    
    cocktails_info = []

    for i in range(len(cocktail_list)):
        try:
            for j in range(len(cocktail_list[i]['drinks'])):
                cocktail = {}
                cocktail['drink'] = cocktail_list[i]['drinks'][j]['strDrink']
                cocktail['ingredients'] = get_ingredients(cocktail_list[i]['drinks'][j],'strIngredient')
                cocktail['ingredients_and_quantities'] = get_ingredients(cocktail_list[i]['drinks'][j],'ingredient_and_quantity')
                cocktail['instructions'] = cocktail_list[i]['drinks'][j]['strInstructions']

                cocktails_info.append(cocktail)
        except:
            pass   #игнорировать коктейли, для которых нет данных

    return cocktails_info


cocktails_info = cocktail_data_clean(cocktail_list)

cocktail_df = pd.DataFrame(cocktails_info)
cocktail_df.drop_duplicates(inplace=True)

def similar_cocktail(cocktail_df, chosen_cocktail):

    # предварительная обработка строки
    cocktail_df['ingredients'] = cocktail_df['ingredients'].str.lower().str.replace('[^\w\s]','')

    # реализация векторизатора tf-idf на ингредиентах
    vectorizer = TfidfVectorizer()
    X = vectorizer.fit_transform(cocktail_df['ingredients'])
    arr = X.toarray()

    # создание таблицы попарной косинусной схожести
    similarity_table = pd.DataFrame(cosine_similarity(arr), columns=cocktail_df['drink'], index=cocktail_df['drink'])
    
    for column in similarity_table.columns:
            similarity_table[column] = np.where(similarity_table[column] >= 1, 0, similarity_table[column])
        
    # выбираем коктейль с наивысшей косинусной схожестью как наиболее похожий на выбранный пользователем коктейль
    similar_cocktail= similarity_table.idxmax()
    new_cocktail = similar_cocktail[chosen_cocktail]

    return new_cocktail

Далее recommend_page.py, где мы создаем приложение Streamlit:

import streamlit as st
from scrape_and_model import similar_cocktail, cocktail_df


def show_recommend_page():
    st.title("Рекомендация коктейлей")

    st.write("""#### Выберите коктейль, который вам нравится, и мы порекомендуем похожий!""")

    cocktail_names = cocktail_df.drink.unique()

    chosen_cocktail = st.selectbox("**Коктейль**:", cocktail_names)
    chosen_cocktail_ingredients_and_quantities = cocktail_df[cocktail_df.drink == chosen_cocktail].ingredients_and_quantities.values

    st.write(f"Коктейль **{chosen_cocktail}** содержит следующие ингредиенты:")
    for ingredient in chosen_cocktail_ingredients_and_quantities[0].rstrip().rstrip(",").split(","):
        st.write(f"- {ingredient}")


    button_clicked = st.button("Рекомендовать коктейль")
    if button_clicked:
        new_cocktail = similar_cocktail(cocktail_df, chosen_cocktail)
        st.subheader(f"Вам может понравиться коктейль: {new_cocktail}")

        new_cocktail_ingredients_and_quantities = cocktail_df[cocktail_df.drink == new_cocktail].ingredients_and_quantities.values
        new_cocktail_instructions = cocktail_df[cocktail_df.drink == new_cocktail].instructions.values

        st.write(f"**Ингредиенты:**")
        for ingredient in new_cocktail_ingredients_and_quantities[0].rstrip().rstrip(",").split(","):
            st.write(f"- {ingredient}")

        st.write(f"**Инструкции**:")
        st.write(new_cocktail_instructions[0])

Наконец, файл app.py довольно прост:

from recommend_page import show_recommend_page

show_recommend_page()

Чтобы запустить приложение локально, нужно ввести:

streamlit run app.py

Вот несколько скриншотов получившегося приложения. Главная страница:

Streamlit app home page (screenshot)

Если мы выберем коктейль из выпадающего меню и нажмем на "Рекомендовать коктейль", мы получим похожий коктейль:

Dropdown with cocktail options (screenshot) Recommended cocktail and instructions (screenshot)

Вот и все! Ваше приложение для рекомендации коктейлей готово.

Спасибо за чтение моей статьи! Я надеюсь, вам понравился этот учебник. Вы можете получить полный код здесь: https://github.com/joakark/cocktail_recommendation