Как парсить Instagram в 2026-м

Как парсить Instagram в 2026-м
Artur Hvalei's Profile Image
Artur Hvalei

Technical Support Specialist, Octo Browser

В прошлой статье мы разобрали парсинг поисковой выдачи Google через Octo Browser и Puppeteer. Теперь поднимем планку: возьмемся за Instagram — платформу с одной из самых жестких антибот-систем в 2026 году. Покажем, какие данные обычно собирают при парсинге и зачем это вообще нужно. Разберем, почему стандартные боты быстро ловят ограничения и баны. Отдельно посмотрим на архитектуру рабочего решения, ошибки, с которыми мы столкнулись на тестах, и способы масштабировать сбор данных без постоянных проблем с аккаунтами. 

Содержание

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

Что и зачем парсят в Instagram

Изучив рынок инструментов и сервисов 2025–2026 гг., можно выделить шесть основных объектов парсинга Instagram:

Данные

Поля

Для чего собирают?

Подписчики аккаунта

username, full_name, verified, is_private, bio

Аудит конкурентов, поиск горячих лидов, изучение аудитории для Meta Ads

Подписки аккаунта

username, full_name, verified, is_private, bio

Анализ целевой аудитории конкурентов и инфлюенсеров

Лайкнувшие пост

username, full_name

Выборка с более высокой вовлеченностью, чем у подписчиков 

Комментаторы 

username, текст комментария

DM-рассылки и анализ тональности

Посты по хештегу/гео

post_url, лайки, caption

Контент-аналитика, поиск UGC

Профиль целиком

bio, posts, ER

Аудит инфлюенсера перед сотрудничеством

Основные сценарии использования:

1. Аудит конкурентов. Можно спарсить подписчиков 3–5 конкурентов, убрать дубли и найти пересечения. Cамые качественные лиды — люди, подписанные на несколько конкурентов сразу.

2. Проверка инфлюенсера/блогера. Перед оплатой коллаборации можно собрать подписчиков и оценить, насколько аудитория живая.

3. DM-рассылки и холодная почта. В нишах SaaS, marketing и e-commerce у 15–35% профилей в bio есть публичный email-адрес.
4. Аудитория Lookalike (похожая аудитория) в Meta Ads. Для ее создания список из имен пользователей (юзернеймов) конвертируется в «посевную» пользовательскую аудиторию (Custom Audience seed). 

5. Прогрев аккаунтов через масслайкинг. Для роста охвата и видимости нормальной активности.

Почему обычные боты палятся в 2026-м

Главное изменение последних лет — Instagram сделал контент скрытым, если нет авторизации в аккаунте. Раньше можно было дергать публичные эндпоинты /?__a=1 без сессии. В 2026-м без авторизации не показывают даже подписчиков public-аккаунта.

Что потребуется для нашего скрипта:

1. Живой залогиненный аккаунт — желательно прогретый, а не только что зарегистрированный. Octo Browser с этим помогает: можно создать аккаунт, авторизоваться вручную, дать ему отстояться и только потом подключать к автоматизации.

2. Каждый аккаунт = отдельный отпечаток. Запускать 20 аккаунтов из одного Chrome — гарантированный бан. В Octo вы можете использовать изолированные профили с разными отпечатками (параметрами WebGL, разрешением экрана, шрифтами, user-agent и т. д.).

3. Прокси на профиль. В идеале мобильные или резидентные. Использовать IP из пула дата-центров крайне рискованно. 

Но даже с правильным отпечатком браузера и надежным прокси puppeteer-бот не выживет без эмуляции человеческих действий. Антибот-системы анализируют:

  • Частотный анализ. Сколько лайков в час, сколько подписок в день, какие интервалы между действиями. Серверная аналитика поймает бота, который лайкает каждые 30 секунд, почти сразу.

  • Отпечаток браузера. Поэтому использование антидетекта критически важно.

  • TLS / network fingerprint. Поэтому важны нормальные прокси и некастомный TLS-стек.

  • Уязвимости библиотек автоматизации (navigator.webdriver, CDP-присутствие, специфичные следы в Error.stack).

Практический вывод: главные причины банов в 2026-м году — это частотные паттерны (слишком много действий за короткое время, слишком ровные интервалы), отпечаток и прокси. Эмуляция человеческих движений мышки, скроллов или набора текста пока необязательна. Но практически эти действия могут использоваться для анализа и начисления очков фрод-скора аккаунту.

Поэтому составляющие архитектуры нашего скрипта такие:

1. Octo Browser — изолированный отпечаток, прокси, профиль с реальной Instagram-сессией.
2. Rebrowser-puppeteer — форк Puppeteer без характерных утечек, которые достаточно легко обнаружить.
3. WindMouse — физическая модель движения мыши. 
4. Поведенческий слой — случайные просмотры сторис и лайки на постах перед парсингом, паузы по логнормальному распределению, overshoot (промахи мимо цели), реакции на чекпойнты.

WindMouse: почему лучше кубических кривых Безье

В предыдущей статье мы использовали кубические кривые Безье для движения мыши. Это базовый подход, но у него есть слабые места: с двумя контрольными точками кривая получается гладкой и предсказуемой. Фигура всегда одна и та же, скорость управляется отдельным easing-параметром, что выглядит слишком равномерно, а микродрожание добавляется поверх отдельным слоем — и видно, что это два разных слоя: гладкая кривая плюс jitter-шум.

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

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

Дополняем «человечность» еще двумя приемами: overshoot — в 30% случаев целимся не точно в кнопку, а проскакиваем чуть мимо и возвращаемся, и микродрожание во время «нажатия» — между mouse.down() и mouse.up() смещаем курсор на 1–2 пикселя в случайную сторону.

Реализация WindMouse в скрипте — функция wind_mouse_trajectory, она генерирует массив точек траектории, по которым потом проходит page.mouse.move.

Готовый скрипт для парсинга подписчиков целевых аккаунтов и одновременного прогрева

Допустим, мы имеем массив UUID профилей Octo, уже залогиненных в аккаунтах Instagram. Наш скрипт будет заходить в каждый, прогревать аккаунт лайками в ленте и просмотром сторис. Уже после прогрева скрипт будет открывать профили Instagram, которые мы указали как цели для парсинга. Там мы лайкаем посты, подписываемся и, наконец, парсим список подписчиков в JSON и CSV. И все это — с естественной траекторией мыши, случайными задержками и проверкой на чекпойнты. Так Instagram не сможет отличить вашу автоматизацию от обычных действий пользователей.

Подготовка профилей

1. Создайте в Octo один или несколько профилей с прокси. Лучше резидентными или мобильными.

2. Вручную откройте каждый профиль в Octo. Авторизуйтесь или зарегистрируйтесь в Instagram. Два-три дня пользуйтесь аккаунтами, как обычный человек (ставьте лайки, смотрите сторис, на кого-то подписывайтесь).
3. Скопируйте UUID профилей из Octo. Они потребуются для заполнения конфигурации в скрипте.

Запуск

  1. Скачайте и установите VS Code.

  2. Скачайте и установите node.js.

  3. Создайте папку в удобном для вас месте и назовите ее, например octo_instagram_scraper.

  4. Откройте эту папку в VS Code.

  5. Создайте файл с расширением .js. Лучше называть его по имени действия, которое будет выполнять код, чтобы не запутаться. Например, octo_instagram_scraper.js.

  6. Вставьте в файл код скрипта.

  7. Заполните конфигурацию в переменной config.

    UUID — это ID профилей Octo;


    target_accounts — аккаунты, которые будете парсить;

    followers_per_target — число подписчиков целевого аккаунта, данные которых вы хотите собирать за проход.

    Остальные параметры в config отвечают за правдоподобное поведение парсера. Можете поэкспериментировать с ними либо не менять.

Заполните конфигурацию в переменной config.
  1. Откройте терминал и выполните команду npm i rebrowser-puppeteer axios, чтобы установить зависимости для NodeJS.

Откройте терминал и выполните команду npm i rebrowser-puppeteer axios

Если VS Code выдает ошибку — откройте от имени администратора Window PowerShell, введите там команду Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned и подтвердите. Затем повторите предыдущий пункт.

  1. Запустите клиент Octo Browser.

  2. . Запустите скрипт в Visual Studio (Ctrl/Cmd + F5) и дождитесь окончания работы скрипта.

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

Сводка по результатам работы парсера. Для примера мы прошли по два аккаунта с каждого нашего профиля: 3 из 4 проходов успешны

Сводка по результатам работы парсера. Для примера мы прошли по два аккаунта с каждого нашего профиля: 3 из 4 проходов успешны

Учитывайте, что Instagram может ограничивать список выдачи подписчиков и это зависит от многих факторов: прогрева аккаунта, количества взаимных контактов, наличия подписки на целевой аккаунт и т. д. Если что-то не сработало — экспериментируйте с прогревом либо меняйте аккаунт/прокси.

Результаты работы парсера в CSV-формате

Результаты работы парсера в CSV-формате

Результаты работы парсера в JSON-формате

Результаты работы парсера в JSON-формате

JSON удобнее для дальнейшей обработки кодом, CSV — для импорта в Excel, Google Sheets, CRM или прямой загрузки в Meta Ads как Custom Audience.

Код скрипта

const axios = require('axios');
const puppeteer = require('rebrowser-puppeteer');
const fs = require('fs').promises;
const path = require('path');

const config = {
    octo_local_api_base_url: `http://localhost:58888/api/profiles`,
    headless_mode: false,
    profiles: [
        {
            uuid: "f7ac08ecae1b4a528b843bc4706ef3dd",
            target_accounts: ["phd_balance", "microbialecology"]
        },
        {
            uuid: "22be57d5c6f44e368258dc5ad6b425d3",
            target_accounts: ["the_brain_scientist", "drkaranrajan"]
        }
    ],
    followers_per_target: 100,
    follow_targets_before_parsing: true,
    likes_per_session: { min: 1, max: 3 },
    likes_on_target: { min: 1, max: 4 },
    stories_probability: 0.3,
    stories_per_session: { min: 1, max: 3 },
    delay_between_likes: { min: 5, max: 10 },
    delay_between_targets: { min: 60, max: 120 },
    delay_between_profiles: { min: 60, max: 120 },
    results_dir: 'instagram_results'
};

async function find_unliked_like(scope_handle) {
    const handle = await scope_handle.evaluateHandle(root => {
        const sections = root.querySelectorAll('section');
        for (const sec of sections) {
            const clickables = sec.querySelectorAll('button, div[role="button"], a[role="button"], a[role="link"]');
            const icon_buttons = [];
            for (const el of clickables) {
                if (el.querySelectorAll('svg').length !== 1) continue;
                const r = el.getBoundingClientRect();
                if (r.width < 16 || r.height < 16) continue;
                icon_buttons.push(el);
            }
            const top_level = icon_buttons.filter(b =>
                !icon_buttons.some(other => other !== b && other.contains(b))
            );
            if (top_level.length < 3 || top_level.length > 5) continue;

            const heart_btn = top_level[0];
            const heart_svg = heart_btn.querySelector('svg');
            if (!heart_svg) continue;

            const path = heart_svg.querySelector('path');
            if (path) {
                const fill = window.getComputedStyle(path).fill || '';
                const m = fill.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
                if (m && +m[1] > 200 && +m[2] < 100 && +m[3] < 100) continue;
            }
            return heart_svg;
        }
        return null;
    });
    return handle.asElement();
}

function random_range(min, max) {
    return min + Math.random() * (max - min);
}

function random_int(min, max) {
    return Math.floor(random_range(min, max + 1));
}

function pick_random(arr) {
    return arr[Math.floor(Math.random() * arr.length)];
}

async function sleep(seconds) {
    return new Promise(resolve => setTimeout(resolve, seconds * 1000));
}

async function human_delay(min_ms = 50, max_ms = 200) {
    const mu = Math.log((min_ms + max_ms) / 2);
    const sigma = random_range(0.3, 0.6);
    let delay = Math.exp(mu + sigma * (Math.random() - 0.5) * 2);
    delay = Math.min(max_ms, Math.max(min_ms, delay));
    await new Promise(resolve => setTimeout(resolve, delay));
}

async function ensure_dir(dir) {
    await fs.mkdir(dir, { recursive: true });
}

async function dump_state(page, label) {
    try {
        const dir = path.join(__dirname, 'debug');
        await ensure_dir(dir);
        const stamp = new Date().toISOString().replace(/[:.]/g, '-');
        const png = path.join(dir, `${stamp}_${label}.png`);
        const html = path.join(dir, `${stamp}_${label}.html`);
        await page.screenshot({ path: png, fullPage: false });
        const body = await page.evaluate(() => document.documentElement.outerHTML);
        await fs.writeFile(html, body);
        console.log(`📸 dump [${label}] → ${png}`);
    } catch (e) {
        console.warn(`📸 dump [${label}] failed: ${e.message}`);
    }
}

function csv_escape(value) {
    if (value === null || value === undefined) return '';
    const s = String(value);
    if (/[",\n\r]/.test(s)) return '"' + s.replace(/"/g, '""') + '"';
    return s;
}

function wind_mouse_trajectory(start, end, options = {}) {
    const {
        gravity = 9,
        wind = 3,
        max_step = 15,
        target_area = 12,
        min_wait_ms = 5,
        max_wait_ms = 12
    } = options;

    let x = start.x, y = start.y;
    let v_x = 0, v_y = 0;
    let w_x = 0, w_y = 0;
    let M = max_step;

    const points = [];
    let prev_x = Math.round(x), prev_y = Math.round(y);

    let safety = 0;
    while (safety++ < 10000) {
        const dist = Math.hypot(end.x - x, end.y - y);
        if (dist < 1) break;

        const w_mag = Math.min(wind, dist);
        if (dist >= target_area) {
            w_x = w_x / Math.sqrt(3) + (Math.random() * 2 - 1) * w_mag / Math.sqrt(5);
            w_y = w_y / Math.sqrt(3) + (Math.random() * 2 - 1) * w_mag / Math.sqrt(5);
        } else {
            w_x /= Math.sqrt(2);
            w_y /= Math.sqrt(2);
            if (M < 3) M = Math.random() * 3 + 3;
            else M /= Math.sqrt(5);
        }

        v_x += w_x + gravity * (end.x - x) / dist;
        v_y += w_y + gravity * (end.y - y) / dist;

        const v_mag = Math.hypot(v_x, v_y);
        if (v_mag > M) {
            const v_clip = M / 2 + Math.random() * M / 2;
            v_x = (v_x / v_mag) * v_clip;
            v_y = (v_y / v_mag) * v_clip;
        }

        x += v_x;
        y += v_y;

        const rx = Math.round(x);
        const ry = Math.round(y);
        if (rx !== prev_x || ry !== prev_y) {
            const wait = min_wait_ms + Math.random() * (max_wait_ms - min_wait_ms);
            points.push({ x: rx, y: ry, wait });
            prev_x = rx; prev_y = ry;
        }
    }

    points.push({ x: Math.round(end.x), y: Math.round(end.y), wait: 5 });
    return points;
}

async function move_mouse_human(page, target, options = {}) {
    const current = await page.evaluate(() => ({
        x: window.__mouseX ?? window.innerWidth / 2,
        y: window.__mouseY ?? window.innerHeight / 2
    }));

    const trajectory = wind_mouse_trajectory(current, target, options);
    for (const p of trajectory) {
        await page.mouse.move(p.x, p.y);
        if (p.wait > 0) await new Promise(r => setTimeout(r, p.wait));
    }

    await page.evaluate(({ x, y }) => {
        window.__mouseX = x;
        window.__mouseY = y;
    }, target);
}

async function human_click(page, selector_or_handle, options = {}) {
    const {
        overshoot_chance = 0.3,
        scroll_into_view = true,
        post_click_delay = [120, 350]
    } = options;

    const handle = typeof selector_or_handle === 'string'
        ? await page.$(selector_or_handle)
        : selector_or_handle;

    if (!handle) throw new Error(`Element not found: ${selector_or_handle}`);

    if (scroll_into_view) {
        await handle.evaluate(el => el.scrollIntoView({ block: 'center', behavior: 'smooth' }));
        await human_delay(500, 1000);
    }

    const box = await handle.boundingBox();
    if (!box) throw new Error('Could not get element coordinates');

    const target = {
        x: box.x + random_range(box.width * 0.25, box.width * 0.75),
        y: box.y + random_range(box.height * 0.25, box.height * 0.75)
    };

    if (Math.random() < overshoot_chance) {
        const overshoot = {
            x: target.x + (Math.random() - 0.5) * random_range(15, 35),
            y: target.y + (Math.random() - 0.5) * random_range(15, 35)
        };
        await move_mouse_human(page, overshoot);
        await human_delay(40, 120);
        await move_mouse_human(page, target);
    } else {
        await move_mouse_human(page, target);
    }

    await human_delay(post_click_delay[0], post_click_delay[1]);

    await page.mouse.down();
    await human_delay(40, 120);

    if (Math.random() < 0.25) {
        await page.mouse.move(
            target.x + (Math.random() - 0.5) * 2,
            target.y + (Math.random() - 0.5) * 2
        );
    }
    await page.mouse.up();

    return { x: target.x, y: target.y };
}

async function human_scroll_window(page, options = {}) {
    const { scrolls = random_int(2, 5), back_chance = 0.2 } = options;

    for (let i = 0; i < scrolls; i++) {
        const distance = random_range(300, 800);
        await page.evaluate(d => window.scrollBy({ top: d, behavior: 'smooth' }), distance);
        await human_delay(900, 2200);

        if (Math.random() < back_chance) {
            const back = random_range(100, 280);
            await page.evaluate(d => window.scrollBy({ top: -d, behavior: 'smooth' }), back);
            await human_delay(500, 1100);
        }
    }
}

async function human_scroll_element(page, element_handle, distance) {
    const before = await element_handle.evaluate(el => el.scrollTop);

    await element_handle.evaluate((el, d) => {
        el.scrollTop = el.scrollTop + d;
        el.dispatchEvent(new WheelEvent('wheel', {
            deltaY: d,
            bubbles: true,
            cancelable: true
        }));
    }, distance);

    await human_delay(700, 1800);
    const after = await element_handle.evaluate(el => el.scrollTop);
    return after - before;
}

async function dismiss_overlays(page, max_passes = 3) {

    const MODAL_SCOPE = [
        'div[role="dialog"]',
        'div[role="alertdialog"]',
        '[aria-modal="true"]',
        'div[data-testid="cookie-policy-manage-dialog"]',
        'div[aria-label*="cookie" i]',
        'div[aria-label*="Cookie"]'
    ].join(', ');

    let dismissed = 0;
    for (let pass = 0; pass < max_passes; pass++) {
        const modals = await page.$$(MODAL_SCOPE);
        if (modals.length === 0) break;

        const before = modals.length;
        await page.keyboard.press('Escape').catch(() => { });
        await human_delay(600, 1400);

        const after_modals = await page.$$(MODAL_SCOPE);
        if (after_modals.length < before) {
            console.log(`🪟 Modal dismissed via ESC`);
            dismissed++;
            continue;
        }
        break;
    }
    return dismissed;
}

async function check_for_block(page) {
    const url = page.url();
    if (url.includes('/challenge/') || url.includes('/accounts/suspended/') ||
        url.includes('/accounts/disabled/')) {
        return { blocked: true, reason: 'redirect: ' + url };
    }

    const block_text = await page.evaluate(() => {
        const text = document.body.innerText.toLowerCase();
        const markers = [
            'we restricted certain activity',
            'try again later',
            'suspicious login attempt',
            'your account has been temporarily',
            'temporary action restriction',
            'suspicious login'
        ];
        for (const m of markers) if (text.includes(m)) return m;
        return null;
    });

    if (block_text) return { blocked: true, reason: 'text: ' + block_text };
    return { blocked: false };
}

async function check_logged_in(page) {
    const url = page.url();
    if (url.includes('/accounts/login') || url.includes('/accounts/emailsignup')) {
        return false;
    }
    const has_login_form = await page.$('input[name="username"]');
    return !has_login_form;
}

async function watch_random_stories(page, count) {
    console.log(`📺 Trying to watch ${count} stories...`);
    const story_buttons = await page.$$('div[role="menuitem"] button[role="button"], li button[role="button"]');
    const visible_stories = [];
    for (const btn of story_buttons) {
        const box = await btn.boundingBox();
        if (box && box.y < 250 && box.width > 30) visible_stories.push(btn);
    }

    if (visible_stories.length === 0) {
        const fallback = await page.$$('canvas');
        for (const c of fallback) {
            const box = await c.boundingBox();
            if (box && box.y < 250) visible_stories.push(c);
        }
    }

    if (visible_stories.length === 0) {
        console.log('   ⏭ No stories found, skipping');
        return 0;
    }

    let watched = 0;
    const to_watch = Math.min(count, visible_stories.length);
    for (let i = 0; i < to_watch; i++) {
        try {
            const story = pick_random(visible_stories);
            await human_click(page, story);
            await human_delay(3000, 7000);
            await page.keyboard.press('Escape');
            await human_delay(800, 1500);
            watched++;
            console.log(`   👀 Story ${watched}/${to_watch} watched`);
        } catch (e) {
            console.log(`   ⚠️ Error while watching story: ${e.message}`);
            await page.keyboard.press('Escape').catch(() => { });
        }
    }
    return watched;
}

async function browse_feed_and_like(page, target_likes) {
    console.log(`❤️ Feed warm-up: ${target_likes} likes`);
    await dismiss_overlays(page);

    if (!page.url().match(/instagram\.com\/?(\?|$)/)) {
        await page.goto('https://www.instagram.com/', {
            waitUntil: 'domcontentloaded',
            timeout: 30000
        }).catch(() => { });
        await human_delay(2000, 3500);
    }

    const articles_appeared = await page.waitForFunction(
        () => document.querySelectorAll('article').length > 0,
        { timeout: 12000 }
    ).then(() => true).catch(() => false);

    if (!articles_appeared) {

        for (let i = 0; i < 3; i++) {
            await human_scroll_window(page, { scrolls: 2 });
            const ok = await page.evaluate(() => document.querySelectorAll('article').length > 0);
            if (ok) break;
        }
    }

    let liked = 0;
    let scrolls_without_progress = 0;

    while (liked < target_likes && scrolls_without_progress < 5) {

        const articles = await page.$$('article');
        let new_like_done = false;

        for (const article of articles) {
            if (liked >= target_likes) break;
            try {

                const like_btn = await find_unliked_like(article);
                if (!like_btn) continue;

                const box = await like_btn.boundingBox();
                if (!box) continue;

                const in_view = await like_btn.evaluate(el => {
                    const r = el.getBoundingClientRect();
                    return r.top > 50 && r.bottom < window.innerHeight - 50;
                });
                if (!in_view) continue;

                if (Math.random() < 0.3) {
                    await human_delay(1500, 4000);
                }

                await human_click(page, like_btn);
                liked++;
                new_like_done = true;
                console.log(`   ❤️ Like ${liked}/${target_likes}`);

                const pause = random_range(
                    config.delay_between_likes.min,
                    config.delay_between_likes.max
                );
                console.log(`   ⏰ Pause ${pause.toFixed(1)} sec`);
                await sleep(pause);

                const block = await check_for_block(page);
                if (block.blocked) {
                    console.log(`   🚫 Blocked after like: ${block.reason}`);
                    return { liked, blocked: true };
                }

                break;
            } catch (e) {
                continue;
            }
        }

        if (!new_like_done) {
            scrolls_without_progress++;
            await human_scroll_window(page, { scrolls: random_int(2, 4), back_chance: 0.05 });
            await human_delay(1500, 3000);
        } else {
            scrolls_without_progress = 0;

            await human_scroll_window(page, { scrolls: random_int(1, 2), back_chance: 0.1 });
            await human_delay(800, 1800);
        }
    }

    console.log(`✅ Warm-up complete, likes: ${liked}`);
    if (liked === 0) await dump_state(page, 'feed_zero_likes');
    return { liked, blocked: false };
}

async function visit_target_profile(page, username) {
    console.log(`🎯 Opening profile @${username}`);
    await page.goto(`https://www.instagram.com/${username}/`, {
        waitUntil: 'domcontentloaded',
        timeout: 30000
    });
    await human_delay(2500, 4500);
    await dismiss_overlays(page);

    const not_found = await page.evaluate(() => {
        const t = document.body.innerText;
        return t.includes("Sorry, this page isn't available")
            || t.includes('Страница недоступна');
    });
    if (not_found) {
        console.log(`   ⚠️ Profile @${username} not found`);
        return false;
    }

    const block = await check_for_block(page);
    if (block.blocked) {
        console.log(`   🚫 Blocked while opening profile: ${block.reason}`);
        return false;
    }

    await human_scroll_window(page, { scrolls: random_int(2, 4) });
    return true;
}

async function like_target_posts(page, count) {
    console.log(`💜 Liking ${count} post(s) on the target account`);

    const post_links = await page.$$('a[href*="/p/"]');
    if (post_links.length === 0) {
        console.log('   ⏭ No posts visible (private account or no publications)');
        return 0;
    }

    let liked = 0;
    const indices = [];
    for (let i = 0; i < count && i < post_links.length; i++) {

        const idx = random_int(0, Math.min(11, post_links.length - 1));
        if (!indices.includes(idx)) indices.push(idx);
    }

    for (const idx of indices) {
        try {
            const post_links_fresh = await page.$$('a[href*="/p/"]');
            if (!post_links_fresh[idx]) continue;

            await human_click(page, post_links_fresh[idx]);
            await human_delay(2500, 4500);

            const post_root = await page.evaluateHandle(() => {
                const dialog = document.querySelector('div[role="dialog"]');
                if (dialog) {
                    const inner_article = dialog.querySelector('article');
                    return inner_article || dialog;
                }
                return document.querySelector('article');
            });
            const post_root_el = post_root.asElement();
            const like_btn = post_root_el ? await find_unliked_like(post_root_el) : null;
            if (like_btn) {
                await human_click(page, like_btn);
                liked++;
                console.log(`   ❤️ Like on post ${liked}/${count}`);
            } else {
                console.log(`   ⏭ Post already liked or like button not found, skipping`);
            }

            if (Math.random() < 0.4) {
                await page.keyboard.press('ArrowRight').catch(() => { });
                await human_delay(800, 1800);
            }

            await human_delay(1500, 3500);
            await page.keyboard.press('Escape');
            await human_delay(1200, 2200);

            const block = await check_for_block(page);
            if (block.blocked) {
                console.log(`   🚫 Blocked after liking a post: ${block.reason}`);
                return liked;
            }

            const pause = random_range(
                config.delay_between_likes.min,
                config.delay_between_likes.max
            );
            await sleep(pause);
        } catch (e) {
            console.log(`   ⚠️ Post-like error: ${e.message}`);
            await page.keyboard.press('Escape').catch(() => { });
        }
    }

    return liked;
}

function setup_friendship_interceptor(page) {
    const state = {
        last_profile_info: null,
        last_friendship_action: null,
        action_blocked: false
    };

    const handler = async (response) => {
        const url = response.url();

        if (/\/api\/v1\/users\/web_profile_info\//.test(url)) {
            try {
                const json = await response.json();
                const u = json?.data?.user;
                if (u) {
                    state.last_profile_info = {
                        username: u.username,
                        id: u.id,
                        followed_by_viewer: !!u.followed_by_viewer,
                        requested_by_viewer: !!u.requested_by_viewer,
                        is_private: !!u.is_private
                    };
                }
            } catch (e) { }
            return;
        }

        if (/\/api\/v1\/friendships\/(create|destroy)\//.test(url)) {
            try {
                const json = await response.json();
                state.last_friendship_action = { status: response.status(), body: json };
                if (json?.spam || json?.feedback_required || json?.error_type === 'feedback_required') {
                    state.action_blocked = true;
                }
            } catch (e) {
                state.last_friendship_action = { status: response.status(), body: null };
            }
        }
    };

    page.on('response', handler);
    return { state, cleanup: () => page.off('response', handler) };
}

async function fetch_profile_info(page, username) {
    return await page.evaluate(async (u) => {
        try {
            const res = await fetch(`/api/v1/users/web_profile_info/?username=${encodeURIComponent(u)}`, {
                headers: { 'X-IG-App-ID': '936619743392459' },
                credentials: 'include'
            });
            if (!res.ok) return { error: `HTTP ${res.status}` };
            const json = await res.json();
            const user = json?.data?.user;
            if (!user) return { error: 'no user in response body' };
            return {
                username: user.username,
                id: user.id,
                followed_by_viewer: !!user.followed_by_viewer,
                requested_by_viewer: !!user.requested_by_viewer,
                is_private: !!user.is_private
            };
        } catch (e) {
            return { error: e.message };
        }
    }, username);
}

async function ensure_following(page, target_username, friendship_iface) {
    const wait_for = async (predicate, max_ms, step = 200) => {
        const start = Date.now();
        while (Date.now() - start < max_ms) {
            const v = predicate();
            if (v) return v;
            await new Promise(r => setTimeout(r, step));
        }
        return predicate() || null;
    };

    const info = await fetch_profile_info(page, target_username);

    if (!info || info.error || !info.username) {
        console.log(`   ⚠️ Could not fetch profile info via API for @${target_username}: ${info?.error || 'no data'}`);
        return false;
    }

    console.log(`   🔎 API friendship state for @${target_username}: following=${info.followed_by_viewer}, requested=${info.requested_by_viewer}, private=${info.is_private}`);

    if (info.followed_by_viewer) {
        console.log(`   ✓ Already following @${target_username}`);
        return true;
    }
    if (info.requested_by_viewer) {
        console.log(`   ✓ Follow request already pending for @${target_username}`);
        return true;
    }

    await page.evaluate(() => window.scrollTo({ top: 0, behavior: 'instant' }));
    await human_delay(800, 1500);

    const handle = await page.evaluateHandle((username) => {
        const heading_candidates = Array.from(
            document.querySelectorAll('h1, h2, span')
        ).filter(el => {
            const text = (el.textContent || '').trim();
            if (text !== username) return false;
            const r = el.getBoundingClientRect();
            return r.top >= 0 && r.top < 500 && r.width > 30;
        });

        if (heading_candidates.length === 0) return null;
        const anchor = heading_candidates[0];

        let container = anchor;
        for (let depth = 0; depth < 10 && container.parentElement; depth++) {
            container = container.parentElement;
            const buttons = Array.from(container.querySelectorAll('button, div[role="button"]'));
            const candidates = buttons.filter(btn => {
                const r = btn.getBoundingClientRect();
                if (r.width < 70 || r.height < 28 || r.width > 400 || r.height > 80) return false;
                const text = (btn.textContent || '').trim();
                if (text.length === 0 || text.length > 60) return false;
                return btn.querySelectorAll('svg').length <= 1;
            });
            if (candidates.length === 0) continue;
            candidates.sort((a, b) => {
                const ra = a.getBoundingClientRect(), rb = b.getBoundingClientRect();
                if (Math.abs(ra.top - rb.top) > 5) return ra.top - rb.top;
                return ra.left - rb.left;
            });
            return candidates[0];
        }
        return null;
    }, target_username);

    const el = handle.asElement();
    if (!el) {
        console.log(`   ⚠️ Primary action button not found near @${target_username} username anchor`);
        await dump_state(page, `no_follow_btn_${target_username}`);
        return false;
    }

    friendship_iface.state.last_friendship_action = null;
    friendship_iface.state.action_blocked = false;

    console.log(`   ➕ Subscribing to @${target_username}...`);
    await human_click(page, el);
    await human_delay(2500, 4000);

    const after = await fetch_profile_info(page, target_username);
    if (after && !after.error) {
        if (after.followed_by_viewer) {
            console.log(`   ✓ Subscribed (API confirmed)`);
            return true;
        }
        if (after.requested_by_viewer) {
            console.log(`   ✓ Follow request sent (private account)`);
            return true;
        }
    }

    if (friendship_iface.state.action_blocked) {
        const body = friendship_iface.state.last_friendship_action?.body;
        const fb = body?.feedback_message || body?.feedback_title || 'feedback_required';
        console.log(`   🚫 Instagram action-blocked the follow: ${fb}`);
        return false;
    }

    console.log(`   ⚠️ Click sent but API still reports not following — silent block or click missed the toggle`);
    return false;
}

function setup_followers_interceptor(page) {
    const state = {
        has_next_page: true,
        api_seen: false,
        responses: 0,
        users_returned: 0,
        empty_with_next: 0,
        cap_detected: false
    };

    const handler = async (response) => {
        const url = response.url();
        const is_followers_api = /\/friendships\/\d+\/followers\//.test(url);
        const is_graphql = /edge_followed_by/.test(url);
        if (!is_followers_api && !is_graphql) return;

        try {
            const json = await response.json();
            state.api_seen = true;
            state.responses++;

            let batch_size = 0;
            let has_next = state.has_next_page;

            if ('next_max_id' in json) {
                has_next = !!json.next_max_id;
                if (Array.isArray(json.users)) {
                    batch_size = json.users.length;
                    state.users_returned += batch_size;
                }
            } else if (json?.data?.user?.edge_followed_by?.page_info) {
                const pi = json.data.user.edge_followed_by.page_info;
                has_next = !!pi.has_next_page;
                const edges = json.data.user.edge_followed_by.edges;
                if (Array.isArray(edges)) {
                    batch_size = edges.length;
                    state.users_returned += batch_size;
                }
            }

            state.has_next_page = has_next;

            if (has_next && batch_size === 0) {
                state.empty_with_next++;
                if (state.empty_with_next >= 1) state.cap_detected = true;
            } else {
                state.empty_with_next = 0;
            }
        } catch (e) {
        }
    };

    page.on('response', handler);
    return { state, cleanup: () => page.off('response', handler) };
}

async function parse_followers(page, target_username, max_count) {
    console.log(`📋 Parsing followers of @${target_username} (target: ${max_count})`);

    await dismiss_overlays(page);

    const { state: api_state, cleanup: detach_interceptor } = setup_followers_interceptor(page);
    try {
        return await _parse_followers_impl(page, target_username, max_count, api_state);
    } finally {
        detach_interceptor();
    }
}

async function _parse_followers_impl(page, target_username, max_count, api_state) {
    const followers_handle = await page.evaluateHandle((target) => {
        const followers_re = new RegExp(`^/${target}/followers/?(\\?|$)`, 'i');

        const anchors = Array.from(document.querySelectorAll('a[href]'));
        for (const a of anchors) {
            if (followers_re.test(a.getAttribute('href') || '')) return a;
        }

        const all_clickable = Array.from(document.querySelectorAll(
            'a, button, div[role="button"], a[role="link"]'
        ));
        const counters = all_clickable.filter(el => {
            const t = (el.innerText || '').trim();
            if (!t || t.length > 60 || !/\d/.test(t)) return false;
            const r = el.getBoundingClientRect();
            return r.width > 0 && r.height > 0 && r.top < window.innerHeight;
        });

        for (const seed of counters) {
            let node = seed;
            for (let depth = 0; depth < 8 && node.parentElement; depth++) {
                const parent = node.parentElement;
                const row = [];
                for (const child of parent.children) {
                    const hit = counters.find(c => child === c || child.contains(c));
                    if (hit) row.push(hit);
                }
                if (row.length >= 2 && row.length <= 4) {
                    return row[row.length - 2];
                }
                node = parent;
            }
        }
        return null;
    }, target_username);

    const followers_link = followers_handle.asElement();
    if (!followers_link) {
        console.log('   ❌ "Followers" button not found');
        await dump_state(page, `target_${target_username}_no_followers_btn`);
        return [];
    }
    await human_click(page, followers_link);
    await human_delay(1800, 3000);

    await page.waitForSelector('div[role="dialog"]', { timeout: 10000 }).catch(() => { });

    const got_users = await page.waitForFunction(() => {
        const dialog = document.querySelector('div[role="dialog"]');
        if (!dialog) return false;
        return dialog.querySelectorAll('a[role="link"][href^="/"]').length >= 3;
    }, { timeout: 15000 }).then(() => true).catch(() => false);

    if (!got_users) {
        console.log('   ❌ Followers modal did not load the list within 15 sec');
        return [];
    }
    await human_delay(800, 1500);

    const find_scrollable = async () => {
        const handle = await page.evaluateHandle(() => {
            const dialog = document.querySelector('div[role="dialog"]');
            if (!dialog) return null;
            const all = dialog.querySelectorAll('div');
            let best = null, best_count = 0;
            for (const el of all) {
                const s = window.getComputedStyle(el);
                if (s.overflowY !== 'auto' && s.overflowY !== 'scroll') continue;
                const links = el.querySelectorAll('a[role="link"][href^="/"]').length;
                if (links > best_count) { best = el; best_count = links; }
            }
            return best;
        });
        return handle.asElement();
    };

    let scrollable = await find_scrollable();
    if (!scrollable) {
        console.log('   ❌ Scrollable container of the modal not found');
        return [];
    }
    console.log(`   📜 Container found, initial scrollHeight: ${await scrollable.evaluate(el => el.scrollHeight)}px`);

    const modal_box = await scrollable.boundingBox();
    if (modal_box) {
        await move_mouse_human(page, {
            x: modal_box.x + modal_box.width / 2,
            y: modal_box.y + modal_box.height / 2
        });
        await human_delay(200, 500);
    }

    const collected = new Map();
    let stagnation = 0;
    const max_stagnation = 5;
    let iteration = 0;
    const max_iterations = 200;

    while (collected.size < max_count && stagnation < max_stagnation && iteration < max_iterations) {
        iteration++;

        const batch = await page.evaluate(() => {
            const dialog = document.querySelector('div[role="dialog"]');
            if (!dialog) return [];
            const links = dialog.querySelectorAll('a[role="link"][href^="/"]');
            const seen = new Map();

            const skip = new Set(['explore', 'reels', 'direct', 'accounts', 'p', 'tv', 'stories', 'about']);

            const button_words = new Set([
                'Follow', 'Following', 'Message', 'Requested', 'Subscribed', 'Subscribe',
                'Подписаться', 'Подписки', 'Подписан', 'Подписана', 'Запрос', 'Сообщение',
                'Отписаться', 'Subscribirse', 'Seguir', 'Suivre'
            ]);

            for (const a of links) {
                const href = a.getAttribute('href');
                const m = href && href.match(/^\/([A-Za-z0-9._]+)\/?$/);
                if (!m) continue;
                const username = m[1];
                if (skip.has(username)) continue;
                if (seen.has(username)) continue;

                let row = a;
                for (let depth = 0; depth < 10 && row.parentElement; depth++) {
                    const parent = row.parentElement;
                    const sibling_links = parent.querySelectorAll('a[role="link"][href^="/"]');
                    let other = 0;
                    for (const l of sibling_links) {
                        const lm = l.getAttribute('href').match(/^\/([A-Za-z0-9._]+)\/?$/);
                        if (lm && lm[1] !== username && !skip.has(lm[1])) { other++; break; }
                    }
                    if (other > 0) break;
                    row = parent;
                }

                const lines = (row.innerText || '')
                    .split(/\n+/).map(s => s.trim()).filter(Boolean);
                let full_name = '';
                for (const line of lines) {
                    if (line === username) continue;
                    if (button_words.has(line)) continue;
                    if (line.length > 150) continue;
                    full_name = line;
                    break;
                }

                const verified = !!row.querySelector('svg[aria-label="Verified"], svg[aria-label*="Verif"], svg[aria-label="Подтвержденный"]');
                const is_private = !!row.querySelector('svg[aria-label="Private"]');

                seen.set(username, { username, full_name, verified, is_private });
            }
            return Array.from(seen.values());
        });

        const before = collected.size;
        for (const u of batch) {
            if (collected.size >= max_count) break;
            if (!collected.has(u.username)) collected.set(u.username, u);
        }
        const added = collected.size - before;

        if (added === 0) stagnation++;
        else stagnation = 0;

        const api_tail = api_state.api_seen
            ? ` [api: ${api_state.responses} resp, ${api_state.users_returned} users, next=${api_state.has_next_page}${api_state.cap_detected ? ', CAP' : ''}]`
            : '';
        process.stdout.write(`   📊 Collected: ${collected.size}/${max_count} (+${added})${api_tail}\r`);

        if (collected.size >= max_count) break;

        if (api_state.api_seen && (!api_state.has_next_page || api_state.cap_detected)) {
            const reason = api_state.cap_detected
                ? `server cap (empty batch with next_max_id present, returned ${api_state.users_returned} users total)`
                : 'has_next_page=false';
            console.log(`\n   🛑 Pagination closed by Instagram: ${reason}`);
            break;
        }

        const wheel_delta = random_range(400, 700);
        await page.mouse.wheel({ deltaY: wheel_delta });
        await human_delay(900, 1700);

        if (stagnation >= 1) {
            await scrollable.evaluate(el => {
                const links = el.querySelectorAll('a[role="link"][href^="/"]');
                if (links.length > 0) {
                    links[links.length - 1].scrollIntoView({ block: 'end' });
                }
            });
            await human_delay(700, 1300);

            const fresh = await find_scrollable();
            if (fresh) scrollable = fresh;
        }

        if (Math.random() < 0.2) await human_delay(1500, 3500);
    }

    console.log(`\n   ✅ Followers collected: ${collected.size}`);

    await page.keyboard.press('Escape').catch(() => { });
    await human_delay(800, 1500);

    return Array.from(collected.values()).slice(0, max_count);
}

async function save_followers(profile_uuid, target, users) {
    const dir = path.join(__dirname, config.results_dir);
    await ensure_dir(dir);

    const stamp = new Date().toISOString().replace(/[:.]/g, '-');
    const base = `${target}_${profile_uuid.slice(0, 8)}_${stamp}`;

    const json_path = path.join(dir, `${base}.json`);
    const csv_path = path.join(dir, `${base}.csv`);

    const json_payload = {
        target_account: target,
        parsed_by_profile: profile_uuid,
        timestamp: new Date().toISOString(),
        total: users.length,
        followers: users
    };
    await fs.writeFile(json_path, JSON.stringify(json_payload, null, 2), 'utf8');

    const csv_lines = ['username,full_name,verified,is_private'];
    for (const u of users) {
        csv_lines.push([
            csv_escape(u.username),
            csv_escape(u.full_name),
            csv_escape(u.verified),
            csv_escape(u.is_private)
        ].join(','));
    }
    await fs.writeFile(csv_path, csv_lines.join('\n'), 'utf8');

    console.log(`💾 JSON: ${json_path}`);
    console.log(`💾 CSV : ${csv_path}`);
}

async function process_target(page, target, profile_uuid) {
    const friendship_iface = setup_friendship_interceptor(page);
    try {
        const ok = await visit_target_profile(page, target);
        if (!ok) return { target, success: false };

        let followed = false;
        if (config.follow_targets_before_parsing) {
            followed = await ensure_following(page, target, friendship_iface);
            if (followed) await human_delay(3000, 6000);
        }

        const target_likes = random_int(config.likes_on_target.min, config.likes_on_target.max);
        if (target_likes > 0) {
            await like_target_posts(page, target_likes);
        }

        await page.goto(`https://www.instagram.com/${target}/`, {
            waitUntil: 'domcontentloaded',
            timeout: 30000
        }).catch(() => { });
        await human_delay(1500, 3000);

        const users = await parse_followers(page, target, config.followers_per_target);
        if (users.length > 0) {
            await save_followers(profile_uuid, target, users);
        }

        return { target, success: true, count: users.length, followed };
    } finally {
        friendship_iface.cleanup();
    }
}

async function process_profile(profile_cfg, idx, total) {
    console.log(`\n${'='.repeat(80)}`);
    console.log(`📋 Profile ${idx + 1}/${total} — UUID ${profile_cfg.uuid}`);
    console.log(`   Targets: ${profile_cfg.target_accounts.join(', ')}`);
    console.log(`${'='.repeat(80)}`);

    await octo_stop_profile(profile_cfg.uuid).catch(() => { });
    await sleep(3);

    let ws_data;
    try {
        ws_data = await octo_start_profile(profile_cfg.uuid);
    } catch (e) {
        const body = e.response?.data;
        const body_str = body ? (typeof body === 'string' ? body : JSON.stringify(body)) : '';
        console.error(`❌ Failed to start profile: ${e.message}${body_str ? ' | Octo: ' + body_str : ''}`);
        return { uuid: profile_cfg.uuid, status: 'start_failed', error: body_str || e.message };
    }

    if (!ws_data?.ws_endpoint) {
        console.error('❌ Octo did not return a ws_endpoint');
        await octo_stop_profile(profile_cfg.uuid).catch(() => { });
        return { uuid: profile_cfg.uuid, status: 'no_ws' };
    }

    let browser;
    try {
        browser = await puppeteer.connect({
            browserWSEndpoint: ws_data.ws_endpoint,
            defaultViewport: null,
            protocolTimeout: 600000
        });
    } catch (e) {
        console.error(`❌ Puppeteer connect: ${e.message}`);
        await octo_stop_profile(profile_cfg.uuid).catch(() => { });
        return { uuid: profile_cfg.uuid, status: 'connect_failed' };
    }

    try {
        const ctx = browser.defaultBrowserContext();
        await ctx.overridePermissions('https://www.instagram.com', []);
        await ctx.overridePermissions('https://instagram.com', []);
    } catch (e) {
        console.warn(`⚠️ overridePermissions: ${e.message}`);
    }

    let stats = { uuid: profile_cfg.uuid, status: 'ok', targets: [], likes: 0, stories: 0 };

    try {
        const page = await browser.newPage();
        await page.setViewport({ width: 1280, height: 900 });

        await page.goto('https://www.instagram.com/', {
            waitUntil: 'domcontentloaded',
            timeout: 45000
        });
        await human_delay(2500, 4500);
        await dismiss_overlays(page);

        await human_delay(800, 1500);
        await dismiss_overlays(page);

        if (!await check_logged_in(page)) {
            console.error('❌ Profile is not logged into Instagram');
            stats.status = 'not_logged_in';
            return stats;
        }

        const block = await check_for_block(page);
        if (block.blocked) {
            console.error(`❌ Account is restricted: ${block.reason}`);
            stats.status = 'blocked';
            return stats;
        }

        if (Math.random() < config.stories_probability) {
            const cnt = random_int(config.stories_per_session.min, config.stories_per_session.max);
            stats.stories = await watch_random_stories(page, cnt);
            await human_delay(1500, 3000);
        }

        const feed_likes = random_int(config.likes_per_session.min, config.likes_per_session.max);
        const feed_result = await browse_feed_and_like(page, feed_likes);
        stats.likes += feed_result.liked;
        if (feed_result.blocked) {
            stats.status = 'blocked_during_feed';
            return stats;
        }

        for (let i = 0; i < profile_cfg.target_accounts.length; i++) {
            const target = profile_cfg.target_accounts[i];
            try {
                const r = await process_target(page, target, profile_cfg.uuid);
                stats.targets.push(r);
            } catch (e) {
                console.error(`❌ Target @${target} error: ${e.message}`);
                stats.targets.push({ target, success: false, error: e.message });
            }

            if (i < profile_cfg.target_accounts.length - 1) {
                const pause = random_range(
                    config.delay_between_targets.min,
                    config.delay_between_targets.max
                );
                console.log(`⏰ Pause between targets: ${pause.toFixed(1)} sec`);
                await sleep(pause);
            }
        }

    } catch (e) {
        console.error(`❌ Profile processing error: ${e.message}`);
        stats.status = 'error';
        stats.error = e.message;
    } finally {
        await octo_stop_profile(profile_cfg.uuid).catch(() => { });
        await sleep(2);
    }

    return stats;
}

async function check_limits(response) {
    const header = response.headers.ratelimit;
    if (!header) return;
    const entries = header.split(',').map(s => s.trim());
    for (const e of entries) {
        const r_match = e.match(/;r=(\d+)/);
        const t_match = e.match(/;t=(\d+)/);
        if (!r_match || !t_match) continue;
        const remaining = parseInt(r_match[1], 10);
        const window_s = parseInt(t_match[1], 10);
        if (remaining < 5) {
            console.log(`⏳ Octo rate-limit, waiting ${window_s + 1} sec`);
            await sleep(window_s + 1);
        }
    }
}

async function octo_start_profile(uuid) {
    const res = await axios({
        method: 'post',
        url: `${config.octo_local_api_base_url}/start`,
        headers: { 'Content-Type': 'application/json' },
        data: {
            uuid,
            headless: config.headless_mode,
            debug_port: true,
            timeout: 60
        }
    });
    await check_limits(res);
    return res.data;
}

async function octo_stop_profile(uuid) {
    const res = await axios({
        method: 'post',
        url: `${config.octo_local_api_base_url}/stop`,
        headers: { 'Content-Type': 'application/json' },
        data: { uuid }
    });
    await check_limits(res);
    return res.data;
}

(async () => {
    console.log('🚀 Octo Instagram Parser & Liker');
    console.log(`   Profiles: ${config.profiles.length}`);
    console.log(`   Followers per target: ${config.followers_per_target}`);
    console.log(`   Feed likes: ${config.likes_per_session.min}${config.likes_per_session.max}`);
    console.log('');

    await ensure_dir(path.join(__dirname, config.results_dir));

    const all_stats = [];
    for (let i = 0; i < config.profiles.length; i++) {
        const stats = await process_profile(config.profiles[i], i, config.profiles.length);
        all_stats.push(stats);

        if (i < config.profiles.length - 1) {
            const pause = random_range(
                config.delay_between_profiles.min,
                config.delay_between_profiles.max
            );
            console.log(`\n⏰ Pause before the next profile: ${pause.toFixed(1)} sec`);
            await sleep(pause);
        }
    }

    console.log(`\n${'='.repeat(80)}`);
    console.log('📊 SUMMARY');
    console.log('='.repeat(80));
    for (const s of all_stats) {
        console.log(`\n${s.uuid}${s.status}`);
        console.log(`   Likes: ${s.likes ?? 0}, stories: ${s.stories ?? 0}`);
        if (s.targets) {
            for (const t of s.targets) {
                if (t.success) console.log(`   ✅ @${t.target}: ${t.count} followers`);
                else console.log(`   ❌ @${t.target}: ${t.error || 'fail'}`);
            }
        }
    }

    const summary_path = path.join(__dirname, config.results_dir, `_summary_${Date.now()}.json`);
    await fs.writeFile(summary_path, JSON.stringify(all_stats, null, 2));
    console.log(`\n📄 Summary report: ${summary_path}`);
    console.log('🎉 Done.');
})();
const axios = require('axios');
const puppeteer = require('rebrowser-puppeteer');
const fs = require('fs').promises;
const path = require('path');

const config = {
    octo_local_api_base_url: `http://localhost:58888/api/profiles`,
    headless_mode: false,
    profiles: [
        {
            uuid: "f7ac08ecae1b4a528b843bc4706ef3dd",
            target_accounts: ["phd_balance", "microbialecology"]
        },
        {
            uuid: "22be57d5c6f44e368258dc5ad6b425d3",
            target_accounts: ["the_brain_scientist", "drkaranrajan"]
        }
    ],
    followers_per_target: 100,
    follow_targets_before_parsing: true,
    likes_per_session: { min: 1, max: 3 },
    likes_on_target: { min: 1, max: 4 },
    stories_probability: 0.3,
    stories_per_session: { min: 1, max: 3 },
    delay_between_likes: { min: 5, max: 10 },
    delay_between_targets: { min: 60, max: 120 },
    delay_between_profiles: { min: 60, max: 120 },
    results_dir: 'instagram_results'
};

async function find_unliked_like(scope_handle) {
    const handle = await scope_handle.evaluateHandle(root => {
        const sections = root.querySelectorAll('section');
        for (const sec of sections) {
            const clickables = sec.querySelectorAll('button, div[role="button"], a[role="button"], a[role="link"]');
            const icon_buttons = [];
            for (const el of clickables) {
                if (el.querySelectorAll('svg').length !== 1) continue;
                const r = el.getBoundingClientRect();
                if (r.width < 16 || r.height < 16) continue;
                icon_buttons.push(el);
            }
            const top_level = icon_buttons.filter(b =>
                !icon_buttons.some(other => other !== b && other.contains(b))
            );
            if (top_level.length < 3 || top_level.length > 5) continue;

            const heart_btn = top_level[0];
            const heart_svg = heart_btn.querySelector('svg');
            if (!heart_svg) continue;

            const path = heart_svg.querySelector('path');
            if (path) {
                const fill = window.getComputedStyle(path).fill || '';
                const m = fill.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
                if (m && +m[1] > 200 && +m[2] < 100 && +m[3] < 100) continue;
            }
            return heart_svg;
        }
        return null;
    });
    return handle.asElement();
}

function random_range(min, max) {
    return min + Math.random() * (max - min);
}

function random_int(min, max) {
    return Math.floor(random_range(min, max + 1));
}

function pick_random(arr) {
    return arr[Math.floor(Math.random() * arr.length)];
}

async function sleep(seconds) {
    return new Promise(resolve => setTimeout(resolve, seconds * 1000));
}

async function human_delay(min_ms = 50, max_ms = 200) {
    const mu = Math.log((min_ms + max_ms) / 2);
    const sigma = random_range(0.3, 0.6);
    let delay = Math.exp(mu + sigma * (Math.random() - 0.5) * 2);
    delay = Math.min(max_ms, Math.max(min_ms, delay));
    await new Promise(resolve => setTimeout(resolve, delay));
}

async function ensure_dir(dir) {
    await fs.mkdir(dir, { recursive: true });
}

async function dump_state(page, label) {
    try {
        const dir = path.join(__dirname, 'debug');
        await ensure_dir(dir);
        const stamp = new Date().toISOString().replace(/[:.]/g, '-');
        const png = path.join(dir, `${stamp}_${label}.png`);
        const html = path.join(dir, `${stamp}_${label}.html`);
        await page.screenshot({ path: png, fullPage: false });
        const body = await page.evaluate(() => document.documentElement.outerHTML);
        await fs.writeFile(html, body);
        console.log(`📸 dump [${label}] → ${png}`);
    } catch (e) {
        console.warn(`📸 dump [${label}] failed: ${e.message}`);
    }
}

function csv_escape(value) {
    if (value === null || value === undefined) return '';
    const s = String(value);
    if (/[",\n\r]/.test(s)) return '"' + s.replace(/"/g, '""') + '"';
    return s;
}

function wind_mouse_trajectory(start, end, options = {}) {
    const {
        gravity = 9,
        wind = 3,
        max_step = 15,
        target_area = 12,
        min_wait_ms = 5,
        max_wait_ms = 12
    } = options;

    let x = start.x, y = start.y;
    let v_x = 0, v_y = 0;
    let w_x = 0, w_y = 0;
    let M = max_step;

    const points = [];
    let prev_x = Math.round(x), prev_y = Math.round(y);

    let safety = 0;
    while (safety++ < 10000) {
        const dist = Math.hypot(end.x - x, end.y - y);
        if (dist < 1) break;

        const w_mag = Math.min(wind, dist);
        if (dist >= target_area) {
            w_x = w_x / Math.sqrt(3) + (Math.random() * 2 - 1) * w_mag / Math.sqrt(5);
            w_y = w_y / Math.sqrt(3) + (Math.random() * 2 - 1) * w_mag / Math.sqrt(5);
        } else {
            w_x /= Math.sqrt(2);
            w_y /= Math.sqrt(2);
            if (M < 3) M = Math.random() * 3 + 3;
            else M /= Math.sqrt(5);
        }

        v_x += w_x + gravity * (end.x - x) / dist;
        v_y += w_y + gravity * (end.y - y) / dist;

        const v_mag = Math.hypot(v_x, v_y);
        if (v_mag > M) {
            const v_clip = M / 2 + Math.random() * M / 2;
            v_x = (v_x / v_mag) * v_clip;
            v_y = (v_y / v_mag) * v_clip;
        }

        x += v_x;
        y += v_y;

        const rx = Math.round(x);
        const ry = Math.round(y);
        if (rx !== prev_x || ry !== prev_y) {
            const wait = min_wait_ms + Math.random() * (max_wait_ms - min_wait_ms);
            points.push({ x: rx, y: ry, wait });
            prev_x = rx; prev_y = ry;
        }
    }

    points.push({ x: Math.round(end.x), y: Math.round(end.y), wait: 5 });
    return points;
}

async function move_mouse_human(page, target, options = {}) {
    const current = await page.evaluate(() => ({
        x: window.__mouseX ?? window.innerWidth / 2,
        y: window.__mouseY ?? window.innerHeight / 2
    }));

    const trajectory = wind_mouse_trajectory(current, target, options);
    for (const p of trajectory) {
        await page.mouse.move(p.x, p.y);
        if (p.wait > 0) await new Promise(r => setTimeout(r, p.wait));
    }

    await page.evaluate(({ x, y }) => {
        window.__mouseX = x;
        window.__mouseY = y;
    }, target);
}

async function human_click(page, selector_or_handle, options = {}) {
    const {
        overshoot_chance = 0.3,
        scroll_into_view = true,
        post_click_delay = [120, 350]
    } = options;

    const handle = typeof selector_or_handle === 'string'
        ? await page.$(selector_or_handle)
        : selector_or_handle;

    if (!handle) throw new Error(`Element not found: ${selector_or_handle}`);

    if (scroll_into_view) {
        await handle.evaluate(el => el.scrollIntoView({ block: 'center', behavior: 'smooth' }));
        await human_delay(500, 1000);
    }

    const box = await handle.boundingBox();
    if (!box) throw new Error('Could not get element coordinates');

    const target = {
        x: box.x + random_range(box.width * 0.25, box.width * 0.75),
        y: box.y + random_range(box.height * 0.25, box.height * 0.75)
    };

    if (Math.random() < overshoot_chance) {
        const overshoot = {
            x: target.x + (Math.random() - 0.5) * random_range(15, 35),
            y: target.y + (Math.random() - 0.5) * random_range(15, 35)
        };
        await move_mouse_human(page, overshoot);
        await human_delay(40, 120);
        await move_mouse_human(page, target);
    } else {
        await move_mouse_human(page, target);
    }

    await human_delay(post_click_delay[0], post_click_delay[1]);

    await page.mouse.down();
    await human_delay(40, 120);

    if (Math.random() < 0.25) {
        await page.mouse.move(
            target.x + (Math.random() - 0.5) * 2,
            target.y + (Math.random() - 0.5) * 2
        );
    }
    await page.mouse.up();

    return { x: target.x, y: target.y };
}

async function human_scroll_window(page, options = {}) {
    const { scrolls = random_int(2, 5), back_chance = 0.2 } = options;

    for (let i = 0; i < scrolls; i++) {
        const distance = random_range(300, 800);
        await page.evaluate(d => window.scrollBy({ top: d, behavior: 'smooth' }), distance);
        await human_delay(900, 2200);

        if (Math.random() < back_chance) {
            const back = random_range(100, 280);
            await page.evaluate(d => window.scrollBy({ top: -d, behavior: 'smooth' }), back);
            await human_delay(500, 1100);
        }
    }
}

async function human_scroll_element(page, element_handle, distance) {
    const before = await element_handle.evaluate(el => el.scrollTop);

    await element_handle.evaluate((el, d) => {
        el.scrollTop = el.scrollTop + d;
        el.dispatchEvent(new WheelEvent('wheel', {
            deltaY: d,
            bubbles: true,
            cancelable: true
        }));
    }, distance);

    await human_delay(700, 1800);
    const after = await element_handle.evaluate(el => el.scrollTop);
    return after - before;
}

async function dismiss_overlays(page, max_passes = 3) {

    const MODAL_SCOPE = [
        'div[role="dialog"]',
        'div[role="alertdialog"]',
        '[aria-modal="true"]',
        'div[data-testid="cookie-policy-manage-dialog"]',
        'div[aria-label*="cookie" i]',
        'div[aria-label*="Cookie"]'
    ].join(', ');

    let dismissed = 0;
    for (let pass = 0; pass < max_passes; pass++) {
        const modals = await page.$$(MODAL_SCOPE);
        if (modals.length === 0) break;

        const before = modals.length;
        await page.keyboard.press('Escape').catch(() => { });
        await human_delay(600, 1400);

        const after_modals = await page.$$(MODAL_SCOPE);
        if (after_modals.length < before) {
            console.log(`🪟 Modal dismissed via ESC`);
            dismissed++;
            continue;
        }
        break;
    }
    return dismissed;
}

async function check_for_block(page) {
    const url = page.url();
    if (url.includes('/challenge/') || url.includes('/accounts/suspended/') ||
        url.includes('/accounts/disabled/')) {
        return { blocked: true, reason: 'redirect: ' + url };
    }

    const block_text = await page.evaluate(() => {
        const text = document.body.innerText.toLowerCase();
        const markers = [
            'we restricted certain activity',
            'try again later',
            'suspicious login attempt',
            'your account has been temporarily',
            'temporary action restriction',
            'suspicious login'
        ];
        for (const m of markers) if (text.includes(m)) return m;
        return null;
    });

    if (block_text) return { blocked: true, reason: 'text: ' + block_text };
    return { blocked: false };
}

async function check_logged_in(page) {
    const url = page.url();
    if (url.includes('/accounts/login') || url.includes('/accounts/emailsignup')) {
        return false;
    }
    const has_login_form = await page.$('input[name="username"]');
    return !has_login_form;
}

async function watch_random_stories(page, count) {
    console.log(`📺 Trying to watch ${count} stories...`);
    const story_buttons = await page.$$('div[role="menuitem"] button[role="button"], li button[role="button"]');
    const visible_stories = [];
    for (const btn of story_buttons) {
        const box = await btn.boundingBox();
        if (box && box.y < 250 && box.width > 30) visible_stories.push(btn);
    }

    if (visible_stories.length === 0) {
        const fallback = await page.$$('canvas');
        for (const c of fallback) {
            const box = await c.boundingBox();
            if (box && box.y < 250) visible_stories.push(c);
        }
    }

    if (visible_stories.length === 0) {
        console.log('   ⏭ No stories found, skipping');
        return 0;
    }

    let watched = 0;
    const to_watch = Math.min(count, visible_stories.length);
    for (let i = 0; i < to_watch; i++) {
        try {
            const story = pick_random(visible_stories);
            await human_click(page, story);
            await human_delay(3000, 7000);
            await page.keyboard.press('Escape');
            await human_delay(800, 1500);
            watched++;
            console.log(`   👀 Story ${watched}/${to_watch} watched`);
        } catch (e) {
            console.log(`   ⚠️ Error while watching story: ${e.message}`);
            await page.keyboard.press('Escape').catch(() => { });
        }
    }
    return watched;
}

async function browse_feed_and_like(page, target_likes) {
    console.log(`❤️ Feed warm-up: ${target_likes} likes`);
    await dismiss_overlays(page);

    if (!page.url().match(/instagram\.com\/?(\?|$)/)) {
        await page.goto('https://www.instagram.com/', {
            waitUntil: 'domcontentloaded',
            timeout: 30000
        }).catch(() => { });
        await human_delay(2000, 3500);
    }

    const articles_appeared = await page.waitForFunction(
        () => document.querySelectorAll('article').length > 0,
        { timeout: 12000 }
    ).then(() => true).catch(() => false);

    if (!articles_appeared) {

        for (let i = 0; i < 3; i++) {
            await human_scroll_window(page, { scrolls: 2 });
            const ok = await page.evaluate(() => document.querySelectorAll('article').length > 0);
            if (ok) break;
        }
    }

    let liked = 0;
    let scrolls_without_progress = 0;

    while (liked < target_likes && scrolls_without_progress < 5) {

        const articles = await page.$$('article');
        let new_like_done = false;

        for (const article of articles) {
            if (liked >= target_likes) break;
            try {

                const like_btn = await find_unliked_like(article);
                if (!like_btn) continue;

                const box = await like_btn.boundingBox();
                if (!box) continue;

                const in_view = await like_btn.evaluate(el => {
                    const r = el.getBoundingClientRect();
                    return r.top > 50 && r.bottom < window.innerHeight - 50;
                });
                if (!in_view) continue;

                if (Math.random() < 0.3) {
                    await human_delay(1500, 4000);
                }

                await human_click(page, like_btn);
                liked++;
                new_like_done = true;
                console.log(`   ❤️ Like ${liked}/${target_likes}`);

                const pause = random_range(
                    config.delay_between_likes.min,
                    config.delay_between_likes.max
                );
                console.log(`   ⏰ Pause ${pause.toFixed(1)} sec`);
                await sleep(pause);

                const block = await check_for_block(page);
                if (block.blocked) {
                    console.log(`   🚫 Blocked after like: ${block.reason}`);
                    return { liked, blocked: true };
                }

                break;
            } catch (e) {
                continue;
            }
        }

        if (!new_like_done) {
            scrolls_without_progress++;
            await human_scroll_window(page, { scrolls: random_int(2, 4), back_chance: 0.05 });
            await human_delay(1500, 3000);
        } else {
            scrolls_without_progress = 0;

            await human_scroll_window(page, { scrolls: random_int(1, 2), back_chance: 0.1 });
            await human_delay(800, 1800);
        }
    }

    console.log(`✅ Warm-up complete, likes: ${liked}`);
    if (liked === 0) await dump_state(page, 'feed_zero_likes');
    return { liked, blocked: false };
}

async function visit_target_profile(page, username) {
    console.log(`🎯 Opening profile @${username}`);
    await page.goto(`https://www.instagram.com/${username}/`, {
        waitUntil: 'domcontentloaded',
        timeout: 30000
    });
    await human_delay(2500, 4500);
    await dismiss_overlays(page);

    const not_found = await page.evaluate(() => {
        const t = document.body.innerText;
        return t.includes("Sorry, this page isn't available")
            || t.includes('Страница недоступна');
    });
    if (not_found) {
        console.log(`   ⚠️ Profile @${username} not found`);
        return false;
    }

    const block = await check_for_block(page);
    if (block.blocked) {
        console.log(`   🚫 Blocked while opening profile: ${block.reason}`);
        return false;
    }

    await human_scroll_window(page, { scrolls: random_int(2, 4) });
    return true;
}

async function like_target_posts(page, count) {
    console.log(`💜 Liking ${count} post(s) on the target account`);

    const post_links = await page.$$('a[href*="/p/"]');
    if (post_links.length === 0) {
        console.log('   ⏭ No posts visible (private account or no publications)');
        return 0;
    }

    let liked = 0;
    const indices = [];
    for (let i = 0; i < count && i < post_links.length; i++) {

        const idx = random_int(0, Math.min(11, post_links.length - 1));
        if (!indices.includes(idx)) indices.push(idx);
    }

    for (const idx of indices) {
        try {
            const post_links_fresh = await page.$$('a[href*="/p/"]');
            if (!post_links_fresh[idx]) continue;

            await human_click(page, post_links_fresh[idx]);
            await human_delay(2500, 4500);

            const post_root = await page.evaluateHandle(() => {
                const dialog = document.querySelector('div[role="dialog"]');
                if (dialog) {
                    const inner_article = dialog.querySelector('article');
                    return inner_article || dialog;
                }
                return document.querySelector('article');
            });
            const post_root_el = post_root.asElement();
            const like_btn = post_root_el ? await find_unliked_like(post_root_el) : null;
            if (like_btn) {
                await human_click(page, like_btn);
                liked++;
                console.log(`   ❤️ Like on post ${liked}/${count}`);
            } else {
                console.log(`   ⏭ Post already liked or like button not found, skipping`);
            }

            if (Math.random() < 0.4) {
                await page.keyboard.press('ArrowRight').catch(() => { });
                await human_delay(800, 1800);
            }

            await human_delay(1500, 3500);
            await page.keyboard.press('Escape');
            await human_delay(1200, 2200);

            const block = await check_for_block(page);
            if (block.blocked) {
                console.log(`   🚫 Blocked after liking a post: ${block.reason}`);
                return liked;
            }

            const pause = random_range(
                config.delay_between_likes.min,
                config.delay_between_likes.max
            );
            await sleep(pause);
        } catch (e) {
            console.log(`   ⚠️ Post-like error: ${e.message}`);
            await page.keyboard.press('Escape').catch(() => { });
        }
    }

    return liked;
}

function setup_friendship_interceptor(page) {
    const state = {
        last_profile_info: null,
        last_friendship_action: null,
        action_blocked: false
    };

    const handler = async (response) => {
        const url = response.url();

        if (/\/api\/v1\/users\/web_profile_info\//.test(url)) {
            try {
                const json = await response.json();
                const u = json?.data?.user;
                if (u) {
                    state.last_profile_info = {
                        username: u.username,
                        id: u.id,
                        followed_by_viewer: !!u.followed_by_viewer,
                        requested_by_viewer: !!u.requested_by_viewer,
                        is_private: !!u.is_private
                    };
                }
            } catch (e) { }
            return;
        }

        if (/\/api\/v1\/friendships\/(create|destroy)\//.test(url)) {
            try {
                const json = await response.json();
                state.last_friendship_action = { status: response.status(), body: json };
                if (json?.spam || json?.feedback_required || json?.error_type === 'feedback_required') {
                    state.action_blocked = true;
                }
            } catch (e) {
                state.last_friendship_action = { status: response.status(), body: null };
            }
        }
    };

    page.on('response', handler);
    return { state, cleanup: () => page.off('response', handler) };
}

async function fetch_profile_info(page, username) {
    return await page.evaluate(async (u) => {
        try {
            const res = await fetch(`/api/v1/users/web_profile_info/?username=${encodeURIComponent(u)}`, {
                headers: { 'X-IG-App-ID': '936619743392459' },
                credentials: 'include'
            });
            if (!res.ok) return { error: `HTTP ${res.status}` };
            const json = await res.json();
            const user = json?.data?.user;
            if (!user) return { error: 'no user in response body' };
            return {
                username: user.username,
                id: user.id,
                followed_by_viewer: !!user.followed_by_viewer,
                requested_by_viewer: !!user.requested_by_viewer,
                is_private: !!user.is_private
            };
        } catch (e) {
            return { error: e.message };
        }
    }, username);
}

async function ensure_following(page, target_username, friendship_iface) {
    const wait_for = async (predicate, max_ms, step = 200) => {
        const start = Date.now();
        while (Date.now() - start < max_ms) {
            const v = predicate();
            if (v) return v;
            await new Promise(r => setTimeout(r, step));
        }
        return predicate() || null;
    };

    const info = await fetch_profile_info(page, target_username);

    if (!info || info.error || !info.username) {
        console.log(`   ⚠️ Could not fetch profile info via API for @${target_username}: ${info?.error || 'no data'}`);
        return false;
    }

    console.log(`   🔎 API friendship state for @${target_username}: following=${info.followed_by_viewer}, requested=${info.requested_by_viewer}, private=${info.is_private}`);

    if (info.followed_by_viewer) {
        console.log(`   ✓ Already following @${target_username}`);
        return true;
    }
    if (info.requested_by_viewer) {
        console.log(`   ✓ Follow request already pending for @${target_username}`);
        return true;
    }

    await page.evaluate(() => window.scrollTo({ top: 0, behavior: 'instant' }));
    await human_delay(800, 1500);

    const handle = await page.evaluateHandle((username) => {
        const heading_candidates = Array.from(
            document.querySelectorAll('h1, h2, span')
        ).filter(el => {
            const text = (el.textContent || '').trim();
            if (text !== username) return false;
            const r = el.getBoundingClientRect();
            return r.top >= 0 && r.top < 500 && r.width > 30;
        });

        if (heading_candidates.length === 0) return null;
        const anchor = heading_candidates[0];

        let container = anchor;
        for (let depth = 0; depth < 10 && container.parentElement; depth++) {
            container = container.parentElement;
            const buttons = Array.from(container.querySelectorAll('button, div[role="button"]'));
            const candidates = buttons.filter(btn => {
                const r = btn.getBoundingClientRect();
                if (r.width < 70 || r.height < 28 || r.width > 400 || r.height > 80) return false;
                const text = (btn.textContent || '').trim();
                if (text.length === 0 || text.length > 60) return false;
                return btn.querySelectorAll('svg').length <= 1;
            });
            if (candidates.length === 0) continue;
            candidates.sort((a, b) => {
                const ra = a.getBoundingClientRect(), rb = b.getBoundingClientRect();
                if (Math.abs(ra.top - rb.top) > 5) return ra.top - rb.top;
                return ra.left - rb.left;
            });
            return candidates[0];
        }
        return null;
    }, target_username);

    const el = handle.asElement();
    if (!el) {
        console.log(`   ⚠️ Primary action button not found near @${target_username} username anchor`);
        await dump_state(page, `no_follow_btn_${target_username}`);
        return false;
    }

    friendship_iface.state.last_friendship_action = null;
    friendship_iface.state.action_blocked = false;

    console.log(`   ➕ Subscribing to @${target_username}...`);
    await human_click(page, el);
    await human_delay(2500, 4000);

    const after = await fetch_profile_info(page, target_username);
    if (after && !after.error) {
        if (after.followed_by_viewer) {
            console.log(`   ✓ Subscribed (API confirmed)`);
            return true;
        }
        if (after.requested_by_viewer) {
            console.log(`   ✓ Follow request sent (private account)`);
            return true;
        }
    }

    if (friendship_iface.state.action_blocked) {
        const body = friendship_iface.state.last_friendship_action?.body;
        const fb = body?.feedback_message || body?.feedback_title || 'feedback_required';
        console.log(`   🚫 Instagram action-blocked the follow: ${fb}`);
        return false;
    }

    console.log(`   ⚠️ Click sent but API still reports not following — silent block or click missed the toggle`);
    return false;
}

function setup_followers_interceptor(page) {
    const state = {
        has_next_page: true,
        api_seen: false,
        responses: 0,
        users_returned: 0,
        empty_with_next: 0,
        cap_detected: false
    };

    const handler = async (response) => {
        const url = response.url();
        const is_followers_api = /\/friendships\/\d+\/followers\//.test(url);
        const is_graphql = /edge_followed_by/.test(url);
        if (!is_followers_api && !is_graphql) return;

        try {
            const json = await response.json();
            state.api_seen = true;
            state.responses++;

            let batch_size = 0;
            let has_next = state.has_next_page;

            if ('next_max_id' in json) {
                has_next = !!json.next_max_id;
                if (Array.isArray(json.users)) {
                    batch_size = json.users.length;
                    state.users_returned += batch_size;
                }
            } else if (json?.data?.user?.edge_followed_by?.page_info) {
                const pi = json.data.user.edge_followed_by.page_info;
                has_next = !!pi.has_next_page;
                const edges = json.data.user.edge_followed_by.edges;
                if (Array.isArray(edges)) {
                    batch_size = edges.length;
                    state.users_returned += batch_size;
                }
            }

            state.has_next_page = has_next;

            if (has_next && batch_size === 0) {
                state.empty_with_next++;
                if (state.empty_with_next >= 1) state.cap_detected = true;
            } else {
                state.empty_with_next = 0;
            }
        } catch (e) {
        }
    };

    page.on('response', handler);
    return { state, cleanup: () => page.off('response', handler) };
}

async function parse_followers(page, target_username, max_count) {
    console.log(`📋 Parsing followers of @${target_username} (target: ${max_count})`);

    await dismiss_overlays(page);

    const { state: api_state, cleanup: detach_interceptor } = setup_followers_interceptor(page);
    try {
        return await _parse_followers_impl(page, target_username, max_count, api_state);
    } finally {
        detach_interceptor();
    }
}

async function _parse_followers_impl(page, target_username, max_count, api_state) {
    const followers_handle = await page.evaluateHandle((target) => {
        const followers_re = new RegExp(`^/${target}/followers/?(\\?|$)`, 'i');

        const anchors = Array.from(document.querySelectorAll('a[href]'));
        for (const a of anchors) {
            if (followers_re.test(a.getAttribute('href') || '')) return a;
        }

        const all_clickable = Array.from(document.querySelectorAll(
            'a, button, div[role="button"], a[role="link"]'
        ));
        const counters = all_clickable.filter(el => {
            const t = (el.innerText || '').trim();
            if (!t || t.length > 60 || !/\d/.test(t)) return false;
            const r = el.getBoundingClientRect();
            return r.width > 0 && r.height > 0 && r.top < window.innerHeight;
        });

        for (const seed of counters) {
            let node = seed;
            for (let depth = 0; depth < 8 && node.parentElement; depth++) {
                const parent = node.parentElement;
                const row = [];
                for (const child of parent.children) {
                    const hit = counters.find(c => child === c || child.contains(c));
                    if (hit) row.push(hit);
                }
                if (row.length >= 2 && row.length <= 4) {
                    return row[row.length - 2];
                }
                node = parent;
            }
        }
        return null;
    }, target_username);

    const followers_link = followers_handle.asElement();
    if (!followers_link) {
        console.log('   ❌ "Followers" button not found');
        await dump_state(page, `target_${target_username}_no_followers_btn`);
        return [];
    }
    await human_click(page, followers_link);
    await human_delay(1800, 3000);

    await page.waitForSelector('div[role="dialog"]', { timeout: 10000 }).catch(() => { });

    const got_users = await page.waitForFunction(() => {
        const dialog = document.querySelector('div[role="dialog"]');
        if (!dialog) return false;
        return dialog.querySelectorAll('a[role="link"][href^="/"]').length >= 3;
    }, { timeout: 15000 }).then(() => true).catch(() => false);

    if (!got_users) {
        console.log('   ❌ Followers modal did not load the list within 15 sec');
        return [];
    }
    await human_delay(800, 1500);

    const find_scrollable = async () => {
        const handle = await page.evaluateHandle(() => {
            const dialog = document.querySelector('div[role="dialog"]');
            if (!dialog) return null;
            const all = dialog.querySelectorAll('div');
            let best = null, best_count = 0;
            for (const el of all) {
                const s = window.getComputedStyle(el);
                if (s.overflowY !== 'auto' && s.overflowY !== 'scroll') continue;
                const links = el.querySelectorAll('a[role="link"][href^="/"]').length;
                if (links > best_count) { best = el; best_count = links; }
            }
            return best;
        });
        return handle.asElement();
    };

    let scrollable = await find_scrollable();
    if (!scrollable) {
        console.log('   ❌ Scrollable container of the modal not found');
        return [];
    }
    console.log(`   📜 Container found, initial scrollHeight: ${await scrollable.evaluate(el => el.scrollHeight)}px`);

    const modal_box = await scrollable.boundingBox();
    if (modal_box) {
        await move_mouse_human(page, {
            x: modal_box.x + modal_box.width / 2,
            y: modal_box.y + modal_box.height / 2
        });
        await human_delay(200, 500);
    }

    const collected = new Map();
    let stagnation = 0;
    const max_stagnation = 5;
    let iteration = 0;
    const max_iterations = 200;

    while (collected.size < max_count && stagnation < max_stagnation && iteration < max_iterations) {
        iteration++;

        const batch = await page.evaluate(() => {
            const dialog = document.querySelector('div[role="dialog"]');
            if (!dialog) return [];
            const links = dialog.querySelectorAll('a[role="link"][href^="/"]');
            const seen = new Map();

            const skip = new Set(['explore', 'reels', 'direct', 'accounts', 'p', 'tv', 'stories', 'about']);

            const button_words = new Set([
                'Follow', 'Following', 'Message', 'Requested', 'Subscribed', 'Subscribe',
                'Подписаться', 'Подписки', 'Подписан', 'Подписана', 'Запрос', 'Сообщение',
                'Отписаться', 'Subscribirse', 'Seguir', 'Suivre'
            ]);

            for (const a of links) {
                const href = a.getAttribute('href');
                const m = href && href.match(/^\/([A-Za-z0-9._]+)\/?$/);
                if (!m) continue;
                const username = m[1];
                if (skip.has(username)) continue;
                if (seen.has(username)) continue;

                let row = a;
                for (let depth = 0; depth < 10 && row.parentElement; depth++) {
                    const parent = row.parentElement;
                    const sibling_links = parent.querySelectorAll('a[role="link"][href^="/"]');
                    let other = 0;
                    for (const l of sibling_links) {
                        const lm = l.getAttribute('href').match(/^\/([A-Za-z0-9._]+)\/?$/);
                        if (lm && lm[1] !== username && !skip.has(lm[1])) { other++; break; }
                    }
                    if (other > 0) break;
                    row = parent;
                }

                const lines = (row.innerText || '')
                    .split(/\n+/).map(s => s.trim()).filter(Boolean);
                let full_name = '';
                for (const line of lines) {
                    if (line === username) continue;
                    if (button_words.has(line)) continue;
                    if (line.length > 150) continue;
                    full_name = line;
                    break;
                }

                const verified = !!row.querySelector('svg[aria-label="Verified"], svg[aria-label*="Verif"], svg[aria-label="Подтвержденный"]');
                const is_private = !!row.querySelector('svg[aria-label="Private"]');

                seen.set(username, { username, full_name, verified, is_private });
            }
            return Array.from(seen.values());
        });

        const before = collected.size;
        for (const u of batch) {
            if (collected.size >= max_count) break;
            if (!collected.has(u.username)) collected.set(u.username, u);
        }
        const added = collected.size - before;

        if (added === 0) stagnation++;
        else stagnation = 0;

        const api_tail = api_state.api_seen
            ? ` [api: ${api_state.responses} resp, ${api_state.users_returned} users, next=${api_state.has_next_page}${api_state.cap_detected ? ', CAP' : ''}]`
            : '';
        process.stdout.write(`   📊 Collected: ${collected.size}/${max_count} (+${added})${api_tail}\r`);

        if (collected.size >= max_count) break;

        if (api_state.api_seen && (!api_state.has_next_page || api_state.cap_detected)) {
            const reason = api_state.cap_detected
                ? `server cap (empty batch with next_max_id present, returned ${api_state.users_returned} users total)`
                : 'has_next_page=false';
            console.log(`\n   🛑 Pagination closed by Instagram: ${reason}`);
            break;
        }

        const wheel_delta = random_range(400, 700);
        await page.mouse.wheel({ deltaY: wheel_delta });
        await human_delay(900, 1700);

        if (stagnation >= 1) {
            await scrollable.evaluate(el => {
                const links = el.querySelectorAll('a[role="link"][href^="/"]');
                if (links.length > 0) {
                    links[links.length - 1].scrollIntoView({ block: 'end' });
                }
            });
            await human_delay(700, 1300);

            const fresh = await find_scrollable();
            if (fresh) scrollable = fresh;
        }

        if (Math.random() < 0.2) await human_delay(1500, 3500);
    }

    console.log(`\n   ✅ Followers collected: ${collected.size}`);

    await page.keyboard.press('Escape').catch(() => { });
    await human_delay(800, 1500);

    return Array.from(collected.values()).slice(0, max_count);
}

async function save_followers(profile_uuid, target, users) {
    const dir = path.join(__dirname, config.results_dir);
    await ensure_dir(dir);

    const stamp = new Date().toISOString().replace(/[:.]/g, '-');
    const base = `${target}_${profile_uuid.slice(0, 8)}_${stamp}`;

    const json_path = path.join(dir, `${base}.json`);
    const csv_path = path.join(dir, `${base}.csv`);

    const json_payload = {
        target_account: target,
        parsed_by_profile: profile_uuid,
        timestamp: new Date().toISOString(),
        total: users.length,
        followers: users
    };
    await fs.writeFile(json_path, JSON.stringify(json_payload, null, 2), 'utf8');

    const csv_lines = ['username,full_name,verified,is_private'];
    for (const u of users) {
        csv_lines.push([
            csv_escape(u.username),
            csv_escape(u.full_name),
            csv_escape(u.verified),
            csv_escape(u.is_private)
        ].join(','));
    }
    await fs.writeFile(csv_path, csv_lines.join('\n'), 'utf8');

    console.log(`💾 JSON: ${json_path}`);
    console.log(`💾 CSV : ${csv_path}`);
}

async function process_target(page, target, profile_uuid) {
    const friendship_iface = setup_friendship_interceptor(page);
    try {
        const ok = await visit_target_profile(page, target);
        if (!ok) return { target, success: false };

        let followed = false;
        if (config.follow_targets_before_parsing) {
            followed = await ensure_following(page, target, friendship_iface);
            if (followed) await human_delay(3000, 6000);
        }

        const target_likes = random_int(config.likes_on_target.min, config.likes_on_target.max);
        if (target_likes > 0) {
            await like_target_posts(page, target_likes);
        }

        await page.goto(`https://www.instagram.com/${target}/`, {
            waitUntil: 'domcontentloaded',
            timeout: 30000
        }).catch(() => { });
        await human_delay(1500, 3000);

        const users = await parse_followers(page, target, config.followers_per_target);
        if (users.length > 0) {
            await save_followers(profile_uuid, target, users);
        }

        return { target, success: true, count: users.length, followed };
    } finally {
        friendship_iface.cleanup();
    }
}

async function process_profile(profile_cfg, idx, total) {
    console.log(`\n${'='.repeat(80)}`);
    console.log(`📋 Profile ${idx + 1}/${total} — UUID ${profile_cfg.uuid}`);
    console.log(`   Targets: ${profile_cfg.target_accounts.join(', ')}`);
    console.log(`${'='.repeat(80)}`);

    await octo_stop_profile(profile_cfg.uuid).catch(() => { });
    await sleep(3);

    let ws_data;
    try {
        ws_data = await octo_start_profile(profile_cfg.uuid);
    } catch (e) {
        const body = e.response?.data;
        const body_str = body ? (typeof body === 'string' ? body : JSON.stringify(body)) : '';
        console.error(`❌ Failed to start profile: ${e.message}${body_str ? ' | Octo: ' + body_str : ''}`);
        return { uuid: profile_cfg.uuid, status: 'start_failed', error: body_str || e.message };
    }

    if (!ws_data?.ws_endpoint) {
        console.error('❌ Octo did not return a ws_endpoint');
        await octo_stop_profile(profile_cfg.uuid).catch(() => { });
        return { uuid: profile_cfg.uuid, status: 'no_ws' };
    }

    let browser;
    try {
        browser = await puppeteer.connect({
            browserWSEndpoint: ws_data.ws_endpoint,
            defaultViewport: null,
            protocolTimeout: 600000
        });
    } catch (e) {
        console.error(`❌ Puppeteer connect: ${e.message}`);
        await octo_stop_profile(profile_cfg.uuid).catch(() => { });
        return { uuid: profile_cfg.uuid, status: 'connect_failed' };
    }

    try {
        const ctx = browser.defaultBrowserContext();
        await ctx.overridePermissions('https://www.instagram.com', []);
        await ctx.overridePermissions('https://instagram.com', []);
    } catch (e) {
        console.warn(`⚠️ overridePermissions: ${e.message}`);
    }

    let stats = { uuid: profile_cfg.uuid, status: 'ok', targets: [], likes: 0, stories: 0 };

    try {
        const page = await browser.newPage();
        await page.setViewport({ width: 1280, height: 900 });

        await page.goto('https://www.instagram.com/', {
            waitUntil: 'domcontentloaded',
            timeout: 45000
        });
        await human_delay(2500, 4500);
        await dismiss_overlays(page);

        await human_delay(800, 1500);
        await dismiss_overlays(page);

        if (!await check_logged_in(page)) {
            console.error('❌ Profile is not logged into Instagram');
            stats.status = 'not_logged_in';
            return stats;
        }

        const block = await check_for_block(page);
        if (block.blocked) {
            console.error(`❌ Account is restricted: ${block.reason}`);
            stats.status = 'blocked';
            return stats;
        }

        if (Math.random() < config.stories_probability) {
            const cnt = random_int(config.stories_per_session.min, config.stories_per_session.max);
            stats.stories = await watch_random_stories(page, cnt);
            await human_delay(1500, 3000);
        }

        const feed_likes = random_int(config.likes_per_session.min, config.likes_per_session.max);
        const feed_result = await browse_feed_and_like(page, feed_likes);
        stats.likes += feed_result.liked;
        if (feed_result.blocked) {
            stats.status = 'blocked_during_feed';
            return stats;
        }

        for (let i = 0; i < profile_cfg.target_accounts.length; i++) {
            const target = profile_cfg.target_accounts[i];
            try {
                const r = await process_target(page, target, profile_cfg.uuid);
                stats.targets.push(r);
            } catch (e) {
                console.error(`❌ Target @${target} error: ${e.message}`);
                stats.targets.push({ target, success: false, error: e.message });
            }

            if (i < profile_cfg.target_accounts.length - 1) {
                const pause = random_range(
                    config.delay_between_targets.min,
                    config.delay_between_targets.max
                );
                console.log(`⏰ Pause between targets: ${pause.toFixed(1)} sec`);
                await sleep(pause);
            }
        }

    } catch (e) {
        console.error(`❌ Profile processing error: ${e.message}`);
        stats.status = 'error';
        stats.error = e.message;
    } finally {
        await octo_stop_profile(profile_cfg.uuid).catch(() => { });
        await sleep(2);
    }

    return stats;
}

async function check_limits(response) {
    const header = response.headers.ratelimit;
    if (!header) return;
    const entries = header.split(',').map(s => s.trim());
    for (const e of entries) {
        const r_match = e.match(/;r=(\d+)/);
        const t_match = e.match(/;t=(\d+)/);
        if (!r_match || !t_match) continue;
        const remaining = parseInt(r_match[1], 10);
        const window_s = parseInt(t_match[1], 10);
        if (remaining < 5) {
            console.log(`⏳ Octo rate-limit, waiting ${window_s + 1} sec`);
            await sleep(window_s + 1);
        }
    }
}

async function octo_start_profile(uuid) {
    const res = await axios({
        method: 'post',
        url: `${config.octo_local_api_base_url}/start`,
        headers: { 'Content-Type': 'application/json' },
        data: {
            uuid,
            headless: config.headless_mode,
            debug_port: true,
            timeout: 60
        }
    });
    await check_limits(res);
    return res.data;
}

async function octo_stop_profile(uuid) {
    const res = await axios({
        method: 'post',
        url: `${config.octo_local_api_base_url}/stop`,
        headers: { 'Content-Type': 'application/json' },
        data: { uuid }
    });
    await check_limits(res);
    return res.data;
}

(async () => {
    console.log('🚀 Octo Instagram Parser & Liker');
    console.log(`   Profiles: ${config.profiles.length}`);
    console.log(`   Followers per target: ${config.followers_per_target}`);
    console.log(`   Feed likes: ${config.likes_per_session.min}${config.likes_per_session.max}`);
    console.log('');

    await ensure_dir(path.join(__dirname, config.results_dir));

    const all_stats = [];
    for (let i = 0; i < config.profiles.length; i++) {
        const stats = await process_profile(config.profiles[i], i, config.profiles.length);
        all_stats.push(stats);

        if (i < config.profiles.length - 1) {
            const pause = random_range(
                config.delay_between_profiles.min,
                config.delay_between_profiles.max
            );
            console.log(`\n⏰ Pause before the next profile: ${pause.toFixed(1)} sec`);
            await sleep(pause);
        }
    }

    console.log(`\n${'='.repeat(80)}`);
    console.log('📊 SUMMARY');
    console.log('='.repeat(80));
    for (const s of all_stats) {
        console.log(`\n${s.uuid}${s.status}`);
        console.log(`   Likes: ${s.likes ?? 0}, stories: ${s.stories ?? 0}`);
        if (s.targets) {
            for (const t of s.targets) {
                if (t.success) console.log(`   ✅ @${t.target}: ${t.count} followers`);
                else console.log(`   ❌ @${t.target}: ${t.error || 'fail'}`);
            }
        }
    }

    const summary_path = path.join(__dirname, config.results_dir, `_summary_${Date.now()}.json`);
    await fs.writeFile(summary_path, JSON.stringify(all_stats, null, 2));
    console.log(`\n📄 Summary report: ${summary_path}`);
    console.log('🎉 Done.');
})();

Масштабирование и безопасность аккаунтов

Чек-лист перед запуском:

1. Профили прогреты.  Аккаунту минимум 2–3 недели, есть аватарка, посты, какая-то лента подписок.
2. Прокси не серверные. Используйте резидентные или мобильные.
3. Селекторы актуальны. Прежде чем запускать на большом количестве аккаунтов, протестируйте скрипт на нескольких и проверьте, что селекторы корректные.
4. Паузы между запусками. Наш скрипт обрабатывает профили в config.profiles последовательно, с паузой 60–120 секунд. При большем количестве аккаунтов увеличьте этот интервал.

Заключение

В одном скрипте мы покрыли два самых востребованных кейса: парсинг подписчиков (для аудита конкурентов и сбора lookalike-аудиторий) и масслайкинг (для прогрева). Их легко расширить:

  • Парсинг лайкнувших/комментаторов под конкретным постом — для самой «горячей» выборки.

  • Сбор bio + email каждого комментатора. Это можно выделить в отдельный пайплайн, чтобы не нагружать парсер-аккаунт лишними действиями.

  • Сравнение списков подписчиков нескольких конкурентов и поиск пересечений — это уже простая логика на чистом Node.js поверх собранных JSON.

  • Слой кэша: чтобы не парсить одних и тех же подписчиков повторно при следующих прогонах.

Также если вы работаете с большими объемами, то легко можете переделать скрипт для параллельного запуска.

Залог успешного парсинга — многослойная система. Octo Browser дает корректный отпечаток и изоляцию профилей, rebrowser-puppeteer — автоматизацию с фиксами от утечек признаков автоматизации, алгоритм WindMouse — реалистичную моторику, поведенческий слой — отвлечения и паузы. В итоге устойчивость системы определяется не отдельным компонентом, а тем, насколько хорошо они работают вместе.

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

Что и зачем парсят в Instagram

Изучив рынок инструментов и сервисов 2025–2026 гг., можно выделить шесть основных объектов парсинга Instagram:

Данные

Поля

Для чего собирают?

Подписчики аккаунта

username, full_name, verified, is_private, bio

Аудит конкурентов, поиск горячих лидов, изучение аудитории для Meta Ads

Подписки аккаунта

username, full_name, verified, is_private, bio

Анализ целевой аудитории конкурентов и инфлюенсеров

Лайкнувшие пост

username, full_name

Выборка с более высокой вовлеченностью, чем у подписчиков 

Комментаторы 

username, текст комментария

DM-рассылки и анализ тональности

Посты по хештегу/гео

post_url, лайки, caption

Контент-аналитика, поиск UGC

Профиль целиком

bio, posts, ER

Аудит инфлюенсера перед сотрудничеством

Основные сценарии использования:

1. Аудит конкурентов. Можно спарсить подписчиков 3–5 конкурентов, убрать дубли и найти пересечения. Cамые качественные лиды — люди, подписанные на несколько конкурентов сразу.

2. Проверка инфлюенсера/блогера. Перед оплатой коллаборации можно собрать подписчиков и оценить, насколько аудитория живая.

3. DM-рассылки и холодная почта. В нишах SaaS, marketing и e-commerce у 15–35% профилей в bio есть публичный email-адрес.
4. Аудитория Lookalike (похожая аудитория) в Meta Ads. Для ее создания список из имен пользователей (юзернеймов) конвертируется в «посевную» пользовательскую аудиторию (Custom Audience seed). 

5. Прогрев аккаунтов через масслайкинг. Для роста охвата и видимости нормальной активности.

Почему обычные боты палятся в 2026-м

Главное изменение последних лет — Instagram сделал контент скрытым, если нет авторизации в аккаунте. Раньше можно было дергать публичные эндпоинты /?__a=1 без сессии. В 2026-м без авторизации не показывают даже подписчиков public-аккаунта.

Что потребуется для нашего скрипта:

1. Живой залогиненный аккаунт — желательно прогретый, а не только что зарегистрированный. Octo Browser с этим помогает: можно создать аккаунт, авторизоваться вручную, дать ему отстояться и только потом подключать к автоматизации.

2. Каждый аккаунт = отдельный отпечаток. Запускать 20 аккаунтов из одного Chrome — гарантированный бан. В Octo вы можете использовать изолированные профили с разными отпечатками (параметрами WebGL, разрешением экрана, шрифтами, user-agent и т. д.).

3. Прокси на профиль. В идеале мобильные или резидентные. Использовать IP из пула дата-центров крайне рискованно. 

Но даже с правильным отпечатком браузера и надежным прокси puppeteer-бот не выживет без эмуляции человеческих действий. Антибот-системы анализируют:

  • Частотный анализ. Сколько лайков в час, сколько подписок в день, какие интервалы между действиями. Серверная аналитика поймает бота, который лайкает каждые 30 секунд, почти сразу.

  • Отпечаток браузера. Поэтому использование антидетекта критически важно.

  • TLS / network fingerprint. Поэтому важны нормальные прокси и некастомный TLS-стек.

  • Уязвимости библиотек автоматизации (navigator.webdriver, CDP-присутствие, специфичные следы в Error.stack).

Практический вывод: главные причины банов в 2026-м году — это частотные паттерны (слишком много действий за короткое время, слишком ровные интервалы), отпечаток и прокси. Эмуляция человеческих движений мышки, скроллов или набора текста пока необязательна. Но практически эти действия могут использоваться для анализа и начисления очков фрод-скора аккаунту.

Поэтому составляющие архитектуры нашего скрипта такие:

1. Octo Browser — изолированный отпечаток, прокси, профиль с реальной Instagram-сессией.
2. Rebrowser-puppeteer — форк Puppeteer без характерных утечек, которые достаточно легко обнаружить.
3. WindMouse — физическая модель движения мыши. 
4. Поведенческий слой — случайные просмотры сторис и лайки на постах перед парсингом, паузы по логнормальному распределению, overshoot (промахи мимо цели), реакции на чекпойнты.

WindMouse: почему лучше кубических кривых Безье

В предыдущей статье мы использовали кубические кривые Безье для движения мыши. Это базовый подход, но у него есть слабые места: с двумя контрольными точками кривая получается гладкой и предсказуемой. Фигура всегда одна и та же, скорость управляется отдельным easing-параметром, что выглядит слишком равномерно, а микродрожание добавляется поверх отдельным слоем — и видно, что это два разных слоя: гладкая кривая плюс jitter-шум.

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

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

Дополняем «человечность» еще двумя приемами: overshoot — в 30% случаев целимся не точно в кнопку, а проскакиваем чуть мимо и возвращаемся, и микродрожание во время «нажатия» — между mouse.down() и mouse.up() смещаем курсор на 1–2 пикселя в случайную сторону.

Реализация WindMouse в скрипте — функция wind_mouse_trajectory, она генерирует массив точек траектории, по которым потом проходит page.mouse.move.

Готовый скрипт для парсинга подписчиков целевых аккаунтов и одновременного прогрева

Допустим, мы имеем массив UUID профилей Octo, уже залогиненных в аккаунтах Instagram. Наш скрипт будет заходить в каждый, прогревать аккаунт лайками в ленте и просмотром сторис. Уже после прогрева скрипт будет открывать профили Instagram, которые мы указали как цели для парсинга. Там мы лайкаем посты, подписываемся и, наконец, парсим список подписчиков в JSON и CSV. И все это — с естественной траекторией мыши, случайными задержками и проверкой на чекпойнты. Так Instagram не сможет отличить вашу автоматизацию от обычных действий пользователей.

Подготовка профилей

1. Создайте в Octo один или несколько профилей с прокси. Лучше резидентными или мобильными.

2. Вручную откройте каждый профиль в Octo. Авторизуйтесь или зарегистрируйтесь в Instagram. Два-три дня пользуйтесь аккаунтами, как обычный человек (ставьте лайки, смотрите сторис, на кого-то подписывайтесь).
3. Скопируйте UUID профилей из Octo. Они потребуются для заполнения конфигурации в скрипте.

Запуск

  1. Скачайте и установите VS Code.

  2. Скачайте и установите node.js.

  3. Создайте папку в удобном для вас месте и назовите ее, например octo_instagram_scraper.

  4. Откройте эту папку в VS Code.

  5. Создайте файл с расширением .js. Лучше называть его по имени действия, которое будет выполнять код, чтобы не запутаться. Например, octo_instagram_scraper.js.

  6. Вставьте в файл код скрипта.

  7. Заполните конфигурацию в переменной config.

    UUID — это ID профилей Octo;


    target_accounts — аккаунты, которые будете парсить;

    followers_per_target — число подписчиков целевого аккаунта, данные которых вы хотите собирать за проход.

    Остальные параметры в config отвечают за правдоподобное поведение парсера. Можете поэкспериментировать с ними либо не менять.

Заполните конфигурацию в переменной config.
  1. Откройте терминал и выполните команду npm i rebrowser-puppeteer axios, чтобы установить зависимости для NodeJS.

Откройте терминал и выполните команду npm i rebrowser-puppeteer axios

Если VS Code выдает ошибку — откройте от имени администратора Window PowerShell, введите там команду Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned и подтвердите. Затем повторите предыдущий пункт.

  1. Запустите клиент Octo Browser.

  2. . Запустите скрипт в Visual Studio (Ctrl/Cmd + F5) и дождитесь окончания работы скрипта.

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

Сводка по результатам работы парсера. Для примера мы прошли по два аккаунта с каждого нашего профиля: 3 из 4 проходов успешны

Сводка по результатам работы парсера. Для примера мы прошли по два аккаунта с каждого нашего профиля: 3 из 4 проходов успешны

Учитывайте, что Instagram может ограничивать список выдачи подписчиков и это зависит от многих факторов: прогрева аккаунта, количества взаимных контактов, наличия подписки на целевой аккаунт и т. д. Если что-то не сработало — экспериментируйте с прогревом либо меняйте аккаунт/прокси.

Результаты работы парсера в CSV-формате

Результаты работы парсера в CSV-формате

Результаты работы парсера в JSON-формате

Результаты работы парсера в JSON-формате

JSON удобнее для дальнейшей обработки кодом, CSV — для импорта в Excel, Google Sheets, CRM или прямой загрузки в Meta Ads как Custom Audience.

Код скрипта

const axios = require('axios');
const puppeteer = require('rebrowser-puppeteer');
const fs = require('fs').promises;
const path = require('path');

const config = {
    octo_local_api_base_url: `http://localhost:58888/api/profiles`,
    headless_mode: false,
    profiles: [
        {
            uuid: "f7ac08ecae1b4a528b843bc4706ef3dd",
            target_accounts: ["phd_balance", "microbialecology"]
        },
        {
            uuid: "22be57d5c6f44e368258dc5ad6b425d3",
            target_accounts: ["the_brain_scientist", "drkaranrajan"]
        }
    ],
    followers_per_target: 100,
    follow_targets_before_parsing: true,
    likes_per_session: { min: 1, max: 3 },
    likes_on_target: { min: 1, max: 4 },
    stories_probability: 0.3,
    stories_per_session: { min: 1, max: 3 },
    delay_between_likes: { min: 5, max: 10 },
    delay_between_targets: { min: 60, max: 120 },
    delay_between_profiles: { min: 60, max: 120 },
    results_dir: 'instagram_results'
};

async function find_unliked_like(scope_handle) {
    const handle = await scope_handle.evaluateHandle(root => {
        const sections = root.querySelectorAll('section');
        for (const sec of sections) {
            const clickables = sec.querySelectorAll('button, div[role="button"], a[role="button"], a[role="link"]');
            const icon_buttons = [];
            for (const el of clickables) {
                if (el.querySelectorAll('svg').length !== 1) continue;
                const r = el.getBoundingClientRect();
                if (r.width < 16 || r.height < 16) continue;
                icon_buttons.push(el);
            }
            const top_level = icon_buttons.filter(b =>
                !icon_buttons.some(other => other !== b && other.contains(b))
            );
            if (top_level.length < 3 || top_level.length > 5) continue;

            const heart_btn = top_level[0];
            const heart_svg = heart_btn.querySelector('svg');
            if (!heart_svg) continue;

            const path = heart_svg.querySelector('path');
            if (path) {
                const fill = window.getComputedStyle(path).fill || '';
                const m = fill.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
                if (m && +m[1] > 200 && +m[2] < 100 && +m[3] < 100) continue;
            }
            return heart_svg;
        }
        return null;
    });
    return handle.asElement();
}

function random_range(min, max) {
    return min + Math.random() * (max - min);
}

function random_int(min, max) {
    return Math.floor(random_range(min, max + 1));
}

function pick_random(arr) {
    return arr[Math.floor(Math.random() * arr.length)];
}

async function sleep(seconds) {
    return new Promise(resolve => setTimeout(resolve, seconds * 1000));
}

async function human_delay(min_ms = 50, max_ms = 200) {
    const mu = Math.log((min_ms + max_ms) / 2);
    const sigma = random_range(0.3, 0.6);
    let delay = Math.exp(mu + sigma * (Math.random() - 0.5) * 2);
    delay = Math.min(max_ms, Math.max(min_ms, delay));
    await new Promise(resolve => setTimeout(resolve, delay));
}

async function ensure_dir(dir) {
    await fs.mkdir(dir, { recursive: true });
}

async function dump_state(page, label) {
    try {
        const dir = path.join(__dirname, 'debug');
        await ensure_dir(dir);
        const stamp = new Date().toISOString().replace(/[:.]/g, '-');
        const png = path.join(dir, `${stamp}_${label}.png`);
        const html = path.join(dir, `${stamp}_${label}.html`);
        await page.screenshot({ path: png, fullPage: false });
        const body = await page.evaluate(() => document.documentElement.outerHTML);
        await fs.writeFile(html, body);
        console.log(`📸 dump [${label}] → ${png}`);
    } catch (e) {
        console.warn(`📸 dump [${label}] failed: ${e.message}`);
    }
}

function csv_escape(value) {
    if (value === null || value === undefined) return '';
    const s = String(value);
    if (/[",\n\r]/.test(s)) return '"' + s.replace(/"/g, '""') + '"';
    return s;
}

function wind_mouse_trajectory(start, end, options = {}) {
    const {
        gravity = 9,
        wind = 3,
        max_step = 15,
        target_area = 12,
        min_wait_ms = 5,
        max_wait_ms = 12
    } = options;

    let x = start.x, y = start.y;
    let v_x = 0, v_y = 0;
    let w_x = 0, w_y = 0;
    let M = max_step;

    const points = [];
    let prev_x = Math.round(x), prev_y = Math.round(y);

    let safety = 0;
    while (safety++ < 10000) {
        const dist = Math.hypot(end.x - x, end.y - y);
        if (dist < 1) break;

        const w_mag = Math.min(wind, dist);
        if (dist >= target_area) {
            w_x = w_x / Math.sqrt(3) + (Math.random() * 2 - 1) * w_mag / Math.sqrt(5);
            w_y = w_y / Math.sqrt(3) + (Math.random() * 2 - 1) * w_mag / Math.sqrt(5);
        } else {
            w_x /= Math.sqrt(2);
            w_y /= Math.sqrt(2);
            if (M < 3) M = Math.random() * 3 + 3;
            else M /= Math.sqrt(5);
        }

        v_x += w_x + gravity * (end.x - x) / dist;
        v_y += w_y + gravity * (end.y - y) / dist;

        const v_mag = Math.hypot(v_x, v_y);
        if (v_mag > M) {
            const v_clip = M / 2 + Math.random() * M / 2;
            v_x = (v_x / v_mag) * v_clip;
            v_y = (v_y / v_mag) * v_clip;
        }

        x += v_x;
        y += v_y;

        const rx = Math.round(x);
        const ry = Math.round(y);
        if (rx !== prev_x || ry !== prev_y) {
            const wait = min_wait_ms + Math.random() * (max_wait_ms - min_wait_ms);
            points.push({ x: rx, y: ry, wait });
            prev_x = rx; prev_y = ry;
        }
    }

    points.push({ x: Math.round(end.x), y: Math.round(end.y), wait: 5 });
    return points;
}

async function move_mouse_human(page, target, options = {}) {
    const current = await page.evaluate(() => ({
        x: window.__mouseX ?? window.innerWidth / 2,
        y: window.__mouseY ?? window.innerHeight / 2
    }));

    const trajectory = wind_mouse_trajectory(current, target, options);
    for (const p of trajectory) {
        await page.mouse.move(p.x, p.y);
        if (p.wait > 0) await new Promise(r => setTimeout(r, p.wait));
    }

    await page.evaluate(({ x, y }) => {
        window.__mouseX = x;
        window.__mouseY = y;
    }, target);
}

async function human_click(page, selector_or_handle, options = {}) {
    const {
        overshoot_chance = 0.3,
        scroll_into_view = true,
        post_click_delay = [120, 350]
    } = options;

    const handle = typeof selector_or_handle === 'string'
        ? await page.$(selector_or_handle)
        : selector_or_handle;

    if (!handle) throw new Error(`Element not found: ${selector_or_handle}`);

    if (scroll_into_view) {
        await handle.evaluate(el => el.scrollIntoView({ block: 'center', behavior: 'smooth' }));
        await human_delay(500, 1000);
    }

    const box = await handle.boundingBox();
    if (!box) throw new Error('Could not get element coordinates');

    const target = {
        x: box.x + random_range(box.width * 0.25, box.width * 0.75),
        y: box.y + random_range(box.height * 0.25, box.height * 0.75)
    };

    if (Math.random() < overshoot_chance) {
        const overshoot = {
            x: target.x + (Math.random() - 0.5) * random_range(15, 35),
            y: target.y + (Math.random() - 0.5) * random_range(15, 35)
        };
        await move_mouse_human(page, overshoot);
        await human_delay(40, 120);
        await move_mouse_human(page, target);
    } else {
        await move_mouse_human(page, target);
    }

    await human_delay(post_click_delay[0], post_click_delay[1]);

    await page.mouse.down();
    await human_delay(40, 120);

    if (Math.random() < 0.25) {
        await page.mouse.move(
            target.x + (Math.random() - 0.5) * 2,
            target.y + (Math.random() - 0.5) * 2
        );
    }
    await page.mouse.up();

    return { x: target.x, y: target.y };
}

async function human_scroll_window(page, options = {}) {
    const { scrolls = random_int(2, 5), back_chance = 0.2 } = options;

    for (let i = 0; i < scrolls; i++) {
        const distance = random_range(300, 800);
        await page.evaluate(d => window.scrollBy({ top: d, behavior: 'smooth' }), distance);
        await human_delay(900, 2200);

        if (Math.random() < back_chance) {
            const back = random_range(100, 280);
            await page.evaluate(d => window.scrollBy({ top: -d, behavior: 'smooth' }), back);
            await human_delay(500, 1100);
        }
    }
}

async function human_scroll_element(page, element_handle, distance) {
    const before = await element_handle.evaluate(el => el.scrollTop);

    await element_handle.evaluate((el, d) => {
        el.scrollTop = el.scrollTop + d;
        el.dispatchEvent(new WheelEvent('wheel', {
            deltaY: d,
            bubbles: true,
            cancelable: true
        }));
    }, distance);

    await human_delay(700, 1800);
    const after = await element_handle.evaluate(el => el.scrollTop);
    return after - before;
}

async function dismiss_overlays(page, max_passes = 3) {

    const MODAL_SCOPE = [
        'div[role="dialog"]',
        'div[role="alertdialog"]',
        '[aria-modal="true"]',
        'div[data-testid="cookie-policy-manage-dialog"]',
        'div[aria-label*="cookie" i]',
        'div[aria-label*="Cookie"]'
    ].join(', ');

    let dismissed = 0;
    for (let pass = 0; pass < max_passes; pass++) {
        const modals = await page.$$(MODAL_SCOPE);
        if (modals.length === 0) break;

        const before = modals.length;
        await page.keyboard.press('Escape').catch(() => { });
        await human_delay(600, 1400);

        const after_modals = await page.$$(MODAL_SCOPE);
        if (after_modals.length < before) {
            console.log(`🪟 Modal dismissed via ESC`);
            dismissed++;
            continue;
        }
        break;
    }
    return dismissed;
}

async function check_for_block(page) {
    const url = page.url();
    if (url.includes('/challenge/') || url.includes('/accounts/suspended/') ||
        url.includes('/accounts/disabled/')) {
        return { blocked: true, reason: 'redirect: ' + url };
    }

    const block_text = await page.evaluate(() => {
        const text = document.body.innerText.toLowerCase();
        const markers = [
            'we restricted certain activity',
            'try again later',
            'suspicious login attempt',
            'your account has been temporarily',
            'temporary action restriction',
            'suspicious login'
        ];
        for (const m of markers) if (text.includes(m)) return m;
        return null;
    });

    if (block_text) return { blocked: true, reason: 'text: ' + block_text };
    return { blocked: false };
}

async function check_logged_in(page) {
    const url = page.url();
    if (url.includes('/accounts/login') || url.includes('/accounts/emailsignup')) {
        return false;
    }
    const has_login_form = await page.$('input[name="username"]');
    return !has_login_form;
}

async function watch_random_stories(page, count) {
    console.log(`📺 Trying to watch ${count} stories...`);
    const story_buttons = await page.$$('div[role="menuitem"] button[role="button"], li button[role="button"]');
    const visible_stories = [];
    for (const btn of story_buttons) {
        const box = await btn.boundingBox();
        if (box && box.y < 250 && box.width > 30) visible_stories.push(btn);
    }

    if (visible_stories.length === 0) {
        const fallback = await page.$$('canvas');
        for (const c of fallback) {
            const box = await c.boundingBox();
            if (box && box.y < 250) visible_stories.push(c);
        }
    }

    if (visible_stories.length === 0) {
        console.log('   ⏭ No stories found, skipping');
        return 0;
    }

    let watched = 0;
    const to_watch = Math.min(count, visible_stories.length);
    for (let i = 0; i < to_watch; i++) {
        try {
            const story = pick_random(visible_stories);
            await human_click(page, story);
            await human_delay(3000, 7000);
            await page.keyboard.press('Escape');
            await human_delay(800, 1500);
            watched++;
            console.log(`   👀 Story ${watched}/${to_watch} watched`);
        } catch (e) {
            console.log(`   ⚠️ Error while watching story: ${e.message}`);
            await page.keyboard.press('Escape').catch(() => { });
        }
    }
    return watched;
}

async function browse_feed_and_like(page, target_likes) {
    console.log(`❤️ Feed warm-up: ${target_likes} likes`);
    await dismiss_overlays(page);

    if (!page.url().match(/instagram\.com\/?(\?|$)/)) {
        await page.goto('https://www.instagram.com/', {
            waitUntil: 'domcontentloaded',
            timeout: 30000
        }).catch(() => { });
        await human_delay(2000, 3500);
    }

    const articles_appeared = await page.waitForFunction(
        () => document.querySelectorAll('article').length > 0,
        { timeout: 12000 }
    ).then(() => true).catch(() => false);

    if (!articles_appeared) {

        for (let i = 0; i < 3; i++) {
            await human_scroll_window(page, { scrolls: 2 });
            const ok = await page.evaluate(() => document.querySelectorAll('article').length > 0);
            if (ok) break;
        }
    }

    let liked = 0;
    let scrolls_without_progress = 0;

    while (liked < target_likes && scrolls_without_progress < 5) {

        const articles = await page.$$('article');
        let new_like_done = false;

        for (const article of articles) {
            if (liked >= target_likes) break;
            try {

                const like_btn = await find_unliked_like(article);
                if (!like_btn) continue;

                const box = await like_btn.boundingBox();
                if (!box) continue;

                const in_view = await like_btn.evaluate(el => {
                    const r = el.getBoundingClientRect();
                    return r.top > 50 && r.bottom < window.innerHeight - 50;
                });
                if (!in_view) continue;

                if (Math.random() < 0.3) {
                    await human_delay(1500, 4000);
                }

                await human_click(page, like_btn);
                liked++;
                new_like_done = true;
                console.log(`   ❤️ Like ${liked}/${target_likes}`);

                const pause = random_range(
                    config.delay_between_likes.min,
                    config.delay_between_likes.max
                );
                console.log(`   ⏰ Pause ${pause.toFixed(1)} sec`);
                await sleep(pause);

                const block = await check_for_block(page);
                if (block.blocked) {
                    console.log(`   🚫 Blocked after like: ${block.reason}`);
                    return { liked, blocked: true };
                }

                break;
            } catch (e) {
                continue;
            }
        }

        if (!new_like_done) {
            scrolls_without_progress++;
            await human_scroll_window(page, { scrolls: random_int(2, 4), back_chance: 0.05 });
            await human_delay(1500, 3000);
        } else {
            scrolls_without_progress = 0;

            await human_scroll_window(page, { scrolls: random_int(1, 2), back_chance: 0.1 });
            await human_delay(800, 1800);
        }
    }

    console.log(`✅ Warm-up complete, likes: ${liked}`);
    if (liked === 0) await dump_state(page, 'feed_zero_likes');
    return { liked, blocked: false };
}

async function visit_target_profile(page, username) {
    console.log(`🎯 Opening profile @${username}`);
    await page.goto(`https://www.instagram.com/${username}/`, {
        waitUntil: 'domcontentloaded',
        timeout: 30000
    });
    await human_delay(2500, 4500);
    await dismiss_overlays(page);

    const not_found = await page.evaluate(() => {
        const t = document.body.innerText;
        return t.includes("Sorry, this page isn't available")
            || t.includes('Страница недоступна');
    });
    if (not_found) {
        console.log(`   ⚠️ Profile @${username} not found`);
        return false;
    }

    const block = await check_for_block(page);
    if (block.blocked) {
        console.log(`   🚫 Blocked while opening profile: ${block.reason}`);
        return false;
    }

    await human_scroll_window(page, { scrolls: random_int(2, 4) });
    return true;
}

async function like_target_posts(page, count) {
    console.log(`💜 Liking ${count} post(s) on the target account`);

    const post_links = await page.$$('a[href*="/p/"]');
    if (post_links.length === 0) {
        console.log('   ⏭ No posts visible (private account or no publications)');
        return 0;
    }

    let liked = 0;
    const indices = [];
    for (let i = 0; i < count && i < post_links.length; i++) {

        const idx = random_int(0, Math.min(11, post_links.length - 1));
        if (!indices.includes(idx)) indices.push(idx);
    }

    for (const idx of indices) {
        try {
            const post_links_fresh = await page.$$('a[href*="/p/"]');
            if (!post_links_fresh[idx]) continue;

            await human_click(page, post_links_fresh[idx]);
            await human_delay(2500, 4500);

            const post_root = await page.evaluateHandle(() => {
                const dialog = document.querySelector('div[role="dialog"]');
                if (dialog) {
                    const inner_article = dialog.querySelector('article');
                    return inner_article || dialog;
                }
                return document.querySelector('article');
            });
            const post_root_el = post_root.asElement();
            const like_btn = post_root_el ? await find_unliked_like(post_root_el) : null;
            if (like_btn) {
                await human_click(page, like_btn);
                liked++;
                console.log(`   ❤️ Like on post ${liked}/${count}`);
            } else {
                console.log(`   ⏭ Post already liked or like button not found, skipping`);
            }

            if (Math.random() < 0.4) {
                await page.keyboard.press('ArrowRight').catch(() => { });
                await human_delay(800, 1800);
            }

            await human_delay(1500, 3500);
            await page.keyboard.press('Escape');
            await human_delay(1200, 2200);

            const block = await check_for_block(page);
            if (block.blocked) {
                console.log(`   🚫 Blocked after liking a post: ${block.reason}`);
                return liked;
            }

            const pause = random_range(
                config.delay_between_likes.min,
                config.delay_between_likes.max
            );
            await sleep(pause);
        } catch (e) {
            console.log(`   ⚠️ Post-like error: ${e.message}`);
            await page.keyboard.press('Escape').catch(() => { });
        }
    }

    return liked;
}

function setup_friendship_interceptor(page) {
    const state = {
        last_profile_info: null,
        last_friendship_action: null,
        action_blocked: false
    };

    const handler = async (response) => {
        const url = response.url();

        if (/\/api\/v1\/users\/web_profile_info\//.test(url)) {
            try {
                const json = await response.json();
                const u = json?.data?.user;
                if (u) {
                    state.last_profile_info = {
                        username: u.username,
                        id: u.id,
                        followed_by_viewer: !!u.followed_by_viewer,
                        requested_by_viewer: !!u.requested_by_viewer,
                        is_private: !!u.is_private
                    };
                }
            } catch (e) { }
            return;
        }

        if (/\/api\/v1\/friendships\/(create|destroy)\//.test(url)) {
            try {
                const json = await response.json();
                state.last_friendship_action = { status: response.status(), body: json };
                if (json?.spam || json?.feedback_required || json?.error_type === 'feedback_required') {
                    state.action_blocked = true;
                }
            } catch (e) {
                state.last_friendship_action = { status: response.status(), body: null };
            }
        }
    };

    page.on('response', handler);
    return { state, cleanup: () => page.off('response', handler) };
}

async function fetch_profile_info(page, username) {
    return await page.evaluate(async (u) => {
        try {
            const res = await fetch(`/api/v1/users/web_profile_info/?username=${encodeURIComponent(u)}`, {
                headers: { 'X-IG-App-ID': '936619743392459' },
                credentials: 'include'
            });
            if (!res.ok) return { error: `HTTP ${res.status}` };
            const json = await res.json();
            const user = json?.data?.user;
            if (!user) return { error: 'no user in response body' };
            return {
                username: user.username,
                id: user.id,
                followed_by_viewer: !!user.followed_by_viewer,
                requested_by_viewer: !!user.requested_by_viewer,
                is_private: !!user.is_private
            };
        } catch (e) {
            return { error: e.message };
        }
    }, username);
}

async function ensure_following(page, target_username, friendship_iface) {
    const wait_for = async (predicate, max_ms, step = 200) => {
        const start = Date.now();
        while (Date.now() - start < max_ms) {
            const v = predicate();
            if (v) return v;
            await new Promise(r => setTimeout(r, step));
        }
        return predicate() || null;
    };

    const info = await fetch_profile_info(page, target_username);

    if (!info || info.error || !info.username) {
        console.log(`   ⚠️ Could not fetch profile info via API for @${target_username}: ${info?.error || 'no data'}`);
        return false;
    }

    console.log(`   🔎 API friendship state for @${target_username}: following=${info.followed_by_viewer}, requested=${info.requested_by_viewer}, private=${info.is_private}`);

    if (info.followed_by_viewer) {
        console.log(`   ✓ Already following @${target_username}`);
        return true;
    }
    if (info.requested_by_viewer) {
        console.log(`   ✓ Follow request already pending for @${target_username}`);
        return true;
    }

    await page.evaluate(() => window.scrollTo({ top: 0, behavior: 'instant' }));
    await human_delay(800, 1500);

    const handle = await page.evaluateHandle((username) => {
        const heading_candidates = Array.from(
            document.querySelectorAll('h1, h2, span')
        ).filter(el => {
            const text = (el.textContent || '').trim();
            if (text !== username) return false;
            const r = el.getBoundingClientRect();
            return r.top >= 0 && r.top < 500 && r.width > 30;
        });

        if (heading_candidates.length === 0) return null;
        const anchor = heading_candidates[0];

        let container = anchor;
        for (let depth = 0; depth < 10 && container.parentElement; depth++) {
            container = container.parentElement;
            const buttons = Array.from(container.querySelectorAll('button, div[role="button"]'));
            const candidates = buttons.filter(btn => {
                const r = btn.getBoundingClientRect();
                if (r.width < 70 || r.height < 28 || r.width > 400 || r.height > 80) return false;
                const text = (btn.textContent || '').trim();
                if (text.length === 0 || text.length > 60) return false;
                return btn.querySelectorAll('svg').length <= 1;
            });
            if (candidates.length === 0) continue;
            candidates.sort((a, b) => {
                const ra = a.getBoundingClientRect(), rb = b.getBoundingClientRect();
                if (Math.abs(ra.top - rb.top) > 5) return ra.top - rb.top;
                return ra.left - rb.left;
            });
            return candidates[0];
        }
        return null;
    }, target_username);

    const el = handle.asElement();
    if (!el) {
        console.log(`   ⚠️ Primary action button not found near @${target_username} username anchor`);
        await dump_state(page, `no_follow_btn_${target_username}`);
        return false;
    }

    friendship_iface.state.last_friendship_action = null;
    friendship_iface.state.action_blocked = false;

    console.log(`   ➕ Subscribing to @${target_username}...`);
    await human_click(page, el);
    await human_delay(2500, 4000);

    const after = await fetch_profile_info(page, target_username);
    if (after && !after.error) {
        if (after.followed_by_viewer) {
            console.log(`   ✓ Subscribed (API confirmed)`);
            return true;
        }
        if (after.requested_by_viewer) {
            console.log(`   ✓ Follow request sent (private account)`);
            return true;
        }
    }

    if (friendship_iface.state.action_blocked) {
        const body = friendship_iface.state.last_friendship_action?.body;
        const fb = body?.feedback_message || body?.feedback_title || 'feedback_required';
        console.log(`   🚫 Instagram action-blocked the follow: ${fb}`);
        return false;
    }

    console.log(`   ⚠️ Click sent but API still reports not following — silent block or click missed the toggle`);
    return false;
}

function setup_followers_interceptor(page) {
    const state = {
        has_next_page: true,
        api_seen: false,
        responses: 0,
        users_returned: 0,
        empty_with_next: 0,
        cap_detected: false
    };

    const handler = async (response) => {
        const url = response.url();
        const is_followers_api = /\/friendships\/\d+\/followers\//.test(url);
        const is_graphql = /edge_followed_by/.test(url);
        if (!is_followers_api && !is_graphql) return;

        try {
            const json = await response.json();
            state.api_seen = true;
            state.responses++;

            let batch_size = 0;
            let has_next = state.has_next_page;

            if ('next_max_id' in json) {
                has_next = !!json.next_max_id;
                if (Array.isArray(json.users)) {
                    batch_size = json.users.length;
                    state.users_returned += batch_size;
                }
            } else if (json?.data?.user?.edge_followed_by?.page_info) {
                const pi = json.data.user.edge_followed_by.page_info;
                has_next = !!pi.has_next_page;
                const edges = json.data.user.edge_followed_by.edges;
                if (Array.isArray(edges)) {
                    batch_size = edges.length;
                    state.users_returned += batch_size;
                }
            }

            state.has_next_page = has_next;

            if (has_next && batch_size === 0) {
                state.empty_with_next++;
                if (state.empty_with_next >= 1) state.cap_detected = true;
            } else {
                state.empty_with_next = 0;
            }
        } catch (e) {
        }
    };

    page.on('response', handler);
    return { state, cleanup: () => page.off('response', handler) };
}

async function parse_followers(page, target_username, max_count) {
    console.log(`📋 Parsing followers of @${target_username} (target: ${max_count})`);

    await dismiss_overlays(page);

    const { state: api_state, cleanup: detach_interceptor } = setup_followers_interceptor(page);
    try {
        return await _parse_followers_impl(page, target_username, max_count, api_state);
    } finally {
        detach_interceptor();
    }
}

async function _parse_followers_impl(page, target_username, max_count, api_state) {
    const followers_handle = await page.evaluateHandle((target) => {
        const followers_re = new RegExp(`^/${target}/followers/?(\\?|$)`, 'i');

        const anchors = Array.from(document.querySelectorAll('a[href]'));
        for (const a of anchors) {
            if (followers_re.test(a.getAttribute('href') || '')) return a;
        }

        const all_clickable = Array.from(document.querySelectorAll(
            'a, button, div[role="button"], a[role="link"]'
        ));
        const counters = all_clickable.filter(el => {
            const t = (el.innerText || '').trim();
            if (!t || t.length > 60 || !/\d/.test(t)) return false;
            const r = el.getBoundingClientRect();
            return r.width > 0 && r.height > 0 && r.top < window.innerHeight;
        });

        for (const seed of counters) {
            let node = seed;
            for (let depth = 0; depth < 8 && node.parentElement; depth++) {
                const parent = node.parentElement;
                const row = [];
                for (const child of parent.children) {
                    const hit = counters.find(c => child === c || child.contains(c));
                    if (hit) row.push(hit);
                }
                if (row.length >= 2 && row.length <= 4) {
                    return row[row.length - 2];
                }
                node = parent;
            }
        }
        return null;
    }, target_username);

    const followers_link = followers_handle.asElement();
    if (!followers_link) {
        console.log('   ❌ "Followers" button not found');
        await dump_state(page, `target_${target_username}_no_followers_btn`);
        return [];
    }
    await human_click(page, followers_link);
    await human_delay(1800, 3000);

    await page.waitForSelector('div[role="dialog"]', { timeout: 10000 }).catch(() => { });

    const got_users = await page.waitForFunction(() => {
        const dialog = document.querySelector('div[role="dialog"]');
        if (!dialog) return false;
        return dialog.querySelectorAll('a[role="link"][href^="/"]').length >= 3;
    }, { timeout: 15000 }).then(() => true).catch(() => false);

    if (!got_users) {
        console.log('   ❌ Followers modal did not load the list within 15 sec');
        return [];
    }
    await human_delay(800, 1500);

    const find_scrollable = async () => {
        const handle = await page.evaluateHandle(() => {
            const dialog = document.querySelector('div[role="dialog"]');
            if (!dialog) return null;
            const all = dialog.querySelectorAll('div');
            let best = null, best_count = 0;
            for (const el of all) {
                const s = window.getComputedStyle(el);
                if (s.overflowY !== 'auto' && s.overflowY !== 'scroll') continue;
                const links = el.querySelectorAll('a[role="link"][href^="/"]').length;
                if (links > best_count) { best = el; best_count = links; }
            }
            return best;
        });
        return handle.asElement();
    };

    let scrollable = await find_scrollable();
    if (!scrollable) {
        console.log('   ❌ Scrollable container of the modal not found');
        return [];
    }
    console.log(`   📜 Container found, initial scrollHeight: ${await scrollable.evaluate(el => el.scrollHeight)}px`);

    const modal_box = await scrollable.boundingBox();
    if (modal_box) {
        await move_mouse_human(page, {
            x: modal_box.x + modal_box.width / 2,
            y: modal_box.y + modal_box.height / 2
        });
        await human_delay(200, 500);
    }

    const collected = new Map();
    let stagnation = 0;
    const max_stagnation = 5;
    let iteration = 0;
    const max_iterations = 200;

    while (collected.size < max_count && stagnation < max_stagnation && iteration < max_iterations) {
        iteration++;

        const batch = await page.evaluate(() => {
            const dialog = document.querySelector('div[role="dialog"]');
            if (!dialog) return [];
            const links = dialog.querySelectorAll('a[role="link"][href^="/"]');
            const seen = new Map();

            const skip = new Set(['explore', 'reels', 'direct', 'accounts', 'p', 'tv', 'stories', 'about']);

            const button_words = new Set([
                'Follow', 'Following', 'Message', 'Requested', 'Subscribed', 'Subscribe',
                'Подписаться', 'Подписки', 'Подписан', 'Подписана', 'Запрос', 'Сообщение',
                'Отписаться', 'Subscribirse', 'Seguir', 'Suivre'
            ]);

            for (const a of links) {
                const href = a.getAttribute('href');
                const m = href && href.match(/^\/([A-Za-z0-9._]+)\/?$/);
                if (!m) continue;
                const username = m[1];
                if (skip.has(username)) continue;
                if (seen.has(username)) continue;

                let row = a;
                for (let depth = 0; depth < 10 && row.parentElement; depth++) {
                    const parent = row.parentElement;
                    const sibling_links = parent.querySelectorAll('a[role="link"][href^="/"]');
                    let other = 0;
                    for (const l of sibling_links) {
                        const lm = l.getAttribute('href').match(/^\/([A-Za-z0-9._]+)\/?$/);
                        if (lm && lm[1] !== username && !skip.has(lm[1])) { other++; break; }
                    }
                    if (other > 0) break;
                    row = parent;
                }

                const lines = (row.innerText || '')
                    .split(/\n+/).map(s => s.trim()).filter(Boolean);
                let full_name = '';
                for (const line of lines) {
                    if (line === username) continue;
                    if (button_words.has(line)) continue;
                    if (line.length > 150) continue;
                    full_name = line;
                    break;
                }

                const verified = !!row.querySelector('svg[aria-label="Verified"], svg[aria-label*="Verif"], svg[aria-label="Подтвержденный"]');
                const is_private = !!row.querySelector('svg[aria-label="Private"]');

                seen.set(username, { username, full_name, verified, is_private });
            }
            return Array.from(seen.values());
        });

        const before = collected.size;
        for (const u of batch) {
            if (collected.size >= max_count) break;
            if (!collected.has(u.username)) collected.set(u.username, u);
        }
        const added = collected.size - before;

        if (added === 0) stagnation++;
        else stagnation = 0;

        const api_tail = api_state.api_seen
            ? ` [api: ${api_state.responses} resp, ${api_state.users_returned} users, next=${api_state.has_next_page}${api_state.cap_detected ? ', CAP' : ''}]`
            : '';
        process.stdout.write(`   📊 Collected: ${collected.size}/${max_count} (+${added})${api_tail}\r`);

        if (collected.size >= max_count) break;

        if (api_state.api_seen && (!api_state.has_next_page || api_state.cap_detected)) {
            const reason = api_state.cap_detected
                ? `server cap (empty batch with next_max_id present, returned ${api_state.users_returned} users total)`
                : 'has_next_page=false';
            console.log(`\n   🛑 Pagination closed by Instagram: ${reason}`);
            break;
        }

        const wheel_delta = random_range(400, 700);
        await page.mouse.wheel({ deltaY: wheel_delta });
        await human_delay(900, 1700);

        if (stagnation >= 1) {
            await scrollable.evaluate(el => {
                const links = el.querySelectorAll('a[role="link"][href^="/"]');
                if (links.length > 0) {
                    links[links.length - 1].scrollIntoView({ block: 'end' });
                }
            });
            await human_delay(700, 1300);

            const fresh = await find_scrollable();
            if (fresh) scrollable = fresh;
        }

        if (Math.random() < 0.2) await human_delay(1500, 3500);
    }

    console.log(`\n   ✅ Followers collected: ${collected.size}`);

    await page.keyboard.press('Escape').catch(() => { });
    await human_delay(800, 1500);

    return Array.from(collected.values()).slice(0, max_count);
}

async function save_followers(profile_uuid, target, users) {
    const dir = path.join(__dirname, config.results_dir);
    await ensure_dir(dir);

    const stamp = new Date().toISOString().replace(/[:.]/g, '-');
    const base = `${target}_${profile_uuid.slice(0, 8)}_${stamp}`;

    const json_path = path.join(dir, `${base}.json`);
    const csv_path = path.join(dir, `${base}.csv`);

    const json_payload = {
        target_account: target,
        parsed_by_profile: profile_uuid,
        timestamp: new Date().toISOString(),
        total: users.length,
        followers: users
    };
    await fs.writeFile(json_path, JSON.stringify(json_payload, null, 2), 'utf8');

    const csv_lines = ['username,full_name,verified,is_private'];
    for (const u of users) {
        csv_lines.push([
            csv_escape(u.username),
            csv_escape(u.full_name),
            csv_escape(u.verified),
            csv_escape(u.is_private)
        ].join(','));
    }
    await fs.writeFile(csv_path, csv_lines.join('\n'), 'utf8');

    console.log(`💾 JSON: ${json_path}`);
    console.log(`💾 CSV : ${csv_path}`);
}

async function process_target(page, target, profile_uuid) {
    const friendship_iface = setup_friendship_interceptor(page);
    try {
        const ok = await visit_target_profile(page, target);
        if (!ok) return { target, success: false };

        let followed = false;
        if (config.follow_targets_before_parsing) {
            followed = await ensure_following(page, target, friendship_iface);
            if (followed) await human_delay(3000, 6000);
        }

        const target_likes = random_int(config.likes_on_target.min, config.likes_on_target.max);
        if (target_likes > 0) {
            await like_target_posts(page, target_likes);
        }

        await page.goto(`https://www.instagram.com/${target}/`, {
            waitUntil: 'domcontentloaded',
            timeout: 30000
        }).catch(() => { });
        await human_delay(1500, 3000);

        const users = await parse_followers(page, target, config.followers_per_target);
        if (users.length > 0) {
            await save_followers(profile_uuid, target, users);
        }

        return { target, success: true, count: users.length, followed };
    } finally {
        friendship_iface.cleanup();
    }
}

async function process_profile(profile_cfg, idx, total) {
    console.log(`\n${'='.repeat(80)}`);
    console.log(`📋 Profile ${idx + 1}/${total} — UUID ${profile_cfg.uuid}`);
    console.log(`   Targets: ${profile_cfg.target_accounts.join(', ')}`);
    console.log(`${'='.repeat(80)}`);

    await octo_stop_profile(profile_cfg.uuid).catch(() => { });
    await sleep(3);

    let ws_data;
    try {
        ws_data = await octo_start_profile(profile_cfg.uuid);
    } catch (e) {
        const body = e.response?.data;
        const body_str = body ? (typeof body === 'string' ? body : JSON.stringify(body)) : '';
        console.error(`❌ Failed to start profile: ${e.message}${body_str ? ' | Octo: ' + body_str : ''}`);
        return { uuid: profile_cfg.uuid, status: 'start_failed', error: body_str || e.message };
    }

    if (!ws_data?.ws_endpoint) {
        console.error('❌ Octo did not return a ws_endpoint');
        await octo_stop_profile(profile_cfg.uuid).catch(() => { });
        return { uuid: profile_cfg.uuid, status: 'no_ws' };
    }

    let browser;
    try {
        browser = await puppeteer.connect({
            browserWSEndpoint: ws_data.ws_endpoint,
            defaultViewport: null,
            protocolTimeout: 600000
        });
    } catch (e) {
        console.error(`❌ Puppeteer connect: ${e.message}`);
        await octo_stop_profile(profile_cfg.uuid).catch(() => { });
        return { uuid: profile_cfg.uuid, status: 'connect_failed' };
    }

    try {
        const ctx = browser.defaultBrowserContext();
        await ctx.overridePermissions('https://www.instagram.com', []);
        await ctx.overridePermissions('https://instagram.com', []);
    } catch (e) {
        console.warn(`⚠️ overridePermissions: ${e.message}`);
    }

    let stats = { uuid: profile_cfg.uuid, status: 'ok', targets: [], likes: 0, stories: 0 };

    try {
        const page = await browser.newPage();
        await page.setViewport({ width: 1280, height: 900 });

        await page.goto('https://www.instagram.com/', {
            waitUntil: 'domcontentloaded',
            timeout: 45000
        });
        await human_delay(2500, 4500);
        await dismiss_overlays(page);

        await human_delay(800, 1500);
        await dismiss_overlays(page);

        if (!await check_logged_in(page)) {
            console.error('❌ Profile is not logged into Instagram');
            stats.status = 'not_logged_in';
            return stats;
        }

        const block = await check_for_block(page);
        if (block.blocked) {
            console.error(`❌ Account is restricted: ${block.reason}`);
            stats.status = 'blocked';
            return stats;
        }

        if (Math.random() < config.stories_probability) {
            const cnt = random_int(config.stories_per_session.min, config.stories_per_session.max);
            stats.stories = await watch_random_stories(page, cnt);
            await human_delay(1500, 3000);
        }

        const feed_likes = random_int(config.likes_per_session.min, config.likes_per_session.max);
        const feed_result = await browse_feed_and_like(page, feed_likes);
        stats.likes += feed_result.liked;
        if (feed_result.blocked) {
            stats.status = 'blocked_during_feed';
            return stats;
        }

        for (let i = 0; i < profile_cfg.target_accounts.length; i++) {
            const target = profile_cfg.target_accounts[i];
            try {
                const r = await process_target(page, target, profile_cfg.uuid);
                stats.targets.push(r);
            } catch (e) {
                console.error(`❌ Target @${target} error: ${e.message}`);
                stats.targets.push({ target, success: false, error: e.message });
            }

            if (i < profile_cfg.target_accounts.length - 1) {
                const pause = random_range(
                    config.delay_between_targets.min,
                    config.delay_between_targets.max
                );
                console.log(`⏰ Pause between targets: ${pause.toFixed(1)} sec`);
                await sleep(pause);
            }
        }

    } catch (e) {
        console.error(`❌ Profile processing error: ${e.message}`);
        stats.status = 'error';
        stats.error = e.message;
    } finally {
        await octo_stop_profile(profile_cfg.uuid).catch(() => { });
        await sleep(2);
    }

    return stats;
}

async function check_limits(response) {
    const header = response.headers.ratelimit;
    if (!header) return;
    const entries = header.split(',').map(s => s.trim());
    for (const e of entries) {
        const r_match = e.match(/;r=(\d+)/);
        const t_match = e.match(/;t=(\d+)/);
        if (!r_match || !t_match) continue;
        const remaining = parseInt(r_match[1], 10);
        const window_s = parseInt(t_match[1], 10);
        if (remaining < 5) {
            console.log(`⏳ Octo rate-limit, waiting ${window_s + 1} sec`);
            await sleep(window_s + 1);
        }
    }
}

async function octo_start_profile(uuid) {
    const res = await axios({
        method: 'post',
        url: `${config.octo_local_api_base_url}/start`,
        headers: { 'Content-Type': 'application/json' },
        data: {
            uuid,
            headless: config.headless_mode,
            debug_port: true,
            timeout: 60
        }
    });
    await check_limits(res);
    return res.data;
}

async function octo_stop_profile(uuid) {
    const res = await axios({
        method: 'post',
        url: `${config.octo_local_api_base_url}/stop`,
        headers: { 'Content-Type': 'application/json' },
        data: { uuid }
    });
    await check_limits(res);
    return res.data;
}

(async () => {
    console.log('🚀 Octo Instagram Parser & Liker');
    console.log(`   Profiles: ${config.profiles.length}`);
    console.log(`   Followers per target: ${config.followers_per_target}`);
    console.log(`   Feed likes: ${config.likes_per_session.min}${config.likes_per_session.max}`);
    console.log('');

    await ensure_dir(path.join(__dirname, config.results_dir));

    const all_stats = [];
    for (let i = 0; i < config.profiles.length; i++) {
        const stats = await process_profile(config.profiles[i], i, config.profiles.length);
        all_stats.push(stats);

        if (i < config.profiles.length - 1) {
            const pause = random_range(
                config.delay_between_profiles.min,
                config.delay_between_profiles.max
            );
            console.log(`\n⏰ Pause before the next profile: ${pause.toFixed(1)} sec`);
            await sleep(pause);
        }
    }

    console.log(`\n${'='.repeat(80)}`);
    console.log('📊 SUMMARY');
    console.log('='.repeat(80));
    for (const s of all_stats) {
        console.log(`\n${s.uuid}${s.status}`);
        console.log(`   Likes: ${s.likes ?? 0}, stories: ${s.stories ?? 0}`);
        if (s.targets) {
            for (const t of s.targets) {
                if (t.success) console.log(`   ✅ @${t.target}: ${t.count} followers`);
                else console.log(`   ❌ @${t.target}: ${t.error || 'fail'}`);
            }
        }
    }

    const summary_path = path.join(__dirname, config.results_dir, `_summary_${Date.now()}.json`);
    await fs.writeFile(summary_path, JSON.stringify(all_stats, null, 2));
    console.log(`\n📄 Summary report: ${summary_path}`);
    console.log('🎉 Done.');
})();

Масштабирование и безопасность аккаунтов

Чек-лист перед запуском:

1. Профили прогреты.  Аккаунту минимум 2–3 недели, есть аватарка, посты, какая-то лента подписок.
2. Прокси не серверные. Используйте резидентные или мобильные.
3. Селекторы актуальны. Прежде чем запускать на большом количестве аккаунтов, протестируйте скрипт на нескольких и проверьте, что селекторы корректные.
4. Паузы между запусками. Наш скрипт обрабатывает профили в config.profiles последовательно, с паузой 60–120 секунд. При большем количестве аккаунтов увеличьте этот интервал.

Заключение

В одном скрипте мы покрыли два самых востребованных кейса: парсинг подписчиков (для аудита конкурентов и сбора lookalike-аудиторий) и масслайкинг (для прогрева). Их легко расширить:

  • Парсинг лайкнувших/комментаторов под конкретным постом — для самой «горячей» выборки.

  • Сбор bio + email каждого комментатора. Это можно выделить в отдельный пайплайн, чтобы не нагружать парсер-аккаунт лишними действиями.

  • Сравнение списков подписчиков нескольких конкурентов и поиск пересечений — это уже простая логика на чистом Node.js поверх собранных JSON.

  • Слой кэша: чтобы не парсить одних и тех же подписчиков повторно при следующих прогонах.

Также если вы работаете с большими объемами, то легко можете переделать скрипт для параллельного запуска.

Залог успешного парсинга — многослойная система. Octo Browser дает корректный отпечаток и изоляцию профилей, rebrowser-puppeteer — автоматизацию с фиксами от утечек признаков автоматизации, алгоритм WindMouse — реалистичную моторику, поведенческий слой — отвлечения и паузы. В итоге устойчивость системы определяется не отдельным компонентом, а тем, насколько хорошо они работают вместе.

Следите за последними новостями Octo Browser

Нажимая кнопку, вы соглашаетесь с нашей политикой конфиденциальности.

Следите за последними новостями Octo Browser

Нажимая кнопку, вы соглашаетесь с нашей политикой конфиденциальности.

Следите за последними новостями Octo Browser

Нажимая кнопку, вы соглашаетесь с нашей политикой конфиденциальности.

Присоединяйтесь к Octo Browser сейчас

Вы можете обращаться за помощью к нашим специалистам службы поддержки в чате в любое время.

Присоединяйтесь к Octo Browser сейчас

Вы можете обращаться за помощью к нашим специалистам службы поддержки в чате в любое время.

Присоединяйтесь к Octo Browser сейчас

Вы можете обращаться за помощью к нашим специалистам службы поддержки в чате в любое время.

©

2026

Octo Browser

©

2026

Octo Browser

©

2026

Octo Browser