Recipe Finder на Python: Tkinter, Edamam и Pillow

Зачем делать собственный поиск рецептов
Интернет полон рецептов, но результаты часто разрозненные, содержат рекламу и нерелевантные страницы. Собственное приложение позволяет:
- Предоставлять таргетированный, удобный интерфейс без лишних элементов.
- Контролировать фильтры (диетические предпочтения, время готовки, кухня).
- Практиковаться с 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:
- Перейдите на Edamam и нажмите кнопку Signup API. Выберите план “Recipe Search API - Developer” и завершите регистрацию.

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

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

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.
Улучшения и расширенные функции (идеи для практического развития)
Фильтры и сортировка
- Параметры Edamam позволяют фильтровать по типу кухни, диетическим меткам и прочему. Добавьте UI для фильтров и передавайте их в params.
Закладки и локальная база
- Позвольте сохранять понравившиеся рецепты в локальную БД (sqlite) или JSON-файл. Сделайте экран избранного.
Поделиться и экспорт
- Кнопки «Поделиться» (копировать ссылку, открыть диалог шаринга) и экспорт результатов в CSV/JSON.
Кеш на уровне API
- Ограничьте частоту запросов через backoff и локальный кеш (для одинаковых запросов возвращайте сохранённый результат в течение, например, 5 минут).
Асинхронность и масштабирование
- Для больших приложений рассмотрите использование asyncio + aiohttp вместо requests.
Многоязычный интерфейс
- Добавьте 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: при отсутствии изображения показывайте нейтральную заглушку.
Критерии приёмки
- Поиск по ключевой фразе возвращает не более 5 результатов и отображает для каждого: заголовок, изображение и кликабельную ссылку.
- UI остаётся отзывчивым при запуске поиска (нет «подвисаний»).
- При отсутствии сети приложение показывает понятное сообщение об ошибке.
- Изображения кешируются и повторные запросы не скачивают изображение заново.
- Ключи 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 — документация по работе с изображениями и методам ресемплинга.
Конец руководства.
Похожие материалы
Несколько аккаунтов Skype: Multi Skype Launcher
Журнал для работы: повысить продуктивность
Персональные звуки уведомлений на Android
Скачивание шоу Hulu для офлайн‑просмотра
Microsoft Start: персонализированная новостная лента