Como raspar dados do Instagram em 2026
03/06/2026


Artur Hvalei
Technical Support Specialist, Octo Browser
Em nosso artigo anterior, cobrimos a extração de resultados de pesquisa do Google usando o Octo Browser e o Puppeteer. Agora, vamos elevar o nível e enfrentar o Instagram, uma plataforma que possui um dos sistemas anti-bot mais rigorosos em 2026. Analisaremos os tipos de dados que as pessoas costumam extrair e por que fazem isso. Explicaremos por que os bots padrão rapidamente se deparam com restrições e banimentos. Também examinaremos a arquitetura de uma solução funcional, os problemas que encontramos durante os testes e maneiras de dimensionar a coleta de dados sem lidar constantemente com problemas de conta.
Índice
Mantenha o anonimato, aproveite o recurso multiconta e alcance seus objetivos com o melhor navegador antidetecção do mercado.
O que as pessoas extraem do Instagram e por quê
Após analisar o mercado de ferramentas e serviços do Instagram em 2025–2026, podemos identificar seis tipos principais de dados que são comumente extraídos:
Dados | Campos | Objetivo |
Seguidores da conta | username, full_name, verified, is_private, bio | Pesquisa de concorrentes, busca de leads de alta qualidade, análise de público-alvo para Meta Ads |
Seguindo da conta | username, full_name, verified, is_private, bio | Análise de público-alvo de concorrentes e influenciadores |
Usuários que curtiram um post | username, full_name | Encontrar usuários com maior engajamento do que a média de seguidores |
Comentaristas | username, comment text | Abordagem por DM e análise de sentimentos |
Posts por hashtag/localização | post_url, likes, caption | Análise de conteúdo e descoberta de UGC |
Perfil completo | bio, posts, ER | Avaliação de influenciadores antes de parcerias |
Principais casos de uso:
Pesquisa de concorrentes. Extraia os seguidores de 3 a 5 concorrentes, remova duplicatas e identifique sobreposições. Os leads de maior qualidade geralmente são usuários que seguem vários concorrentes ao mesmo tempo.
Avaliação de influenciadores/blogueiros. Antes de pagar por uma parceria, colete dados de seguidores e avalie a autenticidade do público.
Abordagem por DM e e-mail frio. Nos nichos de SaaS, marketing e e-commerce, de 15% a 35% dos perfis incluem um endereço de e-mail público em sua bio.
Públicos Semelhantes (Lookalike) no Meta Ads. Os nomes de usuário podem ser convertidos em uma lista semente de Público Customizado para a criação de Públicos Semelhantes.
Preparação de conta por meio de curtidas em massa. Usado para aumentar o alcance e estabelecer um padrão de atividade normal de conta.
Por que bots comuns são detectados em 2026
A maior mudança nos últimos anos é que o Instagram agora oculta conteúdo de usuários não autenticados. Anteriormente, você podia consultar endpoints públicos como /?__a=1 sem uma sessão. Em 2026, mesmo os seguidores de uma conta pública ficam inacessíveis, a menos que você esteja logado.
O que você precisará para o seu script:
1. Uma conta real autenticada. De preferência, uma conta bem preparada, e não uma recém-criada. O Octo Browser ajuda nisso: você pode criar uma conta, fazer o login manualmente, deixar a conta maturar naturalmente e só então automatizá-la.
2. Uma conta significa uma impressão digital. Executar 20 contas do Instagram a partir de uma única instância do Chrome é o caminho garantido para o banimento. Com o Octo, você pode usar perfis isolados com diferentes impressões digitais (parâmetros de Canvas, WebGL, resolução de tela, fontes, user-agent, etc.).
3. Um proxy para cada perfil. Proxies móveis ou residenciais são ideais. Usar endereços IP de datacenter é extremamente arriscado.
No entanto, mesmo com uma impressão digital de navegador adequada e um proxy confiável, um bot de Puppeteer não sobreviverá sem alguma emulação de comportamento humano. Os sistemas antibot analisam:
Padrões de frequência. Quantas curtidas por hora, quantas contas seguidas por dia e os intervalos entre as ações. A análise do lado do servidor sinalizará rapidamente um bot que curte um post a cada 30 segundos.
Impressão digital do navegador. É por isso que usar um navegador antidetecção é crítico.
Impressão digital de rede/TLS. É por isso que proxies de qualidade e uma pilha TLS não personalizada importam.
Vulnerabilidades de frameworks de automação (
navigator.webdriver, presença de CDP, traços distintos emError.stack).
Conclusão prática: em 2026, os principais motivos para banimento de contas são os padrões de frequência de comportamento (muitas ações em um curto período de tempo, intervalos excessivamente consistentes), impressões digitais de navegadores e proxies. Emular movimentos do mouse, rolagem ou digitação ainda não é estritamente necessário. Na prática, contudo, esses sinais ainda podem ser usados para analisar o comportamento e aumentar a pontuação de fraude (fraud score) de uma conta.
É por isso que a arquitetura do nosso script consiste em:
Octo Browser: impressão digital isolada, proxy e um perfil com uma sessão real do Instagram.
Rebrowser-Puppeteer: uma ramificação (fork) do Puppeteer sem os vazamentos comuns que são relativamente fáceis de detectar.
WindMouse: um modelo de movimento do mouse baseado em física de movimento.
Uma camada comportamental: visualizações aleatórias de stories e curtidas em posts antes da extração, distribuições de pausas log-normais, ultrapassagens (errar o alvo e corrigir) e reações a checkpoints.
WindMouse: por que é melhor do que curvas de Bézier cúbicas
Em nosso artigo anterior, usamos curvas de Bézier cúbicas para simular o movimento do mouse. Essa é uma abordagem básica, de modo que tem fraquezas: com apenas dois pontos de controle, a curva é suave e previsível. O trajeto é sempre semelhante; a velocidade é controlada separadamente por um parâmetro de atenuação, o que gera movimentos uniformes demais; e os micro-tremores são adicionados como uma camada separada, tornando óbvio que o movimento se trata de uma curva suave somada a ruídos artificiais.
O WindMouse, em vez disso, simula a física real. A gravidade puxa o cursor em direção ao alvo, enquanto o vento introduz desvios aleatórios que se acumulam com a inércia. Como resultado, a trajetória do movimento torna-se fluida e não abrupta. A velocidade é limitada por um tamanho máximo de passo e diminui naturalmente perto do destino, exatamente como o cursor de uma pessoa real ao mirar em um objeto.
O resultado é uma trajetória com velocidade variável, desaceleração natural e micro-movimentos sutis que são indistinguíveis do comportamento humano real. O mais importante é que, devido ao componente de vento aleatório, o trajeto é diferente a cada execução.
Nós adicionamos mais dois comportamentos semelhantes aos humanos:
Ultrapassagem (Overshoot): em 30% dos casos, o cursor erra intencionalmente o botão de leve e, em seguida, volta para ele.
Tremor de clique: entre o
mouse.down()e omouse.up(), o cursor se desloca de 1 a 2 pixels em uma direção aleatória.
A implementação do WindMouse no script é a função wind_mouse_trajectory. Ela gera uma matriz de pontos de trajetória que são posteriormente utilizados por page.mouse.move().
Script pronto para raspagem de seguidores de contas-alvo e preparação simultânea de contas
Vamos assumir que você já possui uma matriz de UUIDs de perfis do Octo conectados a contas do Instagram. Nosso script iniciará cada perfil, preparará a conta curtindo publicações no feed e visualizando Stories, e então prosseguirá para os perfis do Instagram especificados como alvos de extração. Lá, ele curtirá posts, seguirá contas e, finalmente, extrairá a lista de seguidores nos formatos JSON e CSV. Tudo isso é feito com movimento natural do mouse, atrasos aleatórios e detecção de checkpoints. Com isso, o Instagram não consegue distinguir sua automação de uma atividade normal de usuário.
Preparando perfis
Crie um ou mais perfis no Octo e atribua um proxy a cada um. Recomenda-se o uso de proxies residenciais ou móveis.
Abra cada perfil no Octo manualmente. Faça login ou registre uma conta no Instagram. Durante dois a três dias, utilize a conta como uma pessoa normal: curta publicações, assista a Stories e siga alguns usuários.
Copie os UUIDs dos perfis do Octo. Você precisará deles ao preencher a configuração do script.
Primeiros passos
Baixe e instale o VS Code.
Baixe e instale o Node.js.
Crie uma pasta em qualquer lugar do seu dispositivo e nomeie-a, por exemplo, como
octo_instagram_scraper.Abra a pasta no VS Code.
Crie um arquivo .js. É recomendável nomeá-lo com base na ação que o script executa para evitar confusão. Por exemplo,
octo_instagram_scraper.js.Cole o código do script no arquivo.
Preencha a configuração na variável
config.
UUID — IDs de perfil do Octo.
target_accounts — as contas que você deseja extrair.
followers_per_target — o número de seguidores que você deseja coletar de cada conta-alvo durante uma única execução.
Os parâmetros restantes em config controlam o comportamento realista do extrator. Você pode experimentar com eles ou deixá-los inalterados.

Abra o seu terminal e execute o seguinte comando para instalar as dependências necessárias do Node.js:
npm i rebrowser-puppeteer axios

Se o VS Code exibir um erro, abra o Windows PowerShell como Administrador, execute o seguinte comando e confirme a alteração:
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSignedEm seguida, repita a etapa anterior.
Inicie o Octo Browser.
. Execute o script no Visual Studio Code (
Ctrl/Cmd + F5) e aguarde a conclusão.
O extrator iniciará sequencialmente os perfis especificados em sua configuração. Então, como um usuário comum, ele assistirá a Stories, rolará o feed, curtirá de posts, seguirá contas e extrairá seguidores das contas-alvo. Você pode monitorar o processo no console de depuração.

Resumo dos resultados do analisador. Neste exemplo, duas contas-alvo foram processadas a partir de cada perfil: 3 de 4 execuções foram concluídas com sucesso.
Tenha em mente que o Instagram pode limitar o número de seguidores retornados, dependendo de vários fatores, tais como nível de maturação da conta, o número de conexões mútuas, se a conta segue a conta-alvo e outros. Se algo não funcionar como esperado, experimente realizar atividades de maturação adicionais ou troque de contas e proxies.

Resultados do raspador em formato CSV

Resultados do raspador em formato JSON
O formato Json é mais conveniente para processamento posterior em código, enquanto o CSV é ideal para importação no Excel, Google Sheets, sistemas de CRM ou carregamento direto para o Meta Ads como um Público Customizado.
Código do script
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.'); })();
Escalonamento e segurança de contas
Checklist antes do início:
Os perfis estão preparados corretamente. A conta deve ter pelo menos 2 a 3 semanas, ter uma imagem de perfil, publicações e alguma atividade seguindo outras contas.
Não use proxies de datacenter. Recomendamos usar proxies residenciais ou móveis.
Verifique se os seletores estão atualizados. Antes de executar o script em um grande número de contas, faça testes em algumas delas para confirmar se todos os seletores funcionam corretamente.
Introduza atrasos entre as execuções. Nosso script processa os perfis em
config.profilesde forma sequencial, com pausas entre 60 e 120 segundos. Se usar mais contas, incremente esse intervalo.
Conclusão
Este script único abrange os dois casos de uso mais famosos: extração de seguidores (para pesquisa de concorrência e geração de públicos semelhantes) e curtidas em massa (para maturação de contas). Ele é facilmente extensível para adicionar outras funcionalidades:
Raspagem de usuários que curtiram ou comentaram em um post específico para atingir a fatia de público mais engajada.
Coleta de dados da bio e endereços de e-mail daqueles que comentaram. Esse fluxo pode ser direcionado para uma fila de processamento à parte para não sobrecarregar a conta de raspagem com ações desnecessárias.
Comparação de listas de seguidores entre diversos concorrentes, revelando sobreposições. Isso é muito simples de desenvolver nativamente em Node.js usando os dados em formato JSON gerados.
Criação de uma camada de armazenamento temporário (caching) para evitar reprocessar e raspar os mesmos seguidores em múltiplas tentativas.
Caso opere em grande escala, você também poderá adaptar este script para processar dados de forma paralela.
A raspagem de dados eficiente é baseada em uma arquitetura de múltiplos níveis. O Octo Browser garante uma impressão digital consistente e separação total de perfis, o rebrowser-puppeteer lida com a automação incluindo correções contra vazamentos típicos de detecção, o algoritmo WindMouse gera deslocamentos de mouse reais e a camada de comportamento inclui interrupções e simula pausas realistas. Em termos gerais, a estabilidade de todo o sistema não repousa em uma peça isolada, mas na sinergia e coordenação harmônica de todos esses componentes.
Mantenha o anonimato, aproveite o recurso multiconta e alcance seus objetivos com o melhor navegador antidetecção do mercado.
O que as pessoas extraem do Instagram e por quê
Após analisar o mercado de ferramentas e serviços do Instagram em 2025–2026, podemos identificar seis tipos principais de dados que são comumente extraídos:
Dados | Campos | Objetivo |
Seguidores da conta | username, full_name, verified, is_private, bio | Pesquisa de concorrentes, busca de leads de alta qualidade, análise de público-alvo para Meta Ads |
Seguindo da conta | username, full_name, verified, is_private, bio | Análise de público-alvo de concorrentes e influenciadores |
Usuários que curtiram um post | username, full_name | Encontrar usuários com maior engajamento do que a média de seguidores |
Comentaristas | username, comment text | Abordagem por DM e análise de sentimentos |
Posts por hashtag/localização | post_url, likes, caption | Análise de conteúdo e descoberta de UGC |
Perfil completo | bio, posts, ER | Avaliação de influenciadores antes de parcerias |
Principais casos de uso:
Pesquisa de concorrentes. Extraia os seguidores de 3 a 5 concorrentes, remova duplicatas e identifique sobreposições. Os leads de maior qualidade geralmente são usuários que seguem vários concorrentes ao mesmo tempo.
Avaliação de influenciadores/blogueiros. Antes de pagar por uma parceria, colete dados de seguidores e avalie a autenticidade do público.
Abordagem por DM e e-mail frio. Nos nichos de SaaS, marketing e e-commerce, de 15% a 35% dos perfis incluem um endereço de e-mail público em sua bio.
Públicos Semelhantes (Lookalike) no Meta Ads. Os nomes de usuário podem ser convertidos em uma lista semente de Público Customizado para a criação de Públicos Semelhantes.
Preparação de conta por meio de curtidas em massa. Usado para aumentar o alcance e estabelecer um padrão de atividade normal de conta.
Por que bots comuns são detectados em 2026
A maior mudança nos últimos anos é que o Instagram agora oculta conteúdo de usuários não autenticados. Anteriormente, você podia consultar endpoints públicos como /?__a=1 sem uma sessão. Em 2026, mesmo os seguidores de uma conta pública ficam inacessíveis, a menos que você esteja logado.
O que você precisará para o seu script:
1. Uma conta real autenticada. De preferência, uma conta bem preparada, e não uma recém-criada. O Octo Browser ajuda nisso: você pode criar uma conta, fazer o login manualmente, deixar a conta maturar naturalmente e só então automatizá-la.
2. Uma conta significa uma impressão digital. Executar 20 contas do Instagram a partir de uma única instância do Chrome é o caminho garantido para o banimento. Com o Octo, você pode usar perfis isolados com diferentes impressões digitais (parâmetros de Canvas, WebGL, resolução de tela, fontes, user-agent, etc.).
3. Um proxy para cada perfil. Proxies móveis ou residenciais são ideais. Usar endereços IP de datacenter é extremamente arriscado.
No entanto, mesmo com uma impressão digital de navegador adequada e um proxy confiável, um bot de Puppeteer não sobreviverá sem alguma emulação de comportamento humano. Os sistemas antibot analisam:
Padrões de frequência. Quantas curtidas por hora, quantas contas seguidas por dia e os intervalos entre as ações. A análise do lado do servidor sinalizará rapidamente um bot que curte um post a cada 30 segundos.
Impressão digital do navegador. É por isso que usar um navegador antidetecção é crítico.
Impressão digital de rede/TLS. É por isso que proxies de qualidade e uma pilha TLS não personalizada importam.
Vulnerabilidades de frameworks de automação (
navigator.webdriver, presença de CDP, traços distintos emError.stack).
Conclusão prática: em 2026, os principais motivos para banimento de contas são os padrões de frequência de comportamento (muitas ações em um curto período de tempo, intervalos excessivamente consistentes), impressões digitais de navegadores e proxies. Emular movimentos do mouse, rolagem ou digitação ainda não é estritamente necessário. Na prática, contudo, esses sinais ainda podem ser usados para analisar o comportamento e aumentar a pontuação de fraude (fraud score) de uma conta.
É por isso que a arquitetura do nosso script consiste em:
Octo Browser: impressão digital isolada, proxy e um perfil com uma sessão real do Instagram.
Rebrowser-Puppeteer: uma ramificação (fork) do Puppeteer sem os vazamentos comuns que são relativamente fáceis de detectar.
WindMouse: um modelo de movimento do mouse baseado em física de movimento.
Uma camada comportamental: visualizações aleatórias de stories e curtidas em posts antes da extração, distribuições de pausas log-normais, ultrapassagens (errar o alvo e corrigir) e reações a checkpoints.
WindMouse: por que é melhor do que curvas de Bézier cúbicas
Em nosso artigo anterior, usamos curvas de Bézier cúbicas para simular o movimento do mouse. Essa é uma abordagem básica, de modo que tem fraquezas: com apenas dois pontos de controle, a curva é suave e previsível. O trajeto é sempre semelhante; a velocidade é controlada separadamente por um parâmetro de atenuação, o que gera movimentos uniformes demais; e os micro-tremores são adicionados como uma camada separada, tornando óbvio que o movimento se trata de uma curva suave somada a ruídos artificiais.
O WindMouse, em vez disso, simula a física real. A gravidade puxa o cursor em direção ao alvo, enquanto o vento introduz desvios aleatórios que se acumulam com a inércia. Como resultado, a trajetória do movimento torna-se fluida e não abrupta. A velocidade é limitada por um tamanho máximo de passo e diminui naturalmente perto do destino, exatamente como o cursor de uma pessoa real ao mirar em um objeto.
O resultado é uma trajetória com velocidade variável, desaceleração natural e micro-movimentos sutis que são indistinguíveis do comportamento humano real. O mais importante é que, devido ao componente de vento aleatório, o trajeto é diferente a cada execução.
Nós adicionamos mais dois comportamentos semelhantes aos humanos:
Ultrapassagem (Overshoot): em 30% dos casos, o cursor erra intencionalmente o botão de leve e, em seguida, volta para ele.
Tremor de clique: entre o
mouse.down()e omouse.up(), o cursor se desloca de 1 a 2 pixels em uma direção aleatória.
A implementação do WindMouse no script é a função wind_mouse_trajectory. Ela gera uma matriz de pontos de trajetória que são posteriormente utilizados por page.mouse.move().
Script pronto para raspagem de seguidores de contas-alvo e preparação simultânea de contas
Vamos assumir que você já possui uma matriz de UUIDs de perfis do Octo conectados a contas do Instagram. Nosso script iniciará cada perfil, preparará a conta curtindo publicações no feed e visualizando Stories, e então prosseguirá para os perfis do Instagram especificados como alvos de extração. Lá, ele curtirá posts, seguirá contas e, finalmente, extrairá a lista de seguidores nos formatos JSON e CSV. Tudo isso é feito com movimento natural do mouse, atrasos aleatórios e detecção de checkpoints. Com isso, o Instagram não consegue distinguir sua automação de uma atividade normal de usuário.
Preparando perfis
Crie um ou mais perfis no Octo e atribua um proxy a cada um. Recomenda-se o uso de proxies residenciais ou móveis.
Abra cada perfil no Octo manualmente. Faça login ou registre uma conta no Instagram. Durante dois a três dias, utilize a conta como uma pessoa normal: curta publicações, assista a Stories e siga alguns usuários.
Copie os UUIDs dos perfis do Octo. Você precisará deles ao preencher a configuração do script.
Primeiros passos
Baixe e instale o VS Code.
Baixe e instale o Node.js.
Crie uma pasta em qualquer lugar do seu dispositivo e nomeie-a, por exemplo, como
octo_instagram_scraper.Abra a pasta no VS Code.
Crie um arquivo .js. É recomendável nomeá-lo com base na ação que o script executa para evitar confusão. Por exemplo,
octo_instagram_scraper.js.Cole o código do script no arquivo.
Preencha a configuração na variável
config.
UUID — IDs de perfil do Octo.
target_accounts — as contas que você deseja extrair.
followers_per_target — o número de seguidores que você deseja coletar de cada conta-alvo durante uma única execução.
Os parâmetros restantes em config controlam o comportamento realista do extrator. Você pode experimentar com eles ou deixá-los inalterados.

Abra o seu terminal e execute o seguinte comando para instalar as dependências necessárias do Node.js:
npm i rebrowser-puppeteer axios

Se o VS Code exibir um erro, abra o Windows PowerShell como Administrador, execute o seguinte comando e confirme a alteração:
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSignedEm seguida, repita a etapa anterior.
Inicie o Octo Browser.
. Execute o script no Visual Studio Code (
Ctrl/Cmd + F5) e aguarde a conclusão.
O extrator iniciará sequencialmente os perfis especificados em sua configuração. Então, como um usuário comum, ele assistirá a Stories, rolará o feed, curtirá de posts, seguirá contas e extrairá seguidores das contas-alvo. Você pode monitorar o processo no console de depuração.

Resumo dos resultados do analisador. Neste exemplo, duas contas-alvo foram processadas a partir de cada perfil: 3 de 4 execuções foram concluídas com sucesso.
Tenha em mente que o Instagram pode limitar o número de seguidores retornados, dependendo de vários fatores, tais como nível de maturação da conta, o número de conexões mútuas, se a conta segue a conta-alvo e outros. Se algo não funcionar como esperado, experimente realizar atividades de maturação adicionais ou troque de contas e proxies.

Resultados do raspador em formato CSV

Resultados do raspador em formato JSON
O formato Json é mais conveniente para processamento posterior em código, enquanto o CSV é ideal para importação no Excel, Google Sheets, sistemas de CRM ou carregamento direto para o Meta Ads como um Público Customizado.
Código do script
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.'); })();
Escalonamento e segurança de contas
Checklist antes do início:
Os perfis estão preparados corretamente. A conta deve ter pelo menos 2 a 3 semanas, ter uma imagem de perfil, publicações e alguma atividade seguindo outras contas.
Não use proxies de datacenter. Recomendamos usar proxies residenciais ou móveis.
Verifique se os seletores estão atualizados. Antes de executar o script em um grande número de contas, faça testes em algumas delas para confirmar se todos os seletores funcionam corretamente.
Introduza atrasos entre as execuções. Nosso script processa os perfis em
config.profilesde forma sequencial, com pausas entre 60 e 120 segundos. Se usar mais contas, incremente esse intervalo.
Conclusão
Este script único abrange os dois casos de uso mais famosos: extração de seguidores (para pesquisa de concorrência e geração de públicos semelhantes) e curtidas em massa (para maturação de contas). Ele é facilmente extensível para adicionar outras funcionalidades:
Raspagem de usuários que curtiram ou comentaram em um post específico para atingir a fatia de público mais engajada.
Coleta de dados da bio e endereços de e-mail daqueles que comentaram. Esse fluxo pode ser direcionado para uma fila de processamento à parte para não sobrecarregar a conta de raspagem com ações desnecessárias.
Comparação de listas de seguidores entre diversos concorrentes, revelando sobreposições. Isso é muito simples de desenvolver nativamente em Node.js usando os dados em formato JSON gerados.
Criação de uma camada de armazenamento temporário (caching) para evitar reprocessar e raspar os mesmos seguidores em múltiplas tentativas.
Caso opere em grande escala, você também poderá adaptar este script para processar dados de forma paralela.
A raspagem de dados eficiente é baseada em uma arquitetura de múltiplos níveis. O Octo Browser garante uma impressão digital consistente e separação total de perfis, o rebrowser-puppeteer lida com a automação incluindo correções contra vazamentos típicos de detecção, o algoritmo WindMouse gera deslocamentos de mouse reais e a camada de comportamento inclui interrupções e simula pausas realistas. Em termos gerais, a estabilidade de todo o sistema não repousa em uma peça isolada, mas na sinergia e coordenação harmônica de todos esses componentes.
Mantenha-se atualizado com as últimas notícias do Octo Browser
Ao clicar no botão, você concorda com a nossa Política de Privacidade.
Mantenha-se atualizado com as últimas notícias do Octo Browser
Ao clicar no botão, você concorda com a nossa Política de Privacidade.
Mantenha-se atualizado com as últimas notícias do Octo Browser
Ao clicar no botão, você concorda com a nossa Política de Privacidade.

Junte-se ao Octo Browser agora mesmo
Ou entre em contato com a equipe de suporte no chat para tirar dúvidas a qualquer momento.

Junte-se ao Octo Browser agora mesmo
Ou entre em contato com a equipe de suporte no chat para tirar dúvidas a qualquer momento.
Junte-se ao Octo Browser agora mesmo
Ou entre em contato com a equipe de suporte no chat para tirar dúvidas a qualquer momento.
