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

Ниже — разбор небольшого Python-инструмента для первичного поиска публичных групп. Он не вступает в чаты, не собирает участников, не пишет сообщения и не рассылает ничего от имени аккаунта. Его результат — короткий список кандидатов, который всё равно проверяет человек.

Задача намеренно узкая: взять набор поисковых фраз, получить результаты штатным клиентским API Telegram, отфильтровать их, не присылать дубликаты и отправить сводку администратору.

Почему не Bot API

Bot API не предназначен для глобального поиска публичных сообществ. Поэтому поисковая часть использует отдельную пользовательскую сессию Telethon, а управляющая часть — обычного бота.

Такое разделение оказалось полезным по двум причинам:

  • основной аккаунт не участвует в повторяющихся поисковых запросах;

  • бот с командами можно ограничить списком ID администраторов, а сессия поиска остаётся отдельной.

Для этого нужен отдельный Telegram-аккаунт и зарегистрированное приложение на my.telegram.org. Его файлы сессии и параметры приложения не должны попадать в репозиторий.

Схема

список запросов
        ↓
contacts.SearchRequest
        ↓
проверка типа сущности и диапазона участников
        ↓
проверка названия и описания
        ↓
SQLite: сохранение идентификатора и ссылки
        ↓
уведомление администратору только о новых результатах

Систему не нужно превращать в полноценный краулер. Поиск Telegram уже умеет ранжировать результаты, а на нашей стороне остаётся прозрачная фильтрация.

Поиск и отбор кандидатов

Упрощённый фрагмент сканирования выглядит так:

from telethon import functions, types

async def search_groups(client, queries: list[str]):
    for query in queries:
        result = await client(functions.contacts.SearchRequest(
            q=query,
            limit=50,
        ))

        for chat in result.chats:
            if not isinstance(chat, (types.Channel, types.Chat)):
                continue
            if isinstance(chat, types.Channel) and not chat.megagroup:
                continue
            yield chat

SearchRequest возвращает не только группы: там могут встречаться пользователи, каналы и другие сущности. В моём случае нужны именно публичные группы. Для Channel дополнительно проверяю megagroup, чтобы не смешивать их с каналами.

После этого применяются простые условия:

  • число участников в заданном диапазоне;

  • в названии или описании есть одно из ключевых слов;

  • у объекта есть публичная ссылка;

  • группа ещё не была сохранена в базе.

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

Дедупликация — важнее, чем кажется

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

Я храню в SQLite стабильный идентификатор Telegram-сущности, её публичную ссылку, название, описание и время обнаружения. Перед уведомлением выполняется проверка по chat_id.

CREATE TABLE IF NOT EXISTS groups (
    chat_id INTEGER PRIMARY KEY,
    username TEXT UNIQUE,
    title TEXT NOT NULL,
    description TEXT,
    members_count INTEGER,
    found_at TEXT NOT NULL
);

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

При повторном сканировании в уведомление попадает только то, чего в базе ещё не было. Это снижает шум сильнее любого красивого форматирования сообщений.

Команды и расписание

У инструмента три команды:

  • /scan — выполнить поиск сразу;

  • /groups — показать последние сохранённые результаты;

  • /status — показать текущие параметры поиска.

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

async def periodic_loop(bot_client, scout, hours: int):
    while True:
        try:
            groups = await scout.scan()
            if groups:
                await notify_admin(bot_client, groups)
        except Exception:
            logger.exception("Scheduled scan failed")

        await asyncio.sleep(hours * 3600)

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

Ограничения и что не стоит автоматизировать

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

Также важно не пытаться обходить ограничения Telegram: не делать агрессивный polling, не использовать несколько сессий одновременно и корректно обрабатывать ошибки сети и лимиты. Поисковая сессия должна использоваться эксклюзивно: Telegram может отозвать авторизацию, если один файл сессии одновременно применяется с разных IP-адресов.

Что получилось

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

Исходники и пример конфигурации: https://github.com/relaybit/local-leads-scout

Что можно развить дальше

  • добавить оценку релевантности по нескольким признакам вместо одного ключевого слова;

  • хранить историю изменений названия и числа участников;

  • вынести конфигурацию запросов в интерфейс бота;

  • перейти на PostgreSQL, если появится несколько пользователей или большой объём результатов.

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