Как я автоматизировал первичный поиск публичных Telegram-групп: Telethon, SQLite и дедупликация
Когда нужно найти небольшие тематические 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, если появится несколько пользователей или большой объём результатов.
Но до этого стоит измерить, достаточно ли текущего качества выдачи. Для многих прикладных автоматизаций простая система с явными ограничениями полезнее сложной, но непрозрачной.