Российские технологии и цифровая экономика
ПоискRSS

Статистика подписчиков канала Max: как собрать точный ежедневный ряд данных

6 минут чтения

Статистика подписчиков канала MAX: как собрать ежедневный ряд и не принять сбой за реальные данные

У мессенджера MAX пока нет открытого API, который предоставлял бы полноценную статистику каналов. Числа встречаются в каталогах и агрегаторах, однако это чужие измерения, обновляемые нерегулярно: последний доступный снапшот в рассматриваемом случае был датирован 10 июля. Поэтому для собственной аналитики каналов MAX потребовался независимый ежедневный ряд - график, который показывал бы не приблизительную оценку, а результат конкретного замера.

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

Где находятся данные

Сайт max.ru построен на SvelteKit. Страница каждого публичного канала обращается к собственному data-эндпоинту. Авторизация и токен не требуются: сервер отвечает с кодом HTTP 200, а размер ответа составляет примерно 450 байт.

Однако данные представлены не обычным вложенным JSON. SvelteKit сериализует их в плоский массив с дедупликацией. Объекты содержат не сами значения, а индексы элементов из того же массива. Например, запись `{"channel":2}` означает, что описание канала находится в элементе с индексом 2. Аналогично, конструкция вроде `{"title":3, "participantsCount":6}` указывает, где искать название и число подписчиков.

Поэтому простой поиск строки `participantsCount` неприменим: имя поля может встретиться в схеме раньше фактического значения. Парсер должен последовательно разрешать индексы и только после этого собирать итоговую запись.

Есть и организационное ограничение: эндпоинт пригоден для обогащения уже известного списка, но не для поиска новых каналов. Публичного каталога, из которого можно было бы автоматически получить все хендлы, у max.ru нет.

Первая проблема: незаметное изменение формата

Примерно 8 июля структура ответа изменилась. Раньше объект канала находился непосредственно в `data[0]`, а затем появился дополнительный уровень:

`data[0] = {"linkInfo":1, ...}` → `data[1] = {"channel":2}`.

Старый парсер продолжил работать без исключений, но перестал находить нужную сущность и начал возвращать `None`. Ночной запуск выглядел формально успешным: `ok = 0 / 95 262`, `not_found = 95 262`, `error = 0`, код завершения - 0. Три ночи подряд система не сигнализировала о проблеме, хотя весь ряд фактически замер.

Из этого следуют два важных правила. Во-первых, необходимо поддерживать обе версии разбора: старая структура может сохраняться в кэшах, зеркалах или отдельных ответах. Во-вторых, контроль должен учитывать не только наличие ошибок, но и форму результата. Теперь комбинация `ok = 0` при непустом `targeted` считается аварийной и завершается специальным кодом с уведомлением.

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

Вторая проблема: боты не являются каналами

У ботов используется другая структура ответа:

`{"bot": i}` → `data[i] = {id, name, avatar, description}`.

Поля `participantsCount` там нет, поскольку публичного счётчика подписчиков у ботов MAX не предоставляет. Если без дополнительной логики искать только узел `channel`, любой бот будет классифицирован как отсутствующий.

Именно это произошло во время первой проверки: при запуске с `--entity-type bot` система получила `ok = 0`, `not_found = 20`. В полном ночном сценарии такой подход мог бы пометить как "не найденные" 5 803 реально существующих бота.

Ветку для ботов разместили после нормализации `linkInfo`, а тип сущности стали определять непосредственно по ответу. Жёстко заданный тип `channel` опасен: он способен одномоментно переписать классификацию всех ботов.

Третья проблема: `not_found` скрывал несколько причин

Самой дорогой ошибкой оказалось правило: если парсер возвращает `None`, записывать `not_found`. На практике за одним статусом скрывались как минимум три разных события:

- канал действительно удалён или не существует;
- сервер изменил формат ответа;
- запрос попал под ограничение частоты.

Последний вариант подтверждался наблюдениями. MAX применяет кумулятивный мягкий лимит на IP-адрес и при достижении порога может вернуть HTTP 200 с телом, которое не удаётся нормально разрешить. При этом нет ни 429, ни капчи, ни редиректа.

В результате ночной показатель `not_found` колебался от 13 923 до 30 335 при неизменном `targeted = 82 345`. Это можно было ошибочно прочитать как массовое исчезновение каналов, хотя имена оставались непустыми у всех 82 345 объектов.

Проблема усугублялась тем, что даже пропущенный запрос обновлял `max_checked_at`. Временной ряд получал искусственную дыру, а детектор аномалий интерпретировал её как `flat_then_jump`. Иными словами, система публично показывала последствия собственных сбоев как признаки подозрительного роста.

Почему первые критерии проверки не сработали

Сначала валидным считался любой ответ, где JSON успешно разбирался и присутствовала нода с типом `data`. Но у действительно несуществующего канала такой ноды нет: вместо неё приходит HTTP 200 с телом размером около 98 байт. Поэтому живые удалённые каналы ошибочно попадали в категорию "сервер нас не пропустил" и не исключались из дальнейших расчётов.

Следующая попытка заключалась в добавлении признака "явный 404 внутри ответа 200". Однако и этот подход оказался недостаточным. При троттлинге сервер отдавал формально корректный конверт, а различие проявлялось только при сравнении запросов с разных IP.

Четыре прокси, обращавшиеся почти одновременно к одному URL, получили разные результаты. Это показало, что код ответа сам по себе не определяет качество измерения. При ограничении со стороны сервера верхний уровень `data[0]` мог оставаться валидным словарём с `linkInfo`, тогда как целевой узел оказывался пустым.

Рабочая схема должна отвечать не на вопрос "получили ли мы HTTP 200", а на более узкий: удалось ли извлечь согласованный объект нужного типа и получить именно те поля, которые необходимы для измерения.

Как строить надёжную статистику

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

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

Не менее важна независимая валидация ряда. Сервис статистики каналов MAX должен контролировать долю успешных ответов, число новых `not_found`, распределение кодов и размеров тел, а также совпадение результатов между прокси. Резкое изменение одного из этих параметров обычно говорит не о поведении аудитории, а о техническом событии.

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

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

Прокрутить вверх