Мне понадобился двухконтурный парсер 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-события для аккаунтов, которые:

  1. Состоят в огромном количестве супергрупп.

  2. Являются «пассивными» (не пишут сообщения, не скроллят ленту с официального клиента).

  3. Держат чаты в Муте (Mute) или Архиве.

Telegram просто экономит ресурсы своих серверов и перестает пушить обновления в Telethon. В тестовой группе я сам писал сообщения — чат был «горячим», пуши шли. В чужих чатах я был пассивным наблюдателем, и Telegram перекрыл мне кран.

Инженерное решение: Смена парадигмы на Active Pull

Раз Telegram не хочет присылать мне события, я буду забирать их сам. Почесал репу, полностью выкинул декоратор events.NewMessage() и перешел на архитектуру Active Pull (Пылесос).

Как это работает:

  1. Создаю словарь LAST_MSG_IDS = {}, где храню ID последнего прочитанного сообщения для каждого чата.

  2. Запускаю бесконечный цикл while True.

  3. Каждые 30 секунд скрипт итерируется по списку целевых чатов и делает точечный запрос: client.get_messages(chat_id, min_id=LAST_MSG_IDS[chat_id], limit=50).

  4. Я принудительно высасываю всё, что появилось нового, обновляю счетчик 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. Контур 1 (Грубое сито на Python): Active Pull выкачивает сообщения. Python локально проверяет длину (>5 слов), отсутствие ссылок, отсутствие стоп-слов (уборка, шабашка, дизайнер) и наличие связки Ключ + Интент (например, учет + помогите). Это бесплатно и отсекает 99% мусора на сервере.

  2. Контур 2 (Тонкий скальпель на n8n + LLM): В n8n прилетают только подозрительно похожие на лид тексты. Там их встречает LLM со строгим JSON-промптом на английском языке (для экономии токенов). Нейросеть понимает контекст: если человек пишет "помогите настроить учет" — это лид. Если пишет "как вы справляетесь с учетом, я сам настроил эксель" — это просто болтовня, LLM дает отбой.

Вывод

Не доверяйте events.NewMessage при промышленном парсинге с нагруженных аккаунтов. Telegram без предупреждения режет Push-события для пассивных сессий. Переход на Active Pull с грамотными задержками (sleep) решает проблему «слепоты» парсера на 100%. А связка локальных регулярных выражений с LLM дает идеальный баланс между стоимостью инфраструктуры и качеством лидов.

P.S. Автоматизация имеет смысл только тогда, когда она работает стабильно и не сжигает бюджет на пустые API-вызовы.


Если вашему бизнесу нужна разработка отказоустойчивых парсеров, умных ботов или внедрение LLM в рабочие процессы через n8n — пишите мне в личку Telegram, обсудим архитектуру и цифры.

Комментарии (4)


  1. Mox
    29.06.2026 08:08

    Я не совсем понял что за push. Насколько я знаю есть 2 режима бота - pull и webhook.
    Для production, вроде, рекомендуют настраивать webhook. Дальше вроде проблем быть не должно - если ваш сервер выдерживает такое количество обращений к хуку.

    https://gramio.dev/updates/webhook

    Или я где-то неправ?


    1. GoldGoblin
      29.06.2026 08:08

      Думаю используется не бот api а client api


    1. chernyaevi Автор
      29.06.2026 08:08

      Верно — используется именно Client API (MTProto / Telethon), поэтому логика работы там совершенно другая и вебхуков нет


    1. chernyaevi Автор
      29.06.2026 08:08

      Вы абсолютно правы, но только в контексте стандартного Telegram Bot API (когда бот создается через @BotFather). Там действительно есть getUpdates (pull) и Webhooks. Но в статье речь идет о парсинге чужих чатов и каналов конкурентов. Обычного бота туда никто не пустит, поэтому мы используем MTProto API (библиотека Telethon) — то есть работаем от лица обычного пользовательского аккаунта (юзербота). У клиентского MTProto API нет вебхуков. Там клиент держит постоянное TCP-соединение с серверами Telegram, и сервер сам «пушит» (отправляет) новые события в этот сокет. Именно этот сокет и слушает декоратор events.NewMessage. И именно эту отправку событий (push) Telegram в целях экономии ресурсов отключает для пассивных пользовательских аккаунтов, если они состоят в тысячах групп. Поэтому пришлось переходить на принудительный опрос (Active Pull) через get_messages.