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


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. Они потребуются для заполнения конфигурации в скрипте.
Запуск
Скачайте и установите VS Code.
Скачайте и установите node.js.
Создайте папку в удобном для вас месте и назовите ее, например
octo_instagram_scraper.Откройте эту папку в VS Code.
Создайте файл с расширением .js. Лучше называть его по имени действия, которое будет выполнять код, чтобы не запутаться. Например,
octo_instagram_scraper.js.Вставьте в файл код скрипта.
Заполните конфигурацию в переменной config.
UUID— это ID профилей Octo;target_accounts— аккаунты, которые будете парсить;followers_per_target— число подписчиков целевого аккаунта, данные которых вы хотите собирать за проход.
Остальные параметры в config отвечают за правдоподобное поведение парсера. Можете поэкспериментировать с ними либо не менять.

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

Если VS Code выдает ошибку — откройте от имени администратора Window PowerShell, введите там команду
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSignedи подтвердите. Затем повторите предыдущий пункт.
Запустите клиент Octo Browser.
. Запустите скрипт в Visual Studio (Ctrl/Cmd + F5) и дождитесь окончания работы скрипта.
Парсер последовательно будет запускать профили, которые вы указали в конфигурации. А затем, совсем как обычный пользователь, смотреть сторис, листать ленту, ставить лайки, подписываться и парсить подписчиков целевых аккаунтов. Следить за процессом можно в дебаг-консоли.

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

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

Результаты работы парсера в 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. Они потребуются для заполнения конфигурации в скрипте.
Запуск
Скачайте и установите VS Code.
Скачайте и установите node.js.
Создайте папку в удобном для вас месте и назовите ее, например
octo_instagram_scraper.Откройте эту папку в VS Code.
Создайте файл с расширением .js. Лучше называть его по имени действия, которое будет выполнять код, чтобы не запутаться. Например,
octo_instagram_scraper.js.Вставьте в файл код скрипта.
Заполните конфигурацию в переменной config.
UUID— это ID профилей Octo;target_accounts— аккаунты, которые будете парсить;followers_per_target— число подписчиков целевого аккаунта, данные которых вы хотите собирать за проход.
Остальные параметры в config отвечают за правдоподобное поведение парсера. Можете поэкспериментировать с ними либо не менять.

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

Если VS Code выдает ошибку — откройте от имени администратора Window PowerShell, введите там команду
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSignedи подтвердите. Затем повторите предыдущий пункт.
Запустите клиент Octo Browser.
. Запустите скрипт в Visual Studio (Ctrl/Cmd + F5) и дождитесь окончания работы скрипта.
Парсер последовательно будет запускать профили, которые вы указали в конфигурации. А затем, совсем как обычный пользователь, смотреть сторис, листать ленту, ставить лайки, подписываться и парсить подписчиков целевых аккаунтов. Следить за процессом можно в дебаг-консоли.

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

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

Результаты работы парсера в 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 сейчас
Вы можете обращаться за помощью к нашим специалистам службы поддержки в чате в любое время.
