Узнайте, почему равномерные случайные задержки — ловушка, и как экспоненциальное распределение с отрубливанием хвоста помогает имитировать человеческое поведение. Начните писать безопасные скрипты уже сегодня.
Детектор поведения X (Twitter) смотрит не только на то, что делает аккаунт, но и на когда. Два верных признака бота:
Двигатель задержек должен выдавать интервалы, которые на графике выглядят как человеческая активность: иногда быстрые, иногда медленные, изредка паузы на минуты, никогда не предсказуемые.
Очевидный подход: выбрать случайную задержку между min и max.
delay = random(30, 90) # секундЭто неправильно по тонкой причине. Равномерное распределение даёт плоскую гистограмму — равная вероятность любой задержки в диапазоне. Но человеческая активность не плоская. Когда человек скроллит X, его действия группируются: быстрые серии ответов за минуту, потом пауза, потом ещё серия. Распределение интервалов между действиями человека — тяжелохвостое: много коротких промежутков, несколько длинных, а не плоская полоса.
Плоское распределение задержек — это свой собственный отпечаток. Оно не соответствует реальности, и детектор, смотрящий на форму распределения, а не только на среднее, засечёт его.
Интервалы между действиями человека хорошо моделируются экспоненциальным распределением — тем же, что описывает радиоактивный распад. Его плотность вероятности:
f(x) = λ * e^(-λx)
У экспоненты два свойства, совпадающих с поведением человека:
Сэмплирование из экспоненты даёт тот самый пачечный, тяжелохвостый паттерн, которого нет у равномерного распределения.
import random
import math
def exponential_delay(mean_ms: float) -> float:
"""Обратное преобразование для экспоненциального распределения."""
u = random.random() # равномерное на [0, 1)
return -math.log(1 - u) * mean_msЕдинственный параметр mean_ms управляет средней задержкой. Форма — короткие промежутки с редкими длинными — получается автоматически.
У чистой экспоненты одна проблема: тяжёлый хвост слишком тяжёлый. При mean_ms = 30000 (средняя 30 с) иногда будут задержки по 3, 4 и даже 5 минут. Это не опасно (люди так паузируют), но резко падает пропускная способность — модуль половину времени тратит на редкие длинные паузы.
Решение — обрезать хвост, сохранив форму:
def capped_exponential_delay(mean_ms: float, max_ms: float) -> float:
raw = exponential_delay(mean_ms)
return min(raw, max_ms)Обрезание сохраняет форму (много коротких, мало длинных) в нужном диапазоне и отсекает патологически длинные паузы. Ограничение обычно ставят в 3–4 раза выше среднего, чтобы хвост оставался достаточно тяжёлым.
Собирая всё вместе, наш двигатель задержек:
import random
def next_action_delay(base_mean_ms: float, max_ms: float) -> float:
"""Возвращает задержку в миллисекундах."""
# Базовая задержка из экспоненты с обрезанием
delay = capped_exponential_delay(base_mean_ms, max_ms)
# Иногда имитируем отвлечение человека: ~10% шанс умножить задержку на 2-5
if random.random() < 0.10:
delay *= 2 + random.random() * 3
# Избегаем круглых чисел
delay = deround(delay)
return delay
def deround(ms: float) -> float:
"""Отталкивает задержку от круглых значений."""
round_seconds = [10, 15, 20, 30, 45, 60, 90, 120]
for s in round_seconds:
target = s * 1000
if abs(ms - target) < 500:
# Сдвигаем на случайную величину 800-2000 мс в любую сторону
nudge = (800 + random.random() * 1200) * (1 if random.random() < 0.5 else -1)
return max(1000, ms + nudge)
return msВставка «отвлечения» и «дероундинг» — это детали, которые отличают проходимый двигатель от хорошего.
Даже с отличным распределением, если задержки часто попадают на 30.0, 60.0, 45.0 секунд — само округление становится сигналом. Таймеры на основе фиксированных пауз дают круглые числа; человеческое время реакции — нет.
Функция deround сдвигает любую задержку, оказавшуюся в пределах 500 мс от круглого числа, на случайную величину. Это мелочь, но мелочи накапливаются. Аккаунт, у которого каждая задержка — круглое число секунд, выглядит как таймер. Аккаунт с задержками 32.4 с, 47.1 с, 28.9 с — как человек.
Задержки не работают в вакууме — они ограничены окном работы аккаунта. Если следующая задержка выталкивает действие за конец окна, модуль должен решить: сжать задержку или отложить на завтра.
Мы откладываем. Сжатие задержки для попадания в окно создаёт неестественную кластеризацию действий у границ окна — именно тот краевой эффект, который замечает детектор. Вместо этого, если действие выпадает за окно, мы ждём следующего открытия:
import asyncio
def is_within_window(next_at: float, window: tuple) -> bool:
"""Проверяет, попадает ли время в окно (start, end)."""
return window[0] <= next_at <= window[1]
def next_window_open(window: tuple) -> float:
"""Возвращает время открытия следующего окна (завтра)."""
# Упрощённо: прибавляем сутки к start
return window[0] + 86400000
async def schedule_next_action(delay_ms: float, window: tuple) -> bool:
next_at = asyncio.get_event_loop().time() * 1000 + delay_ms
if is_within_window(next_at, window):
await asyncio.sleep(delay_ms / 1000)
return True
# Ждём следующего окна
reopen_at = next_window_open(window)
await asyncio.sleep((reopen_at - asyncio.get_event_loop().time() * 1000) / 1000)
return TrueРезультат: действия происходят нерегулярно, по-человечески, внутри настроенных рабочих часов, а аккаунт молчит вне их. Никакой активности в 3 часа ночи, никакой кластеризации в 21:59. Оба сигнала мы избегаем.
Не все действия должны иметь одинаковый профиль задержки. Ответ незнакомцу и публикация поста имеют разный естественный ритм:
delay_profiles = {
"reply": {"mean_ms": 45000, "cap_ms": 180000, "distraction_rate": 0.10},
"post": {"mean_ms": 0, "cap_ms": 0, "distraction_rate": 0}, # планируется, без задержки
"follow": {"mean_ms": 90000, "cap_ms": 600000, "distraction_rate": 0.15},
"dm": {"mean_ms": 120000, "cap_ms": 600000, "distraction_rate": 0.20},
"repost": {"mean_ms": 60000, "cap_ms": 300000, "distraction_rate": 0.10},
}Ответы относительно быстрые (человек отвечает сериями). Подписки медленнее (человек раздумывает). ЛС — самые медленные и вариативные (человек пишет, перечитывает, колеблется). Настройка профиля под тип действия делает общий паттерн активности более связным: человек, который отвечает каждые 45 с, но пишет ЛС каждые 2 минуты, правдоподобен; тот, кто делает и то и другое с интервалом 45 с — нет.
Мы A/B-тестировали двигатель задержек против простого равномерного на одинаковых аккаунтах. За 8 недель:
Разница была не в величине задержки (оба двигателя имели схожее среднее), а в форме распределения. Плоские распределения помечаются; тяжелохвостые, дераундженные — нет. Математика — это разница.
Двигатель задержек — это неприглядная инфраструктура. Никто не открывает приложение и не думает: «Ух ты, отличное распределение задержек». Но это тот слой, который определяет, будут ли все остальные функции — AI-ответы, планирование, движок персоны — работать на живом аккаунте или забаненном. Правильная математика — это цена входа.
Хочешь закрепить знания на практике?
Решай задачи на Algolit — интерактивная платформа для обучения
Начать бесплатно →