СправочникИнструменты › Прокси в ZennoPoster

Прокси в ZennoPoster: настройка шаблона, профилей и привязка адреса к потоку

Баннер iprazon: приватный серверный пул IPv4 и SOCKS5 для прогонов в ZennoPoster

ZennoPoster это среда автоматизации действий в браузере и по HTTP под Windows. Шаблон собирается кубиками в ProjectMaker, сохраняется отдельным файлом проекта, а сама программа запускает этот файл в несколько потоков одновременно. Каждый поток поднимает собственный экземпляр браузера или собственный HTTP-клиент, и выход в сеть у него идёт с адреса машины до тех пор, пока в проекте не сказано иначе. Прокси здесь задаёт точку выхода конкретному потоку, и от того, в каком месте проекта эта точка задана, зависит вся дальнейшая механика прогона.

Разбор ниже построен от шаблона к потоку. Сначала я показываю четыре места, где в ZennoPoster вносится выходной адрес, и чем они отличаются по охвату. Дальше идёт встроенный менеджер списка с его флажками и чекером, форматы строк, которые понимает каждое поле, приём для удержания одного выхода на всё время работы шаблона, передача адреса через входные настройки задачи, разбор ошибки авторизации внутри проекта и сбор статистики отказов прямо из кубиков. Цифры я привожу свои, снятые с прогонов на собственных шаблонах.

Где в проекте ZennoPoster задаётся выходной адрес

Мест ровно четыре, и путаница между ними даёт большую часть вопросов от новичков. Первое место это общий список программы: адреса лежат в менеджере ZennoPoster, флажок в свойствах задачи разрешает шаблону их брать, и поток получает строку в момент старта. Второе место это кубик установки прокси внутри самого шаблона: он срабатывает в той точке схемы, куда я его поставил, и меняет выход по ходу выполнения. Третье место это входные настройки задачи, где оператор вписывает адрес или ссылку на список руками перед запуском. Четвёртое место это C#-вставка с вызовом instance.SetProxy, которая подставляет строку из своей переменной.

Схема четырёх мест проекта, где шаблону задаётся выходной адрес

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

Место в проектеОхватМомент подстановкиЧто видно в логе задачи
Менеджер списка программыВсе потоки задачиСтарт потокаСтрока выдачи адреса перед первым кубиком
Кубик установки проксиВетка шаблонаМомент выполнения кубикаОтдельная запись о смене выхода
Входные настройки задачиОдна задача целикомПеред стартом, рукамиЗначение переменной в шапке лога
Вставка instance.SetProxyЭкземпляр браузера потокаЛюбая точка схемыТо, что я сам записал в лог

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

Ещё одна тонкость касается HTTP-кубиков. Они ходят мимо браузера, своим клиентом, и адрес им передаётся отдельным параметром прямо в вызове. Установка выхода для браузера на них не распространяется совсем. Шаблон, где половина шагов идёт браузером, а половина запросами, легко получает два разных выхода в одном потоке, и такое расхождение видно сразу по журналу принимающей стороны.

Встроенный менеджер списка: как программа раздаёт адреса потокам

Менеджер живёт в настройках самой программы, отдельной вкладкой. Список туда попадает тремя путями: вставкой текста, загрузкой файла и подпиской на ссылку. Третий путь я считаю рабочим, потому что список пула обновляется в реальном времени, и подписка снимает всю ручную работу по переносу строк. Программа перечитывает ссылку по расписанию, я ставлю интервал в 30 минут.

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

Чекер внутри менеджера проверяет строки перед выдачей. У него три поля: адрес проверки, таймаут и периодичность. Адрес проверки я ставлю свой, простую страницу, которая отвечает текстом с адресом обращения. Таймаут держу на 8 секундах: строка, которая не отвечает за это время, в прогоне всё равно даст таймаут на первом же шаге. Периодичность ставлю в 15 минут, чаще смысла нет.

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

Отдельно про поведение менеджера при обрыве. Когда строка перестаёт отвечать посреди прогона, программа помечает её нерабочей и выдаёт потоку следующую. Для HTTP-задач это правильно и незаметно. Для браузерных это разрыв сессии на середине сценария: страница уже открыта, куки собраны, а выход внезапно другой. Именно поэтому серьёзные браузерные шаблоны я на менеджер не завязываю и раздаю адреса своим кодом.

Форматы подачи списка и что принимает каждое поле

Пул выдаётся в двух видах сразу, IP:PORT и IP:PORT:LOGIN:PASS, ссылкой или файлом. Первый вид работает после привязки своего адреса в кабинете, второй по паре логина. ZennoPoster понимает оба, но пишутся они в разных местах по-разному, и на этом спотыкаются чаще всего.

менеджер списка программы, одна строка на адрес:
203.0.113.24:8080
203.0.113.25:8080:zp7:Kq3sT9vb

то же для SOCKS5, протокол указывается меткой списка:
203.0.113.26:1080
203.0.113.27:1080:zp7:Kq3sT9vb

В коде и в кубиках строка собирается иначе: протокол пишется префиксом, пара логина уходит перед адресом через собаку. Такую форму принимают и instance.SetProxy, и параметр прокси у HTTP-кубика.

instance.SetProxy("http://203.0.113.24:8080");
instance.SetProxy("socks5://zp7:Kq3sT9vb@203.0.113.27:1080");
Формат строкиГде принимаетсяНа что обратить внимание
IP:PORTМенеджер, входные настройки, вставкиРаботает после привязки адреса машины в кабинете
IP:PORT:LOGIN:PASSМенеджер, файл спискаВ коде эта форма разбирается вручную по двоеточиям
http://IP:PORTSetProxy, HTTP-кубикПрефикс обязателен, иначе браузер уйдёт напрямую
socks5://LOGIN:PASS@IP:PORTSetProxy, HTTP-кубикПара логина стоит перед адресом
Ссылка на списокМенеджер, подпискаПрограмма перечитывает адрес по расписанию

Разбор строки из четырёх частей в форму с префиксом занимает одну строку кода, и эту функцию я держу в общем коде проекта.

string ToUri(string raw, string scheme) {
    var p = raw.Split(':');
    return p.Length == 4
        ? scheme + "://" + p[2] + ":" + p[3] + "@" + p[0] + ":" + p[1]
        : scheme + "://" + p[0] + ":" + p[1];
}

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

Один выход на всё время работы шаблона в одном потоке

Это главный вопрос темы, и я разберу его подробно. Задача формулируется так: поток номер 7 стартовал, взял строку списка и должен ходить через неё до самого конца сценария, включая все повторы и все перезапуски внутри задачи. Менеджер программы сам по себе такого не гарантирует: он раздаёт строки по кругу, и после разрыва поток спокойно получит другую.

Сравнение общего списка на все потоки и привязки выхода к профилю потока

Приём, которым я пользуюсь, держится на общей памяти программы. У ZennoPoster есть глобальные переменные, видимые всем потокам сразу, и я храню там пары вида «номер потока и строка списка». Стартовый кубик проекта смотрит в эту память: пара уже есть, поток берёт свою строку, пары нет, поток вычитывает следующую из файла и записывает пару обратно. Повторный запуск того же потока попадает в готовую запись и получает тот же выход, что был у него час назад.

// стартовый кубик: выдать потоку его строку и запомнить пару
string tid  = project.Variables["threadId"].Value;
string key  = "zp_pool_" + tid;
string line = Global.Variables.GetVariable("pool", key);

if (string.IsNullOrEmpty(line)) {
    lock (SyncObjects.ListSyncer) {
        var all = System.IO.File.ReadAllLines(project.Directory + @"\pool.txt");
        int idx  = int.Parse(tid) % all.Length;
        line = all[idx].Trim();
    }
    Global.Variables.SetVariable("pool", key, line);
    project.SendInfoToLog("поток " + tid + " получил строку " + line.Split(':')[0], true);
}

project.Variables["proxy"].Value = line;
instance.SetProxy(ToUri(line, "socks5"));

Блокировка вокруг чтения файла нужна обязательно. Сорок потоков стартуют почти одновременно, и без синхронизации две трети из них прочитают файл в один и тот же момент, получив одинаковый индекс. Я потерял на этом полдня, пока не сообразил посмотреть, сколько разных выходов реально участвует в прогоне: их было четыре при сорока потоках.

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

Схема раздачиЧто происходит при разрывеГде видно причину отказа
Список менеджера по кругуПоток получает следующую доступную строкуПричина размазана по всей задаче
Список менеджера с запретом повтораПоток ждёт освобождения строкиПростой потока в общем счётчике
Пара в общей памяти по номеру потокаПоток возвращается к своей строкеОтказ виден по конкретной паре
Строка из входных настроекЗадача целиком идёт одним выходомОдна строка журнала на всю задачу

Список под такую схему я держу заметно шире числа потоков: подмена сбойной строки тогда занимает секунды и не требует пересборки пар в памяти. Обычно я беру приватные адреса под шаблоны ZennoPoster с запасом в полтора раза от числа экземпляров браузера, и запас этот расходуется как раз на замены по ходу ночи.

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

Передача адреса через входные настройки задачи

Входные настройки это переменные проекта, помеченные особым флагом в ProjectMaker. Они видны в свойствах задачи внутри ZennoPoster и правятся руками без открытия шаблона. Я выношу туда всё, что меняется от прогона к прогону, и адрес пула стоит там первым пунктом.

Входные настройки задачи
  pool_url        : https://.../list.txt
  pool_scheme     : socks5
  threads_per_ip  : 3
  retry_on_407    : 2
  fails_csv       : D:\zp\logs\fails.csv

Читаются они обычным обращением к переменной, никакой отдельной механики там нет. Ссылку на список я скачиваю первым кубиком в файл рядом с проектом и дальше работаю с файлом, потому что сорок потоков, дёргающих ссылку одновременно, дают лишнюю нагрузку на пустом месте.

string url = project.Variables["pool_url"].Value;
string dst = project.Directory + @"\pool.txt";

lock (SyncObjects.FileSyncer) {
    var fi = new System.IO.FileInfo(dst);
    if (!fi.Exists || (DateTime.Now - fi.LastWriteTime).TotalMinutes > 30)
        new System.Net.WebClient().DownloadFile(url, dst);
}

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

Второй способ передачи это прямое значение. Оператор вписывает одну строку в поле входной настройки, шаблон берёт её и работает только через неё, игнорируя и менеджер, и файл. Такой режим я включаю на отладке: когда нужно проверить поведение шаблона на конкретном выходе, лишние источники адресов только мешают. Флаг режима тоже стоит во входных настройках, обычной единицей.

Третий способ пригодится тем, кто запускает задачи из очереди сторонней программой. ZennoPoster принимает значения входных настроек прямо в команде запуска, и адрес приходит туда вместе с остальными параметрами задачи. Я так делаю, когда очередь заданий формируется на сервере и каждой задаче нужен свой выход. Схема стройная: сервер знает, какая строка кому досталась, и в его журнале это записано раньше, чем шаблон начал работу.

Ошибка авторизации внутри шаблона: разбор кода 407

Код 407 означает, что прокси-сервер ждёт пару логина и не получил её. В ZennoPoster эта ошибка проявляется тремя разными способами, и по внешнему виду они не похожи друг на друга. Браузерный кубик показывает пустую страницу и пишет в лог отказ навигации. HTTP-кубик возвращает тело ответа с текстом об авторизации. C#-вставка бросает исключение и роняет всю ветку.

Причин у кода 407 у меня набралось четыре. Первая: строка вида IP:PORT попала в шаблон, который работает по паре логина. Вторая: пара логина есть, но при сборке строки потерялся префикс протокола, и браузер отправил её как обычный адрес. Третья: в поле привязки в кабинете стоит другой адрес машины, и сервер отказывает уже по нему. Четвёртая: пара логина обновилась в кабинете, а файл списка на диске остался старым.

int tries = 0;
int limit = int.Parse(project.Variables["retry_on_407"].Value);

while (tries <= limit) {
    instance.ActiveTab.Navigate(project.Variables["target"].Value);
    if (instance.ActiveTab.HttpStatusCode != 407) break;

    Note("407", line);                       // счётчик отказов, см. ниже
    line = NextLine(project);                // следующая строка того же файла
    Global.Variables.SetVariable("pool", key, line);
    instance.SetProxy(ToUri(line, project.Variables["pool_scheme"].Value));
    tries++;
}

Логика повтора у меня жёсткая: две попытки и остановка ветки. Третья попытка при коде 407 бессмысленна, потому что причина сидит в настройке, и перебор строк её не вылечит. Остановка с записью в журнал даёт мне утром точный адрес проблемы вместо тысячи одинаковых строк лога.

Отдельно предупрежу про порядок вызовов. Установку выхода нужно делать до открытия первой страницы в экземпляре браузера. Вызов SetProxy на уже открытой вкладке применяется к следующей навигации, и первый запрос успевает уйти со старой точки. В шаблонах, где кубик установки прокси стоял вторым после запуска браузера, я находил ровно одно постороннее обращение с адреса машины на каждый прогон. Кубик переехал на первую позицию схемы, обращения исчезли.

Профили потоков и связка с антидетект-браузером

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

string dir  = project.Directory + @"\profiles\" + tid;
string file = dir + @"\profile.zpprofile";

System.IO.Directory.CreateDirectory(dir);
if (System.IO.File.Exists(file)) instance.ProfileLoadFromFile(file, true, true, true, true, true, true);
// ... сценарий ...
instance.ProfileSaveToFile(file, true, true, true, true, true, true);

Связка «номер потока, строка списка, папка профиля» держится вместе на всём прогоне и переживает перезапуск задачи. Профиль поднимается с диска, строка приходит из общей памяти, экземпляр браузера получает обе части до первой навигации. Такой порядок я считаю обязательным для любого шаблона, где сценарий идёт дольше пары минут.

Часть работы уходит во внешний антидетект-браузер, когда шаблону нужен отпечаток, которого встроенный экземпляр не даёт. ZennoPoster подключается к такому браузеру по локальному порту отладки и водит его теми же кубиками. Выход в этом случае прописывается в настройках профиля внешнего браузера, и синхронизировать две настройки приходится вручную: строка в файле пула и строка в карточке профиля должны совпадать. Под такие связки я держу отдельный список и беру прокси для профилей Dolphin Anty с той же выдачей списком, чтобы формат строки в обеих программах был одинаковым.

Число профилей на машину я ограничиваю сверху по памяти. Каждый экземпляр браузера съедает от 200 до 400 мегабайт, и сорок экземпляров на машине с 32 гигабайтами уже подходят к пределу. HTTP-шаблоны такой нагрузки не создают вовсе, там сорок потоков занимают меньше гигабайта, поэтому смешанные прогоны я развожу по разным задачам с разным числом потоков.

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

Потоки в ZennoPoster считаются на двух уровнях. Первый уровень это сама программа: у задачи есть поле максимального числа потоков, и общая сумма по всем активным задачам ограничена лицензией. Второй уровень это пул: доступ выдаётся с лимитом одновременных подключений, и обычный пакет держит 1000 потоков, корпоративный поднимает планку до 3000.

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

ПараметрОбычный пакетКорпоративный пакет
Потоков одновременно1000до 3000
Привязок адресов в пакете22
Потоков на привязку при двух активных500до 1500
Сколько занимает браузерный прогон на 40 потоков4 процентаоколо 1,3 процента
Сколько занимает HTTP-прогон на 200 потоков20 процентовоколо 7 процентов

Две вещи про лимит стоит держать в голове заранее. Пакеты по потокам между собой не складываются: два подряд оформленных пакета оставляют планку на прежней отметке в 1000 потоков. И при двух активных привязках лимит делится между ними пополам, по 500 на каждую сторону. Для одной машины с ZennoPoster это цифры с огромным запасом: даже тяжёлый браузерный прогон на сорок экземпляров занимает от них считанные проценты, и упирается такая машина в память задолго до любых сетевых пределов.

Запас я расходую иначе, и вот тут он действительно пригождается. На одной привязке у меня работает ZennoPoster с браузерными задачами, на второй сервер с HTTP-шаблонами и парсерами, и обе стороны живут в своих 500 потоках без взаимного влияния. Трафик на всех пакетах безлимитный, поэтому загрузка страниц с картинками и выгрузка файлов внутри сценариев на расчёт вообще не влияет. Считать нужно только одновременность.

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

Статистика отказов: счётчики прямо из кубиков шаблона

Журнал ZennoPoster показывает ход каждой задачи построчно, и на прогоне в несколько тысяч задач читать его целиком невозможно. Поэтому счётчики я веду сам, из шаблона, в отдельный файл с разделителями. Функция записи занимает шесть строк и вызывается из каждой ветки отказа.

void Note(string reason, string line) {
    string row = string.Join(";", new [] {
        DateTime.Now.ToString("HH:mm:ss"),
        project.Variables["threadId"].Value,
        line.Split(':')[0],
        reason,
        project.Variables["step"].Value });
    lock (SyncObjects.FileSyncer)
        System.IO.File.AppendAllText(project.Variables["fails_csv"].Value, row + "\n");
}

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

Столбцы причин отказов за один ночной прогон шаблона

Разбор последнего прогона выглядел так. Задач в очереди было 4200, отказов набралось 226, что составляет 5,4 процента. Таймауты соединения дали 96 записей и распределились по всем потокам ровно, без перекоса на конкретные строки. Код 407 дал 54 записи, и все они пришлись на первые двенадцать минут прогона: файл списка на диске оказался старше обновления пары логина в кабинете. Капча на выходе дала 38 записей, разрывы SOCKS5-сессии 21, отказ 403 от принимающей стороны 17.

Причина в файле счётчиковЧто за ней стоитЧто я правлю
timeoutПотоков на строку больше, чем канал отдаётСнижаю threads_per_ip на единицу
407Файл списка разошёлся с настройкой доступаПерекачиваю список по ссылке и рестартую задачу
captchaТемп обращений с точки выхода выше порогаДобавляю паузу в блок навигации
socks_dropСессия оборвалась внутри длинного сценарияВозвращаю поток к своей строке из общей памяти
403Принимающая сторона отказала по отпечаткуПересобираю профиль потока с нуля

Сводка по файлу собирается любой таблицей за минуту, я группирую по причине и по первой части строки. Ценность цифр появляется на второй неделе наблюдений: видно, какая причина растёт, а какая держится фоном. Ровный фон из таймаутов в один-два процента я считаю нормой прогона и не трогаю. Скачок доли одной причины за сутки означает изменение на принимающей стороне, и разбираться нужно именно там.

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

Ночной прогон на 4200 задач от старта до утреннего отчёта

Соберу всё в один порядок на примере обычного ночного прогона. Шаблон браузерный, сценарий на восемь шагов, очередь на 4200 задач, четырнадцать строк списка и по одному потоку на строку.

Вечером я проверяю три вещи: свежесть файла списка, наличие папок профилей и пустой файл счётчиков с заголовком. Стартовый кубик раздаёт пары «поток и строка» и пишет их в лог, поэтому первые полминуты прогона я смотрю на экран: четырнадцать разных точек выхода в логе означают, что блокировка вокруг чтения файла работает. Дальше я запускаю очередь целиком и ухожу.

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

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

Пул на 12 000 активных строк позволяет держать список шире расчёта, и подмена сбойной строки посреди прогона занимает секунды. Автоматическая ротация внутри пула при этом работает в мою пользу: между заходами точка выхода подбирается сама, обращения размазываются по разным адресам, и доля капчи в счётчиках держится у нижней границы. Когда я разворачиваю шаблон на новой машине, я беру адреса для прогонов в ZennoPoster списком в двух форматах сразу, подкладываю файл рядом с проектом и запускаю первую задачу тем же вечером.

Смежные материалы вики дополняют картину с других сторон: разбор Dolphin Anty и подмены отпечатка пригодится, когда сценарий водит внешний браузер, Key Collector и съём частотности показывает ту же арифметику потоков на сборе семантики, а A-Parser и многопоточный сбор закрывает работу без браузера вовсе. Из раздела о сборе данных ближе всего страница про ротацию адресов: там разобрано, как меняется точка выхода между заходами и что это даёт длинным сценариям.