Мне понадобился двухконтурный парсер Telegram-чатов для лидогенерации (искать клиентов на услуги автоматизации и разработки ботов).
Стек: Python (Telethon) на сервере ➔ Webhook ➔ n8n ➔ LLM (OpenAI) ➔ Google Таблицы + Уведомления.
В этой статье я покажу, как я столкнулся с абсолютной «слепотой» парсера на боевых чатах, почему стандартный слушатель events.NewMessage не годится для продакшена и как я перевел архитектуру на Active Pull. В итоге я получил неубиваемую систему, которая не пропускает ни одного сообщения и экономит сотни долларов на токенах OpenAI благодаря грамотной локальной фильтрации.
Иллюзия работы
Логика была простой: скрипт берет список целевых чатов, стоп-слов и интентов из Google Таблицы (через n8n), слушает чаты, фильтрует мусор и шлет потенциальные лиды в n8n, где нейросеть делает финальную квалификацию (Lead / Not Lead).
Я написал код, запустил в закрытой тестовой группе. Парсер мгновенно ловил сообщения и пушил их в вебхук. Всё супер, выкатываю на прод.
И тут начинается просто жесть. В реальных «боевых» чатах (их было около 26) стояла абсолютная тишина. Ни одного лида за сутки. При этом мой аккаунт-ищейка 100% состоял в этих группах. Парсер просто ослеп. Хотя лидов из тестовой группы ловил на ура....
Разбор ошибок: Баги первого слоя
Прежде чем я докопался до архитектурной проблемы Телеграма, я выгреб пачку локальных граблей.
Грабли 1: Флоаты из Excel
n8n отдавал ID чатов из Google Таблиц в формате float (например, -100123456789.0). Telethon оперирует строгими int. Естественно, сравнение строк "-100123456789.0" == "-100123456789" давало False, и парсер молча отбрасывал сообщения.
Фикс: Написал функцию clean_id(), жестко отрезающую .0 и приводящую всё к целому числу.
Грабли 2: DDoS самого себя (FloodWait)
В старом коде на каждое входящее событие (до проверки текста) вызывался await event.get_chat(). В высоконагруженных чатах это генерировало сотни запросов к API в минуту. Telegram либо кидал FloodWaitError, либо тихо рвал сокет.
Фикс: Перенес тяжелые API-запросы строго внутрь блока if has_keyword and has_intent. Сначала фильтрую текст локально, потом дергаю API.
Грабли 3: Тихие зависания сокетов
Telethon подвержен проблеме silent disconnect. Процесс висит в ОС, ошибок в логах нет, но пакеты не идут.
Фикс: Написал асинхронный connection_watchdog, который раз в 60 секунд делает элементарный await client.get_me(). Если ответа нет — жесткий реконнект.
Главная боль: Блэкаут от Telegram (MTProto)
Я вычистил код, добавил логирование сырых событий. Тестовая группа работает. Реальные чаты — молчат. Ни одного события NewMessage не прилетает.
Чтобы проверить, не забанен ли аккаунт, я набросал скрипт dump_history.py, который через client.iter_messages выкачал историю за последние 24 часа. И о чудо! Скрипт выкачал сотни сообщений. Аккаунт не забанен, он всё видит. Но почему не срабатывает слушатель?
В чем была загвоздка (Мясо):
Я уперся в архитектурную оптимизацию серверов Telegram. Мой аккаунт-ищейка состоял в ~3000 чатах (закешировано 2889 диалогов).
Стандартный слушатель @client.on(events.NewMessage()) работает по принципу Push-уведомлений. Но серверы Telegram отключают Push-события для аккаунтов, которые:
Состоят в огромном количестве супергрупп.
Являются «пассивными» (не пишут сообщения, не скроллят ленту с официального клиента).
Держат чаты в Муте (Mute) или Архиве.
Telegram просто экономит ресурсы своих серверов и перестает пушить обновления в Telethon. В тестовой группе я сам писал сообщения — чат был «горячим», пуши шли. В чужих чатах я был пассивным наблюдателем, и Telegram перекрыл мне кран.
Инженерное решение: Смена парадигмы на Active Pull
Раз Telegram не хочет присылать мне события, я буду забирать их сам. Почесал репу, полностью выкинул декоратор events.NewMessage() и перешел на архитектуру Active Pull (Пылесос).
Как это работает:
Создаю словарь LAST_MSG_IDS = {}, где храню ID последнего прочитанного сообщения для каждого чата.
Запускаю бесконечный цикл while True.
Каждые 30 секунд скрипт итерируется по списку целевых чатов и делает точечный запрос: client.get_messages(chat_id, min_id=LAST_MSG_IDS[chat_id], limit=50).
Я принудительно высасываю всё, что появилось нового, обновляю счетчик min_id и прогоняю текст через фильтры.
Почему за это не банят:
26 чатов с паузой await asyncio.sleep(1) между ними + 30 секунд отдыха = 1 круг занимает ~56 секунд. Это всего ~26 запросов в минуту. Для антиспам-системы Telegram это смешная нагрузка, абсолютно неотличимая от живого человека, который листает папки. Бан за чтение (Read-Only) за такое не дают.
Тот самый кусок кода, который спас проект:
code Python
LAST_MSG_IDS = {} async def active_pull_parser(): logging.info("=== ЗАПУСК АКТИВНОГО PULL-ПАРСЕРА ===") while True: for chat_id in list(CHATS_RESOLVED): try: # Инициализация: запоминаем ID последнего сообщения в чате if chat_id not in LAST_MSG_IDS: msgs = await client.get_messages(chat_id, limit=1) if msgs: LAST_MSG_IDS[chat_id] = msgs[0].id continue # Принудительно запрашиваем всё, что новее LAST_MSG_IDS msgs = await client.get_messages(chat_id, min_id=LAST_MSG_IDS[chat_id], limit=50) if not msgs: await asyncio.sleep(0.5) continue # Обновляем счетчик LAST_MSG_IDS[chat_id] = max(m.id for m in msgs) for msg in msgs: text = msg.text if not text: continue # --- БЛОК ЛОКАЛЬНОЙ ФИЛЬТРАЦИИ --- if len(text.split()) < 6: continue # Отсекаем короткий флуд if re.search(r'(https?://|t\.me|www\.)', text.lower()): continue # Отсекаем спам ссылками # Проверка стоп-слов, ключей и интентов... # (Если всё ок -> отправляем requests.post в n8n) except Exception as e: logging.debug(f"Ошибка чтения чата {chat_id}: {e}") await asyncio.sleep(1) # Защита от FloodWait await asyncio.sleep(30) # Пауза перед новым кругом
Жадность vs Экономия: Роль LLM в архитектуре
Когда я починил транспорт, возник вопрос фильтрации. Я решил провести эксперимент: убрал локальные ключи на Python и стал отправлять в n8n вообще все сообщения длиннее 5 слов без ссылок. Идея была в том, чтобы LLM (GPT-4o-mini) сама искала скрытые боли клиентов (подход Data Lake).
Результат: n8n начал разрываться от вебхуков каждые полчаса. Люди в чатах обсуждали страховки, ругали поддержку, болтали о жизни. Нейронка честно отсекала их (is_lead: false), но я начал впустую жечь лимиты Executions в n8n и токены OpenAI.
Итоговая (идеальная) архитектура — Двойной фильтр:
Контур 1 (Грубое сито на Python): Active Pull выкачивает сообщения. Python локально проверяет длину (>5 слов), отсутствие ссылок, отсутствие стоп-слов (уборка, шабашка, дизайнер) и наличие связки Ключ + Интент (например, учет + помогите). Это бесплатно и отсекает 99% мусора на сервере.
Контур 2 (Тонкий скальпель на n8n + LLM): В n8n прилетают только подозрительно похожие на лид тексты. Там их встречает LLM со строгим JSON-промптом на английском языке (для экономии токенов). Нейросеть понимает контекст: если человек пишет "помогите настроить учет" — это лид. Если пишет "как вы справляетесь с учетом, я сам настроил эксель" — это просто болтовня, LLM дает отбой.
Вывод
Не доверяйте events.NewMessage при промышленном парсинге с нагруженных аккаунтов. Telegram без предупреждения режет Push-события для пассивных сессий. Переход на Active Pull с грамотными задержками (sleep) решает проблему «слепоты» парсера на 100%. А связка локальных регулярных выражений с LLM дает идеальный баланс между стоимостью инфраструктуры и качеством лидов.
P.S. Автоматизация имеет смысл только тогда, когда она работает стабильно и не сжигает бюджет на пустые API-вызовы.
Если вашему бизнесу нужна разработка отказоустойчивых парсеров, умных ботов или внедрение LLM в рабочие процессы через n8n — пишите мне в личку Telegram, обсудим архитектуру и цифры.
Mox
Я не совсем понял что за push. Насколько я знаю есть 2 режима бота - pull и webhook.
Для production, вроде, рекомендуют настраивать webhook. Дальше вроде проблем быть не должно - если ваш сервер выдерживает такое количество обращений к хуку.
https://gramio.dev/updates/webhook
Или я где-то неправ?
GoldGoblin
Думаю используется не бот api а client api
chernyaevi Автор
Верно — используется именно Client API (MTProto / Telethon), поэтому логика работы там совершенно другая и вебхуков нет
chernyaevi Автор
Вы абсолютно правы, но только в контексте стандартного Telegram Bot API (когда бот создается через @BotFather). Там действительно есть getUpdates (pull) и Webhooks. Но в статье речь идет о парсинге чужих чатов и каналов конкурентов. Обычного бота туда никто не пустит, поэтому мы используем MTProto API (библиотека Telethon) — то есть работаем от лица обычного пользовательского аккаунта (юзербота). У клиентского MTProto API нет вебхуков. Там клиент держит постоянное TCP-соединение с серверами Telegram, и сервер сам «пушит» (отправляет) новые события в этот сокет. Именно этот сокет и слушает декоратор events.NewMessage. И именно эту отправку событий (push) Telegram в целях экономии ресурсов отключает для пассивных пользовательских аккаунтов, если они состоят в тысячах групп. Поэтому пришлось переходить на принудительный опрос (Active Pull) через get_messages.