Гид по технологиям

Recipe Finder на Python: Tkinter, Edamam и Pillow

7 min read Программирование Обновлено 27 Apr 2026
Recipe Finder на Python: Tkinter + Edamam
Recipe Finder на Python: Tkinter + Edamam

Женщина готовит блюдо на кухне

Зачем делать собственный поиск рецептов

Интернет полон рецептов, но результаты часто разрозненные, содержат рекламу и нерелевантные страницы. Собственное приложение позволяет:

  • Предоставлять таргетированный, удобный интерфейс без лишних элементов.
  • Контролировать фильтры (диетические предпочтения, время готовки, кухня).
  • Практиковаться с HTTP, API-ключами, обработкой изображений и GUI.

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

Что вы узнаете

  • Как настроить окружение и установить библиотеки.
  • Как получить ключи Edamam Recipe Search API.
  • Как реализовать функцию get_top_5_recipes с обработкой ошибок и кешированием изображений.
  • Как не блокировать GUI (потоки/асинхронность) и как улучшить UX.
  • Практические чек-листы, критерии приёмки, тесты и советы по безопасности и приватности.

Установка зависимостей

Необходимые пакеты: Tkinter, requests, Pillow (PIL). Модуль webbrowser входит в стандартную библиотеку Python.

Примечание: в некоторых системах Tkinter уже включён. В средах типа Ubuntu/Debian может потребоваться системный пакет (например, python3-tk).

Установка через pip (локально для вашего окружения):

pip install requests Pillow

Если Tkinter отсутствует, установите через менеджер пакетов системы:

  • Ubuntu/Debian: sudo apt install python3-tk
  • macOS (Homebrew, редко требуется): brew install python-tk

Важно: не вставляйте ключи API в общий репозиторий — храните их в .env или в переменных окружения.

Получение ключа Edamam Recipe Search API

Чтобы получить app_id и app_key:

  1. Перейдите на Edamam и нажмите кнопку Signup API. Выберите план “Recipe Search API - Developer” и завершите регистрацию.

Страница регистрации Edamam API

  1. Войдите в аккаунт, откройте раздел Accounts и нажмите Go to Dashboard.

Кнопка

  1. В разделе Applications найдите Recipe Search API и нажмите View. Скопируйте Application ID и Application Keys — они потребуются в приложении.

Вкладка приложений в панели Edamam

Important: храните ключи секретно (см. раздел Безопасность и приватность).

Архитектура решения — кратко

  • GUI: Tkinter (ввод запроса, кнопка поиска, canvas с элементами рецептов и вертикальной прокруткой).
  • API: Edamam Recipe Search (search endpoint). Параметры q, app_id, app_key, from, to.
  • Работа с изображениями: requests (stream=True) + Pillow для открытия и изменения размера.
  • UX: не блокировать основной поток — выполнять HTTP-запросы и обработку изображений в фоновом потоке.
  • Оптимизации: кеширование изображений, таймауты, обработка ошибок, graceful fallback, ограничения по частоте запросов.

Компоновка функции get_top_5_recipes — пошагово

Приведём полную, расширенную и устойчивую реализацию ключевых частей приложения. Включены обработка ошибок, таймауты, использование requests.Session, кеш изображений и запуск в отдельном потоке, чтобы GUI не «замораживался».

Примерный файл app.py (сокращённо выделены ключевые части):

import tkinter as tk
from tkinter import messagebox
import requests
from PIL import Image, ImageTk
import webbrowser
import threading
import os
import io

# Настройки
EDAMAM_API_URL = "https://api.edamam.com/search"
IMAGE_CACHE_DIR = "image_cache"
os.makedirs(IMAGE_CACHE_DIR, exist_ok=True)

# Помогает переиспользовать соединения
session = requests.Session()
session.headers.update({"User-Agent": "recipe-finder/1.0"})

# Списки для виджетов
recipe_list = []
recipe_labels = []
recipe_images = []
recipe_links = []

# Полезные утилиты

def save_image_to_cache(url, content):
    # Генерируем имя файла на основе URL (без безопасности для простоты примера)
    filename = os.path.join(IMAGE_CACHE_DIR, str(abs(hash(url))) + ".jpg")
    try:
        with open(filename, "wb") as f:
            f.write(content)
    except Exception:
        return None
    return filename


def load_image_from_cache(url):
    filename = os.path.join(IMAGE_CACHE_DIR, str(abs(hash(url))) + ".jpg")
    if os.path.exists(filename):
        try:
            return Image.open(filename)
        except Exception:
            return None
    return None


def open_link(link):
    webbrowser.open(link)


def clear_recipe_list():
    recipe_list.clear()
    for label in recipe_labels:
        label.pack_forget()
    recipe_labels.clear()
    for image_label in recipe_images:
        # image_label у нас хранит PhotoImage в attribute .image
        try:
            image_label.pack_forget()
        except Exception:
            pass
    recipe_images.clear()
    for link_label in recipe_links:
        link_label.pack_forget()
    recipe_links.clear()


def get_top_5_recipes():
    # Запуск в отдельном потоке
    threading.Thread(target=_get_top_5_recipes_worker, daemon=True).start()


def _get_top_5_recipes_worker():
    recipe_name = entry_recipe_name.get().strip()
    if not recipe_name:
        messagebox.showinfo("Поиск", "Введите название рецепта")
        return

    # Считывание ключей из переменных окружения (безопаснее)
    app_id = os.getenv("EDAMAM_APP_ID")
    app_key = os.getenv("EDAMAM_APP_KEY")
    if not app_id or not app_key:
        messagebox.showerror("Ошибка", "Не заданы EDAMAM_APP_ID или EDAMAM_APP_KEY")
        return

    api_url = EDAMAM_API_URL
    params = {
        "q": recipe_name,
        "app_id": app_id,
        "app_key": app_key,
        "from": 0,
        "to": 5,
    }

    try:
        response = session.get(api_url, params=params, timeout=10)
        response.raise_for_status()
        data = response.json()
    except requests.RequestException as e:
        messagebox.showerror("Сеть", f"Ошибка при запросе: {e}")
        return
    except ValueError:
        messagebox.showerror("Данные", "Получен некорректный JSON от сервера")
        return

    # UI-очистка в основном потоке
    root.after(0, clear_recipe_list)

    if "hits" in data and data["hits"]:
        for i, hit in enumerate(data["hits"]):
            recipe = hit.get("recipe", {})
            recipe_list.append(recipe)
            recipe_name_label = recipe.get("label", "Без названия")
            recipe_link = recipe.get("url", "")
            image_url = recipe.get("image")

            # Работа с изображением: сначала попытка загрузить из кеша
            pil_img = None
            if image_url:
                pil_img = load_image_from_cache(image_url)
                if pil_img is None:
                    try:
                        img_resp = session.get(image_url, timeout=10)
                        img_resp.raise_for_status()
                        pil_img = Image.open(io.BytesIO(img_resp.content))
                        save_image_to_cache(image_url, img_resp.content)
                    except Exception:
                        pil_img = None

            if pil_img is None:
                # Заглушка — создаём простое изображение
                pil_img = Image.new("RGB", (200, 200), color=(240, 240, 240))

            # Изменяем размер
            pil_img = pil_img.resize((200, 200), Image.LANCZOS)
            photo_image = ImageTk.PhotoImage(pil_img)

            # Добавляем UI-элементы в основной поток
            root.after(0, lambda i=i, recipe_name_label=recipe_name_label, photo_image=photo_image, recipe_link=recipe_link: _add_recipe_widget(i, recipe_name_label, photo_image, recipe_link))
    else:
        messagebox.showinfo("Результаты", "Результаты не найдены")


def _add_recipe_widget(i, recipe_name_label, photo_image, recipe_link):
    recipe_title_label = tk.Label(canvas_frame, text=f"{i+1}. {recipe_name_label}", font=("Helvetica", 12, "bold"), bg="white")
    recipe_title_label.pack(pady=(5, 0), anchor=tk.CENTER)

    image_label = tk.Label(canvas_frame, image=photo_image, bg="white")
    image_label.image = photo_image
    image_label.pack(pady=(0, 5), anchor=tk.CENTER)

    link_label = tk.Label(canvas_frame, text=recipe_link, fg="blue", cursor="hand2", bg="white")
    link_label.pack(pady=(0, 10), anchor=tk.CENTER)
    link_label.bind("", lambda event, link=recipe_link: open_link(link))

    recipe_labels.append(recipe_title_label)
    recipe_images.append(image_label)
    recipe_links.append(link_label)

# UI init (сокращённо)
root = tk.Tk()
root.title("Recipe Finder")
root.geometry("600x700")
root.configure(bg="#F1F1F1")

frame = tk.Frame(root, bg="#F1F1F1")
frame.pack(fill=tk.BOTH, expand=tk.YES, padx=20, pady=20)

label_recipe_name = tk.Label(frame, text="Введите название рецепта:", font=("Helvetica", 14, "bold"), bg="#F1F1F1")
label_recipe_name.pack()

entry_recipe_name = tk.Entry(frame, font=("Helvetica", 12))
entry_recipe_name.pack(pady=5)

search_button = tk.Button(frame, text="Поиск рецептов", font=("Helvetica", 12, "bold"), command=get_top_5_recipes)
search_button.pack(pady=10)

canvas = tk.Canvas(frame, bg="white")
canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=tk.YES)

scrollbar = tk.Scrollbar(frame, orient=tk.VERTICAL, command=canvas.yview)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
canvas.configure(yscrollcommand=scrollbar.set)

canvas_frame = tk.Frame(canvas, bg="white")
canvas.create_window((0, 0), window=canvas_frame, anchor=tk.NW)
canvas_frame.bind("", lambda event: canvas.configure(scrollregion=canvas.bbox("all")))

root.mainloop()

Примечания к коду:

  • Мы используем requests.Session для переиспользования TCP-соединений.
  • Все сетевые операции проходят в отдельном потоке, чтобы не блокировать Tkinter.
  • Изображения кешируются по простому хешу URL, чтобы не скачивать их заново.
  • Ключи читаются из переменных окружения — безопаснее, чем хардкод в коде.

UI: структура и рекомендации по стилю

  • Один H1 в приложении (заголовок окна) и компактная панель управления (ввод + кнопка).
  • Используйте canvas + frame для списка результатов с вертикальной прокруткой.
  • Центрируйте элементы, добавляйте отступы (padding) для читаемости.
  • Делайте ссылки кликабельными и подчёркивайте их цветом.

Обработка ошибок и надёжность

  • Всегда задавайте таймаут у requests.get (например, timeout=10).
  • Используйте response.raise_for_status() для проверки HTTP-статуса.
  • Оборачивайте потенциально опасные операции (открытие изображений, запись в файл) в try/except.
  • Показывайте пользователю понятные сообщения через messagebox.

Улучшения и расширенные функции (идеи для практического развития)

  1. Фильтры и сортировка

    • Параметры Edamam позволяют фильтровать по типу кухни, диетическим меткам и прочему. Добавьте UI для фильтров и передавайте их в params.
  2. Закладки и локальная база

    • Позвольте сохранять понравившиеся рецепты в локальную БД (sqlite) или JSON-файл. Сделайте экран избранного.
  3. Поделиться и экспорт

    • Кнопки «Поделиться» (копировать ссылку, открыть диалог шаринга) и экспорт результатов в CSV/JSON.
  4. Кеш на уровне API

    • Ограничьте частоту запросов через backoff и локальный кеш (для одинаковых запросов возвращайте сохранённый результат в течение, например, 5 минут).
  5. Асинхронность и масштабирование

    • Для больших приложений рассмотрите использование asyncio + aiohttp вместо requests.
  6. Многоязычный интерфейс

    • Добавьте i18n (gettext) для локализации интерфейса.

Альтернативные подходы

  • Другие API: Spoonacular, TheMealDB, Tasty API (проверьте условия использования и тарифы).
  • Скрейпинг: если нет API, можно парсить сайты (не рекомендуется без согласия — проверьте правила и robots.txt).
  • Веб-приложение: для кроссплатформенности можно сделать веб‑фронтенд (Flask/FastAPI + React/Vue).

Ментальные модели и эвристики при проектировании

  • Keep UI responsive: никогда не выполняйте сетевые операции в основном потоке.
  • Fail fast, explain later: ловите ошибки и давайте пользователю понятный текст.
  • Least privilege: храните ключи отдельно и ограничивайте их права, если API позволяет.
  • Graceful degradation: при отсутствии изображения показывайте нейтральную заглушку.

Критерии приёмки

  1. Поиск по ключевой фразе возвращает не более 5 результатов и отображает для каждого: заголовок, изображение и кликабельную ссылку.
  2. UI остаётся отзывчивым при запуске поиска (нет «подвисаний»).
  3. При отсутствии сети приложение показывает понятное сообщение об ошибке.
  4. Изображения кешируются и повторные запросы не скачивают изображение заново.
  5. Ключи API не хардкодятся в коде (читаются из переменных окружения или защищённого файла).

Тесты и критерии приёмки — минимальный набор

  • Unit: тест парсинга JSON-ответа (правильное извлечение label, image, url).
  • Integration: тест, который эмулирует успешный и неуспешный ответ от API (mock) и проверяет реакцию GUI-слоя.
  • UI: ручная проверка прокрутки, отображения картинок, открытия ссылок.

Безопасность и приватность (GDPR и общие рекомендации)

  • Не храните персональные данные без необходимости. Рецепты — публичные данные, однако поведение пользователя (закладки, поисковые запросы) стоит хранить локально и не передавать третьим лицам.
  • Храните ключи API в переменных окружения или в защищённом хранилище; не коммитите их в репозиторий.
  • Для сбора аналитики всегда запрашивайте согласие пользователя; анонимизируйте данные.

Производительность и масштабирование

  • Используйте кеширование изображений и результатов поиска.
  • Ограничьте количество параллельных потоков при загрузке изображений.
  • Для больших объёмов данных рассматривайте перенос части логики на сервер (backend API), чтобы хранить и индексировать результаты.

Модель зрелости проекта (Maturity levels)

  • MVP: базовый поиск, отображение 5 результатов, открытие ссылки в браузере.
  • Stage 2: кеширование, обработка ошибок, фоновые запросы, сортировка и фильтры.
  • Stage 3: аккаунты пользователей, закладки синхронизируемые через сервер, аналитика и мультиплатформенность.

Чек-лист перед релизом

  • Ключи API вынесены из кода.
  • Обработаны сетевые ошибки и таймауты.
  • Добавлен механизм кеширования изображений.
  • Проведены ручные UI‑тесты (поиск, прокрутка, открытие ссылок, внешний вид).
  • Сформирована инструкция по деплою и необходимые системные зависимости.

Примеры отказов / когда подход не подходит

  • Если вам нужен полный контроль над содержимым рецептов (пошаговыми инструкциями, видео и т. п.), внешний API может быть слишком ограниченным.
  • Для коммерческих приложений проверьте ограничения тарифного плана Edamam (лимиты, лицензирование).

Шаблон описания релиза (анонс)

Скоро: простой и быстрый локальный поиск рецептов на Python. Введите название блюда — приложение найдёт топ‑5 вариантов, покажет картинки и ссылки. Удобный интерфейс и поддержка закладок в планах.

Социальный предпросмотр (OG)

OG title: Быстрый поиск рецептов — Python + Tkinter OG description: Локальное приложение поиска рецептов (Edamam + Pillow) — топ‑5 результатов, картинки и ссылки в одном окне.

Вывод и краткое резюме

Создание Recipe Finder — отличная задача для прокачки навыков работы с HTTP‑API, GUI и обработкой изображений. В этой статье вы получили рабочую архитектуру, готовый пример кода и набор практических улучшений: фильтры, кеш, многопоточность и рекомендации по безопасности. Начинайте с MVP и постепенно улучшайте функционал по чек‑листу.

Пример результата поиска

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


Ключевые ссылки и ресурсы:

  • Документация Edamam Recipe Search API — используйте Dashboard для получения app_id и app_key.
  • Pillow — документация по работе с изображениями и методам ресемплинга.

Конец руководства.

Поделиться: X/Twitter Facebook LinkedIn Telegram
Автор
Редакция

Похожие материалы

Несколько аккаунтов Skype: Multi Skype Launcher
Программное обеспечение

Несколько аккаунтов Skype: Multi Skype Launcher

Журнал для работы: повысить продуктивность
Productivity

Журнал для работы: повысить продуктивность

Персональные звуки уведомлений на Android
Android.

Персональные звуки уведомлений на Android

Скачивание шоу Hulu для офлайн‑просмотра
Стриминг

Скачивание шоу Hulu для офлайн‑просмотра

Microsoft Start: персонализированная новостная лента
Новости

Microsoft Start: персонализированная новостная лента

Как изменить имя в Epic Games быстро
Гайды

Как изменить имя в Epic Games быстро