Создание интерактивных меню в Python-ботах: пошаговое руководство

Пройдите тест, узнайте какой профессии подходите
Сколько вам лет
0%
До 18
От 18 до 24
От 25 до 34
От 35 до 44
От 45 до 49
От 50 до 54
Больше 55

Для кого эта статья:

  • Разработчики, изучающие создание чат-ботов на Python
  • Студенты, проходящие курсы программирования и желающие улучшить навыки в разработке ботов
  • Профессионалы, стремящиеся повысить юзабилити и функциональность своих программных решений

    Создание чат-бота без продуманного интерфейса — всё равно что построить дом без дверей. Пользователи быстро запутаются и уйдут, не разобравшись в командах. Интерактивные меню решают эту проблему, превращая взаимодействие с ботом в интуитивно понятный процесс. В этом руководстве вы научитесь создавать удобные навигационные системы для ваших Python-ботов, от простых кнопок до многоуровневых меню с обработкой колбэков. Готовы превратить своего бота из примитивного текстового интерфейса в полноценное интерактивное приложение? 🚀

Хотите быстро освоить создание ботов с интерактивными меню и другие востребованные навыки Python-разработчика? Обучение Python-разработке от Skypro поможет вам перейти от теории к реальным проектам за 9 месяцев. Наши студенты создают функциональных ботов с продвинутыми интерфейсами уже на втором месяце обучения, а к выпуску формируют портфолио из 5+ проектов, с которыми успешно трудоустраиваются.

Интерактивное меню в Python-ботах: основы и преимущества

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

Когда я только начинал создавать своего первого бота для управления проектами, пользователи постоянно жаловались на необходимость запоминать команды. Внедрение интерактивного меню увеличило удержание на 67% и сократило количество ошибок пользователей почти в 3 раза.

Александр Соколов, Python-разработчик

Мой первый бот для Telegram был простым конвертером валют. Пользователи писали команду типа "/convert 100 USD to EUR", и бот выдавал результат. Звучит просто, но на практике 80% сообщений содержали ошибки: опечатки в названиях валют, неправильный формат запроса, забытые пробелы. Логи были заполнены сообщениями об ошибках.

После внедрения интерактивного меню с кнопками выбора валют и суммы ситуация кардинально изменилась. Количество успешных конвертаций выросло с 20% до 95%. Пользователи просто выбирали из списка валюту, вводили сумму и нажимали кнопку "Конвертировать". Бот стали использовать в 3 раза чаще, а количество жалоб сократилось почти до нуля.

Давайте рассмотрим основные преимущества интерактивных меню в ботах:

  • Повышение юзабилити — пользователи видят доступные опции, что снижает когнитивную нагрузку
  • Снижение ошибок — структурированный ввод минимизирует возможность опечаток или неверных команд
  • Направленное взаимодействие — вы контролируете пользовательский сценарий, предлагая только релевантные опции
  • Визуальная привлекательность — кнопки и меню делают интерфейс более современным и привычным для пользователей
  • Статистика использования — легче отслеживать, какие функции бота используются чаще
Тип взаимодействия Текстовые команды Интерактивное меню
Скорость освоения Низкая (требуется запоминание команд) Высокая (интуитивно понятный интерфейс)
Процент ошибок пользователя 30-40% 5-10%
Удовлетворенность пользователей Средняя Высокая
Сложность реализации Низкая Средняя

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

  • Reply-клавиатуры — физически отображаются под полем ввода сообщения
  • Inline-клавиатуры — отображаются прямо в сообщении бота и позволяют создавать более сложные интерфейсы

Теперь, когда мы понимаем ценность интерактивных меню, давайте разберемся, как настроить проект для их создания. 🛠️

Пошаговый план для смены профессии

Настройка проекта и выбор библиотек для создания бота

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

Дмитрий Волков, Lead Python Developer

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

Однако, когда клиент начал добавлять новые требования — многоуровневые меню, более сложные сценарии взаимодействия, асинхронную обработку запросов — мы столкнулись с ограничениями выбранной библиотеки. Перепись проекта на aiogram заняла почти неделю, но результат стоил усилий. Производительность бота выросла на 40%, а код стал гораздо чище и поддерживаемым.

Главный урок, который мы извлекли: выбирайте библиотеку не только для текущих задач, но и с учетом потенциального масштабирования проекта.

Давайте сравним наиболее популярные библиотеки для создания ботов в Python:

Библиотека Преимущества Недостатки Подходит для
aiogram (3.x) Асинхронная, масштабируемая, современный API Более сложная для начинающих Сложных ботов с высокой нагрузкой
pyTelegramBotAPI Простота использования, понятная документация Ограниченная масштабируемость Простых ботов и быстрых прототипов
python-telegram-bot Полная имплементация Telegram Bot API Более обширный API требует изучения Средних и сложных проектов
discord.py Отлично подходит для Discord-ботов Только для платформы Discord Сообществ и игровых серверов Discord

Для нашего руководства я выбрал aiogram, так как он предоставляет наиболее гибкие инструменты для создания интерактивных меню и хорошо масштабируется для сложных проектов.

Вот базовые шаги для настройки проекта с aiogram:

  1. Создайте виртуальное окружение Python:
python -m venv venv
source venv/bin/activate # На Windows: venv\Scripts\activate

  1. Установите необходимые пакеты:
pip install aiogram==3.0.0b7

  1. Создайте базовую структуру проекта:
my_bot/
├── bot.py # Основной файл бота
├── config.py # Конфигурация (токены, настройки)
├── keyboards.py # Модуль для клавиатур и меню
└── handlers.py # Обработчики сообщений и колбэков

Теперь давайте создадим базовый код для нашего бота в файле bot.py:

Python
Скопировать код
from aiogram import Bot, Dispatcher, types
from aiogram.filters import CommandStart
import asyncio
import logging
from config import BOT_TOKEN

# Настраиваем логирование
logging.basicConfig(level=logging.INFO)

# Инициализируем бота и диспетчер
bot = Bot(token=BOT_TOKEN)
dp = Dispatcher()

# Обработчик команды /start
@dp.message(CommandStart())
async def cmd_start(message: types.Message):
await message.answer(
"Привет! Я бот с интерактивным меню.",
reply_markup=types.ReplyKeyboardRemove()
)

# Запуск бота
async def main():
await dp.start_polling(bot)

if __name__ == "__main__":
asyncio.run(main())

А в файле config.py необходимо создать переменную с токеном:

Python
Скопировать код
BOT_TOKEN = "ваш_токен_от_BotFather"

Это базовая настройка проекта. Теперь мы готовы перейти к созданию структуры интерактивного меню. 🧩

Структура интерактивного меню и типы кнопок в aiogram

Aiogram предлагает два основных типа клавиатур для создания интерактивных меню: reply-клавиатуры и inline-клавиатуры. Каждый тип имеет свои особенности и сценарии использования. Давайте разберем их подробнее и научимся создавать различные структуры меню.

Reply-клавиатуры отображаются под полем ввода сообщения и заменяют стандартную клавиатуру. Они идеальны для основного меню и частых действий. В aiogram 3.x создание reply-клавиатуры выглядит так:

Python
Скопировать код
from aiogram import types
from aiogram.utils.keyboard import ReplyKeyboardBuilder

def get_main_menu():
builder = ReplyKeyboardBuilder()

# Добавляем кнопки в клавиатуру
builder.row(
types.KeyboardButton(text="📊 Статистика"),
types.KeyboardButton(text="⚙️ Настройки")
)
builder.row(
types.KeyboardButton(text="❓ Помощь"),
types.KeyboardButton(text="👤 Профиль")
)

# Формируем клавиатуру с параметрами
return builder.as_markup(
resize_keyboard=True, # Адаптивный размер
one_time_keyboard=False # Клавиатура не скрывается после нажатия
)

# Использование клавиатуры в обработчике
@dp.message(CommandStart())
async def cmd_start(message: types.Message):
await message.answer(
"Выберите действие:",
reply_markup=get_main_menu()
)

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

Python
Скопировать код
from aiogram import types
from aiogram.utils.keyboard import InlineKeyboardBuilder

def get_products_menu():
builder = InlineKeyboardBuilder()

# Добавляем кнопки с callback_data
builder.row(
types.InlineKeyboardButton(
text="🍕 Пицца", 
callback_data="category:pizza"
),
types.InlineKeyboardButton(
text="🍔 Бургеры", 
callback_data="category:burgers"
)
)
builder.row(
types.InlineKeyboardButton(
text="🥤 Напитки", 
callback_data="category:drinks"
),
types.InlineKeyboardButton(
text="🍰 Десерты", 
callback_data="category:desserts"
)
)
builder.row(
types.InlineKeyboardButton(
text="🔙 Назад", 
callback_data="back:main"
)
)

return builder.as_markup()

Кроме стандартных кнопок, inline-клавиатура поддерживает несколько специальных типов:

  • URL-кнопки — открывают указанную ссылку
  • Callback-кнопки — отправляют данные боту для обработки
  • Web App кнопки — открывают веб-приложения внутри Telegram
  • Login кнопки — предоставляют аутентификацию через Telegram
  • Switch Inline кнопки — переключают бота в режим inline-запросов

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

Python
Скопировать код
def get_grid_menu():
builder = InlineKeyboardBuilder()

# Создаем сетку 3x3 с цифрами
buttons = [
types.InlineKeyboardButton(
text=str(i), 
callback_data=f"number:{i}"
) for i in range(1, 10)
]

# Добавляем кнопки сеткой 3x3
builder.add(*buttons)
builder.adjust(3) # 3 кнопки в строке

return builder.as_markup()

При создании структуры меню важно продумать навигацию. Хорошей практикой является создание кнопок "Назад" и "Главное меню" для многоуровневых структур:

Python
Скопировать код
def get_submenu(category_id):
builder = InlineKeyboardBuilder()

# Динамическое создание кнопок на основе категории
if category_id == "pizza":
items = ["Маргарита", "Пепперони", "Гавайская", "Четыре сыра"]
elif category_id == "drinks":
items = ["Кола", "Спрайт", "Сок", "Вода"]
else:
items = ["Товар 1", "Товар 2", "Товар 3"]

# Добавляем кнопки с товарами
for item in items:
builder.row(
types.InlineKeyboardButton(
text=item, 
callback_data=f"product:{category_id}:{item}"
)
)

# Навигационные кнопки
builder.row(
types.InlineKeyboardButton(
text="🔙 Назад к категориям", 
callback_data="back:categories"
),
types.InlineKeyboardButton(
text="🏠 Главное меню", 
callback_data="menu:main"
)
)

return builder.as_markup()

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

Реализация inline keyboard и обработка колбэков в Python

Одним из ключевых аспектов создания интерактивного меню является правильная обработка колбэков (callback queries) — данных, отправляемых боту при нажатии на кнопки inline-клавиатуры. В этом разделе мы рассмотрим, как эффективно организовать этот процесс.

В aiogram 3.x обработка колбэков происходит через регистрацию обработчиков с соответствующими фильтрами. Рекомендуется использовать структурированный формат callback_data для упрощения обработки:

Python
Скопировать код
from aiogram import Router, F
from aiogram.types import CallbackQuery

router = Router()

# Обработчик колбэков категорий
@router.callback_query(F.data.startswith("category:"))
async def process_category_selection(callback: CallbackQuery):
# Извлекаем идентификатор категории из callback_data
category_id = callback.data.split(":")[1]

# Отвечаем пользователю и обновляем клавиатуру
await callback.message.edit_text(
f"Вы выбрали категорию: {category_id}",
reply_markup=get_submenu(category_id)
)

# Обязательно отвечаем на колбэк, чтобы убрать индикатор загрузки
await callback.answer()

# Обработчик колбэков навигации
@router.callback_query(F.data.startswith("back:"))
async def process_back_button(callback: CallbackQuery):
destination = callback.data.split(":")[1]

if destination == "main":
await callback.message.edit_text(
"Главное меню:",
reply_markup=get_main_menu_inline()
)
elif destination == "categories":
await callback.message.edit_text(
"Выберите категорию:",
reply_markup=get_products_menu()
)

await callback.answer()

Для более сложной обработки колбэков можно использовать фабричный подход с использованием CallbackData. В aiogram 3.x для этого есть специальный класс:

Python
Скопировать код
from aiogram.filters.callback_data import CallbackData

class ProductCallback(CallbackData, prefix="product"):
category_id: str
product_id: str
action: str = "view" # Действие по умолчанию

# Создание кнопки с использованием фабрики
button = types.InlineKeyboardButton(
text="Пепперони", 
callback_data=ProductCallback(
category_id="pizza",
product_id="pepperoni"
).pack()
)

# Обработчик с использованием фабрики
@router.callback_query(ProductCallback.filter())
async def process_product_action(
callback: CallbackQuery, 
callback_data: ProductCallback
):
# Извлекаем данные из структурированного объекта
category = callback_data.category_id
product = callback_data.product_id
action = callback_data.action

if action == "view":
# Показываем информацию о товаре
await callback.message.edit_text(
f"Товар: {product} из категории {category}\n\n"
f"Здесь будет описание товара...",
reply_markup=get_product_menu(category, product)
)
elif action == "add":
# Добавляем товар в корзину
await callback.answer(f"Товар {product} добавлен в корзину!", show_alert=True)

await callback.answer()

При работе с колбэками важно помнить о некоторых ограничениях:

  1. Размер callback_data — Telegram ограничивает размер callback_data до 64 байт
  2. Время ответа — на колбэк необходимо ответить в течение 30 секунд
  3. Частота обновлений — существуют ограничения на частоту обновления сообщений

Вот примеры различных паттернов взаимодействия с inline-клавиатурой:

Паттерн взаимодействия Описание Пример использования
Изменение текущего меню Обновление текста и клавиатуры текущего сообщения Навигация по категориям
Пагинация Переключение между страницами результатов Список товаров, новостная лента
Одноразовые уведомления Всплывающие сообщения без изменения интерфейса Подтверждение действий, ошибки
Многошаговые формы Последовательный сбор информации от пользователя Регистрация, заказ товаров

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

Python
Скопировать код
class PaginationCallback(CallbackData, prefix="page"):
action: str # next, prev, current
page: int

def get_paginated_menu(items, page=0, items_per_page=5):
builder = InlineKeyboardBuilder()

# Определяем начальный и конечный индексы для текущей страницы
start_idx = page * items_per_page
end_idx = min(start_idx + items_per_page, len(items))

# Добавляем кнопки для текущих элементов
for item in items[start_idx:end_idx]:
builder.row(
types.InlineKeyboardButton(
text=item, 
callback_data=f"select:{item}"
)
)

# Кнопки навигации
navigation = []

# Кнопка "Назад", если не первая страница
if page > 0:
navigation.append(
types.InlineKeyboardButton(
text="◀️ Назад", 
callback_data=PaginationCallback(
action="prev", 
page=page-1
).pack()
)
)

# Информация о текущей странице
navigation.append(
types.InlineKeyboardButton(
text=f"{page+1}/{(len(items)+items_per_page-1)//items_per_page}", 
callback_data=PaginationCallback(
action="current", 
page=page
).pack()
)
)

# Кнопка "Вперед", если не последняя страница
if end_idx < len(items):
navigation.append(
types.InlineKeyboardButton(
text="Вперед ▶️", 
callback_data=PaginationCallback(
action="next", 
page=page+1
).pack()
)
)

builder.row(*navigation)
return builder.as_markup()

@router.callback_query(PaginationCallback.filter())
async def process_pagination(callback: CallbackQuery, callback_data: PaginationCallback):
# Здесь items должны быть доступны или извлечены из БД/состояния
items = ["Товар 1", "Товар 2", "Товар 3", "Товар 4", "Товар 5", 
"Товар 6", "Товар 7", "Товар 8", "Товар 9", "Товар 10"]

action = callback_data.action
current_page = callback_data.page

if action == "current":
# Просто обновляем сообщение колбэка
await callback.answer(f"Текущая страница: {current_page+1}")
return

# Обновляем меню с новой страницей
await callback.message.edit_reply_markup(
reply_markup=get_paginated_menu(items, page=current_page)
)
await callback.answer()

Такая организация обработки колбэков позволяет создавать сложные интерактивные интерфейсы, поддерживая при этом читаемость и поддерживаемость кода. 🧠

Расширение функционала: многоуровневые меню и диспетчеры

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

Одна из ключевых концепций при построении сложных меню — FSM (Finite State Machine, или конечный автомат). В aiogram 3.x для этого используется встроенная система состояний:

Python
Скопировать код
from aiogram.fsm.state import State, StatesGroup
from aiogram.fsm.context import FSMContext

# Определяем состояния для нашего меню
class MenuStates(StatesGroup):
main = State() # Главное меню
catalog = State() # Каталог товаров
category = State() # Выбранная категория
product = State() # Просмотр товара
cart = State() # Корзина
checkout = State() # Оформление заказа

# Обработчик перехода в главное меню
@router.message(Command("menu"))
@router.callback_query(F.data == "menu:main")
async def show_main_menu(event: Union[Message, CallbackQuery], state: FSMContext):
# Устанавливаем состояние
await state.set_state(MenuStates.main)

# Сохраняем дополнительные данные в состоянии
await state.update_data(last_visited=datetime.now().isoformat())

# Отправляем меню в зависимости от типа события
if isinstance(event, CallbackQuery):
await event.message.edit_text(
"🏠 Главное меню",
reply_markup=get_main_menu_inline()
)
await event.answer()
else: # Message
await event.answer(
"🏠 Главное меню",
reply_markup=get_main_menu_inline()
)

# Обработчик выбора категории
@router.callback_query(MenuStates.main, F.data.startswith("category:"))
async def show_category(callback: CallbackQuery, state: FSMContext):
category_id = callback.data.split(":")[1]

# Устанавливаем состояние категории
await state.set_state(MenuStates.category)

# Сохраняем данные категории в состоянии
await state.update_data(current_category=category_id)

# Загружаем продукты для категории (здесь могла бы быть загрузка из БД)
products = get_products_for_category(category_id)

await callback.message.edit_text(
f"Категория: {category_id.capitalize()}\n"
f"Доступно товаров: {len(products)}",
reply_markup=get_category_menu(category_id, products)
)
await callback.answer()

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

Python
Скопировать код
class MenuDispatcher:
def __init__(self):
self.menus = {}
self.default_menu = None

def register_menu(self, menu_id, get_content_func, get_markup_func):
"""Регистрирует новое меню"""
self.menus[menu_id] = {
'get_content': get_content_func,
'get_markup': get_markup_func
}

def set_default(self, menu_id):
"""Устанавливает меню по умолчанию"""
if menu_id in self.menus:
self.default_menu = menu_id

async def show_menu(self, menu_id, event, state=None, **kwargs):
"""Показывает указанное меню"""
if menu_id not in self.menus:
if self.default_menu:
menu_id = self.default_menu
else:
raise ValueError(f"Menu {menu_id} not found and no default menu set")

menu = self.menus[menu_id]
content = await menu['get_content'](**kwargs)
markup = await menu['get_markup'](**kwargs)

# Сохраняем текущее меню в состоянии, если оно предоставлено
if state:
await state.update_data(current_menu=menu_id, menu_params=kwargs)

# Показываем меню в зависимости от типа события
if isinstance(event, CallbackQuery):
await event.message.edit_text(content, reply_markup=markup)
await event.answer()
else: # Message
await event.answer(content, reply_markup=markup)

return menu_id

# Использование диспетчера меню
menu_dispatcher = MenuDispatcher()

# Регистрация меню
menu_dispatcher.register_menu(
'main',
lambda **kw: "🏠 Главное меню",
lambda **kw: get_main_menu_inline()
)

menu_dispatcher.register_menu(
'category',
lambda category_id, **kw: f"Категория: {category_id.capitalize()}",
lambda category_id, **kw: get_category_menu(category_id)
)

menu_dispatcher.set_default('main')

# Обработчик для использования диспетчера
@router.callback_query(F.data.startswith("menu:"))
async def process_menu_navigation(callback: CallbackQuery, state: FSMContext):
menu_id = callback.data.split(":")[1]

# Извлекаем параметры из callback_data или состояния
params = {}
if len(callback.data.split(":")) > 2:
# Парсим дополнительные параметры из callback_data
params_str = callback.data.split(":", 2)[2]
for param in params_str.split(","):
if "=" in param:
key, value = param.split("=")
params[key] = value

# Если параметров нет в callback_data, пробуем взять из состояния
if not params:
data = await state.get_data()
params = data.get("menu_params", {})

# Показываем меню через диспетчер
await menu_dispatcher.show_menu(menu_id, callback, state, **params)

Для удобной организации сложных интерфейсов можно создать систему "слоев" меню:

  • Основное меню — первый уровень навигации (Каталог, Корзина, Профиль)
  • Контекстное меню — зависит от текущего контекста (например, действия с товаром)
  • Динамическое меню — генерируется на основе данных (список товаров категории)
  • Сервисное меню — доступно всегда (Помощь, Назад, Главное меню)

При создании многоуровневых меню важно следить за UX и предусматривать возможность быстрой навигации. Вот некоторые рекомендации:

UX-рекомендация Реализация
Возможность вернуться на шаг назад Кнопка "Назад" с сохранением истории навигации в состоянии
Быстрый доступ к главному меню Постоянная кнопка "Главное меню" в нижней части клавиатуры
Визуальная иерархия Группировка кнопок по функциональности, использование эмодзи для визуальных акцентов
Индикация текущего положения Заголовки с указанием текущего раздела, хлебные крошки для глубоких структур
Ограничение глубины меню Не более 3-4 уровней вложенности для предотвращения потери контекста

Для трекинга поведения пользователей в сложных меню можно реализовать аналитику навигации:

Python
Скопировать код
async def track_menu_navigation(user_id, from_menu, to_menu, context=None):
"""Записывает переход между меню для аналитики"""
logging.info(f"Navigation: {user_id} moved from {from_menu} to {to_menu}")

# Здесь может быть код для записи в БД или отправки в аналитическую систему

# Пример структуры события навигации
navigation_event = {
"user_id": user_id,
"timestamp": datetime.now().isoformat(),
"from_menu": from_menu,
"to_menu": to_menu,
"context": context or {},
"session_id": generate_session_id(user_id)
}

# Сохранение события (например, в MongoDB)
# await navigation_collection.insert_one(navigation_event)

Использование этих техник позволит вам создавать сложные и гибкие интерфейсы для ботов, которые останутся понятными для пользователей и поддерживаемыми для разработчиков. 🎮

Работа с интерактивными меню в Python-ботах — это не просто набор технических приемов, а целое искусство создания удобных интерфейсов. Изучив основы и продвинутые техники из этого руководства, вы сможете создавать ботов, которые будут по-настоящему полезны вашей аудитории. Помните, что каждая кнопка в меню — это возможность направить пользователя к нужной информации или действию без лишних слов и объяснений. Именно в этом и заключается главная ценность хорошо спроектированного интерактивного меню.

Читайте также

Проверь как ты усвоил материалы статьи
Пройди тест и узнай насколько ты лучше других читателей
Какой библиотекой рекомендуется пользоваться для создания ботов на Python?
1 / 5

Загрузка...