Cómo extraer datos de Instagram en 2026


Artur Hvalei
Technical Support Specialist, Octo Browser
En nuestro artículo anterior, cubrimos la extracción de resultados de búsqueda de Google utilizando Octo Browser y Puppeteer. Ahora, elevemos la dificultad y enfrentemos a Instagram, una plataforma con uno de los sistemas anti-bot más estrictos en 2026. Analizaremos los tipos de datos que la gente suele extraer y el porqué. Explicaremos por qué los bots estándar se topan rápidamente con restricciones y bloqueos. También examinaremos la arquitectura de una solución funcional, los problemas que encontramos durante las pruebas y las formas de escalar la recopilación de datos sin lidiar constantemente con problemas de cuentas.
Contenidos
Manténgase anónimo, aproveche el multicuentas y logre sus objetivos con el navegador antidetección de mayor calidad del mercado.
¿Te gustaría probar Octo Browser con descuento?
Usa el código promocional OCTOSCRAPER para obtener un 30% de descuento en cualquier suscripción. Esta oferta solo es válida para nuevos usuarios.
Qué extrae la gente de Instagram y por qué
Tras analizar el mercado de herramientas y servicios de Instagram en 2025–2026, podemos identificar seis tipos principales de datos que se suelen extraer con frecuencia:
Datos | Campos | Propósito |
Seguidores de la cuenta | username, full_name, verified, is_private, bio | Investigación de competidores, búsqueda de leads de alta calidad, análisis de audiencia para Meta Ads |
Seguidos de la cuenta | username, full_name, verified, is_private, bio | Análisis de audiencia objetivo de competidores e influencers |
Usuarios a los que les gustó una publicación | username, full_name | Búsqueda de usuarios con una interacción superior a la media de seguidores |
Comentaristas | username, comment text | Prospección por mensajes directos (DM) y análisis de sentimiento |
Publicaciones por hashtag/ubicación | post_url, likes, caption | Análisis de contenido y descubrimiento de UGC (contenido generado por el usuario) |
Perfil completo | bio, posts, ER | Evaluación de influencers antes de una colaboración |
Casos de uso principales:
Investigación de competidores. Extraiga los seguidores de 3 a 5 competidores, elimine duplicados e identifique coincidencias. Los leads de mayor calidad suelen ser usuarios que siguen a varios competidores al mismo tiempo.
Evaluación de influencers/bloggers. Antes de pagar por una colaboración, recopile los datos de sus seguidores y evalúe qué tan auténtica es su audiencia.
Prospección por mensajes directos (DM) y correos en frío. En los nichos de SaaS, marketing y comercio electrónico, entre el 15 % y el 35 % de los perfiles incluyen una dirección de correo electrónico pública en su biografía.
Audiencias similares en Meta Ads. Los nombres de usuario se pueden convertir en una audiencia semilla personalizada para crear audiencias similares (Lookalike Audiences).
Preparación de cuentas mediante likes masivos. Se utiliza para aumentar el alcance y establecer un patrón de actividad normal en la cuenta.
Por qué se detectan los bots comunes en 2026
El mayor cambio de los últimos años es que Instagram ahora oculta el contenido a los usuarios no autenticados. Anteriormente, se podían realizar consultas a endpoints públicos como /?__a=1 sin necesidad de iniciar sesión. En 2026, incluso los seguidores de una cuenta pública no están disponibles a menos que haya iniciado sesión.
Lo que necesitará para su script:
1. Una cuenta real con sesión iniciada. Preferiblemente una cuenta con bastante antigüedad y bien preparada, en lugar de una recién creada. Octo Browser ayuda en esto: puede crear una cuenta, iniciar sesión manualmente, dejar que gane antigüedad de forma natural y solo entonces automatizarla.
2. Una cuenta significa una huella digital. Ejecutar 20 cuentas de Instagram desde una sola instancia de Chrome es una forma garantizada de ser bloqueado. Con Octo, puede utilizar perfiles aislados con diferentes huellas digitales (parámetros de WebGL, resolución de pantalla, fuentes, user-agent, etc.).
3. Un proxy para cada perfil. Los proxies residenciales o móviles son ideales. Utilizar direcciones IP de centros de datos es sumamente riesgoso.
Sin embargo, incluso con una huella digital de navegador adecuada y un proxy confiable, un bot de Puppeteer no sobrevivirá sin cierto nivel de emulación del comportamiento humano. Los sistemas antibot analizan:
Patrones de frecuencia. Cuántos likes por hora, cuántos seguimientos por día y los intervalos entre acciones. Las analíticas del lado del servidor detectarán rápidamente a un bot que da like a una publicación cada 30 segundos.
Huella digital del navegador (Browser fingerprinting). Por eso es fundamental utilizar un navegador antidetección.
Huella digital de red/TLS. Por eso son importantes los proxies de calidad y una pila TLS no personalizada.
Vulnerabilidades del entorno de automatización (
navigator.webdriver, presencia de CDP, rastros distintivos enError.stack).
Conclusión práctica: en 2026, los motivos principales por los que se suspenden las cuentas son los patrones de frecuencia de comportamiento (demasiadas acciones en un periodo corto, intervalos excesivamente uniformes), las huellas digitales del navegador y los proxies. Emular movimientos del mouse, el desplazamiento por la pantalla o la escritura aún no es estrictamente necesario. En la práctica, sin embargo, estas señales pueden seguir utilizándose para analizar el comportamiento e incrementar el nivel de sospecha (fraud score) de una cuenta.
Por eso, la arquitectura de nuestro script consta de:
Octo Browser: huella digital aislada, proxy y un perfil con una sesión real de Instagram.
Rebrowser-Puppeteer: una bifurcación (fork) de Puppeteer sin las fugas comunes que son relativamente fáciles de detectar.
WindMouse: un modelo de movimiento del mouse basado en la física.
Una capa de comportamiento: visualizaciones aleatorias de Stories y likes en publicaciones antes de la extracción de datos, distribuciones de pausa log-normales, sobrepasos del objetivo (perder el blanco y corregir) y reacciones a los checkpoints.
WindMouse: por qué es mejor que las curvas de Bézier cúbicas
En nuestro artículo anterior, utilizamos curvas de Bézier cúbicas para simular el movimiento del mouse. Es un enfoque básico, pero tiene debilidades: al contar solo con dos puntos de control, la curva es suave y predecible. La trayectoria siempre es similar, la velocidad se controla por separado mediante un parámetro de aceleración (easing), lo que genera un movimiento demasiado uniforme, y el microtemblor se añade como una capa independiente, haciendo evidente que el movimiento consiste en una curva suave con ruido artificial.
WindMouse, en cambio, simula la física real. La gravedad atrae el cursor hacia el objetivo, mientras que el viento introduce desviaciones aleatorias que se acumulan con la inercia. Como resultado, la trayectoria del movimiento es fluida en lugar de ser brusca. La velocidad se limita mediante un tamaño máximo de paso y disminuye de forma natural cerca del destino, igual que el cursor de una persona real al apuntar a un objeto.
El resultado es una trayectoria con velocidad variable, desaceleración natural y micromovimientos sutiles que resultan indistinguibles del comportamiento humano real. Lo más importante es que, debido al componente de viento aleatorio, la trayectoria es diferente cada vez.
Añadimos otros dos comportamientos muy humanos:
Sobrepaso (Overshoot): en el 30 % de los casos, el cursor no da directamente en el botón a propósito, sino que se pasa ligeramente y luego regresa hacia él.
Temblor de clic (Click jitter): entre
mouse.down()ymouse.up(), el cursor se desplaza entre 1 y 2 píxeles en una dirección aleatoria.
La implementación de WindMouse en el script es la función wind_mouse_trajectory. Esta genera un array de puntos de trayectoria que posteriormente utiliza page.mouse.move().
Script listo para usar para extraer seguidores de cuentas objetivo y preparar cuentas simultáneamente
Asumamos que ya tiene un array de UUIDs de perfiles de Octo con sesiones iniciadas en cuentas de Instagram. Nuestro script iniciará cada perfil, preparará la cuenta dando likes a publicaciones en el feed y viendo Stories, y luego procederá a los perfiles de Instagram especificados como objetivos de extracción. Allí, dará likes a publicaciones, seguirá cuentas y finalmente extraerá la lista de seguidores en formatos JSON y CSV. Todo esto se realiza con movimientos naturales del mouse, retrasos aleatorios y detección de checkpoints. Como resultado, Instagram no puede distinguir su automatización de la actividad de un usuario normal.
Cómo preparar perfiles
Cree uno o más perfiles en Octo y asigne un proxy a cada uno. Se recomiendan proxies residenciales o móviles.
Abra manualmente cada perfil en Octo. Inicie sesión o registre una cuenta de Instagram. Durante dos o tres días, use la cuenta como una persona normal: dé likes a publicaciones, mire Stories y siga a algunos usuarios.
Copie los UUIDs de los perfiles de Octo. Los necesitará al completar la configuración del script.
Primeros pasos
Descargue e instale VS Code.
Descargue e instale Node.js.
Cree una carpeta en cualquier lugar de su dispositivo y llámela, por ejemplo,
octo_instagram_scraper.Abra la carpeta en VS Code.
Cree un archivo .js. Es mejor nombrarlo según la acción que realiza el script para evitar confusiones. Por ejemplo,
octo_instagram_scraper.js.Pegue el código del script en el archivo.
Complete la configuración en la variable
config.
UUID — IDs de perfil de Octo.
target_accounts — las cuentas que desea extraer.
followers_per_target — el número de seguidores que desea recopilar de cada cuenta objetivo durante una sola ejecución.
Los parámetros restantes en config controlan un comportamiento del scraper realista. Puede experimentar con ellos o dejarlos tal como están.

Abra su terminal y ejecute el siguiente comando para instalar las dependencias necesarias de Node.js:
npm i rebrowser-puppeteer axios

Si VS Code muestra un error, abra Windows PowerShell como Administrador, ejecute el siguiente comando y confirme el cambio:
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSignedLuego repita el paso anterior.
Inicie Octo Browser.
. Ejecute el script en Visual Studio Code (
Ctrl/Cmd + F5) y espere a que finalice.
El scraper iniciará secuencialmente los perfiles especificados en su configuración. Luego, al igual que un usuario regular, mirará Stories, se desplazará por el feed, dará likes a publicaciones, seguirá cuentas y extraerá los seguidores de las cuentas objetivo. Puede supervisar el proceso en la consola de depuración.

Resumen de resultados del parser. En este ejemplo, se procesaron dos cuentas objetivo de cada perfil: 3 de las 4 ejecuciones se completaron con éxito.
Tenga en cuenta que Instagram puede limitar la cantidad de seguidores devueltos, dependiendo de varios factores como el nivel de preparación de la cuenta, la cantidad de conexiones mutuas, si la cuenta sigue a la cuenta objetivo, entre otros. Si algo no funciona como se espera, experimente con actividad adicional de preparación o cambie de cuentas y proxies.

Resultados del scraper en formato CSV

Resultados del scraper en formato JSON
El formato JSON es más conveniente para un procesamiento posterior en código, mientras que el formato CSV es ideal para importar a Excel, Google Sheets, sistemas CRM o subirlo directamente a Meta Ads como una Audiencia Personalizada.
Código del 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.'); })();
Escalamiento y seguridad de la cuenta
Lista de verificación antes del lanzamiento:
Los perfiles están debidamente preparados. La cuenta debe tener al menos 2–3 semanas de antigüedad, contar con una foto de perfil, publicaciones y cierta actividad de seguimientos.
No utilice proxies de centros de datos. Utilice proxies residenciales o móviles.
Verifique que los selectores estén actualizados. Antes de ejecutar el script en una gran cantidad de cuentas, pruébelo en unas cuantas cuentas y confirme que todos los selectores funcionan correctamente.
Añada pausas entre ejecuciones. Nuestro script procesa los perfiles en
config.profilesde manera secuencial, con una pausa de 60 a 120 segundos entre ellos. Si utiliza más cuentas, aumente este intervalo.
Conclusión
Este único script cubre dos de los casos de uso más populares: extracción de seguidores (para investigación de competidores y creación de audiencias similares) y likes masivos (para preparar cuentas). Se puede ampliar fácilmente con funcionalidades adicionales:
Extraer usuarios que dieron like o comentaron en una publicación específica para obtener el segmento de audiencia con mayor interacción.
Recopilar información de biografía y direcciones de correo electrónico de los comentaristas. Esto se puede trasladar a un flujo de trabajo independiente para evitar sobrecargar la cuenta del scraper con acciones adicionales.
Comparar listas de seguidores de múltiples competidores e identificar coincidencias. Esto es fácil de implementar directamente en Node.js utilizando los datos JSON recopilados.
Añadir una capa de caché para evitar extraer repetidamente a los mismos seguidores en diferentes ejecuciones.
Si trabaja a gran escala, también puede adaptar el script para una ejecución en paralelo.
La extracción de datos exitosa se basa en un sistema de múltiples capas. Octo Browser proporciona una huella digital constante y aislamiento de perfiles; rebrowser-puppeteer ofrece automatización con correcciones para las fugas comunes de detección de automatización; el algoritmo WindMouse genera movimientos de cursor realistas, y la capa de comportamiento introduce distracciones y pausas. En última instancia, la resiliencia del sistema no depende de un solo componente, sino de qué tan eficazmente trabajen todos en conjunto.
Manténgase anónimo, aproveche el multicuentas y logre sus objetivos con el navegador antidetección de mayor calidad del mercado.
¿Te gustaría probar Octo Browser con descuento?
Usa el código promocional OCTOSCRAPER para obtener un 30% de descuento en cualquier suscripción. Esta oferta solo es válida para nuevos usuarios.
Qué extrae la gente de Instagram y por qué
Tras analizar el mercado de herramientas y servicios de Instagram en 2025–2026, podemos identificar seis tipos principales de datos que se suelen extraer con frecuencia:
Datos | Campos | Propósito |
Seguidores de la cuenta | username, full_name, verified, is_private, bio | Investigación de competidores, búsqueda de leads de alta calidad, análisis de audiencia para Meta Ads |
Seguidos de la cuenta | username, full_name, verified, is_private, bio | Análisis de audiencia objetivo de competidores e influencers |
Usuarios a los que les gustó una publicación | username, full_name | Búsqueda de usuarios con una interacción superior a la media de seguidores |
Comentaristas | username, comment text | Prospección por mensajes directos (DM) y análisis de sentimiento |
Publicaciones por hashtag/ubicación | post_url, likes, caption | Análisis de contenido y descubrimiento de UGC (contenido generado por el usuario) |
Perfil completo | bio, posts, ER | Evaluación de influencers antes de una colaboración |
Casos de uso principales:
Investigación de competidores. Extraiga los seguidores de 3 a 5 competidores, elimine duplicados e identifique coincidencias. Los leads de mayor calidad suelen ser usuarios que siguen a varios competidores al mismo tiempo.
Evaluación de influencers/bloggers. Antes de pagar por una colaboración, recopile los datos de sus seguidores y evalúe qué tan auténtica es su audiencia.
Prospección por mensajes directos (DM) y correos en frío. En los nichos de SaaS, marketing y comercio electrónico, entre el 15 % y el 35 % de los perfiles incluyen una dirección de correo electrónico pública en su biografía.
Audiencias similares en Meta Ads. Los nombres de usuario se pueden convertir en una audiencia semilla personalizada para crear audiencias similares (Lookalike Audiences).
Preparación de cuentas mediante likes masivos. Se utiliza para aumentar el alcance y establecer un patrón de actividad normal en la cuenta.
Por qué se detectan los bots comunes en 2026
El mayor cambio de los últimos años es que Instagram ahora oculta el contenido a los usuarios no autenticados. Anteriormente, se podían realizar consultas a endpoints públicos como /?__a=1 sin necesidad de iniciar sesión. En 2026, incluso los seguidores de una cuenta pública no están disponibles a menos que haya iniciado sesión.
Lo que necesitará para su script:
1. Una cuenta real con sesión iniciada. Preferiblemente una cuenta con bastante antigüedad y bien preparada, en lugar de una recién creada. Octo Browser ayuda en esto: puede crear una cuenta, iniciar sesión manualmente, dejar que gane antigüedad de forma natural y solo entonces automatizarla.
2. Una cuenta significa una huella digital. Ejecutar 20 cuentas de Instagram desde una sola instancia de Chrome es una forma garantizada de ser bloqueado. Con Octo, puede utilizar perfiles aislados con diferentes huellas digitales (parámetros de WebGL, resolución de pantalla, fuentes, user-agent, etc.).
3. Un proxy para cada perfil. Los proxies residenciales o móviles son ideales. Utilizar direcciones IP de centros de datos es sumamente riesgoso.
Sin embargo, incluso con una huella digital de navegador adecuada y un proxy confiable, un bot de Puppeteer no sobrevivirá sin cierto nivel de emulación del comportamiento humano. Los sistemas antibot analizan:
Patrones de frecuencia. Cuántos likes por hora, cuántos seguimientos por día y los intervalos entre acciones. Las analíticas del lado del servidor detectarán rápidamente a un bot que da like a una publicación cada 30 segundos.
Huella digital del navegador (Browser fingerprinting). Por eso es fundamental utilizar un navegador antidetección.
Huella digital de red/TLS. Por eso son importantes los proxies de calidad y una pila TLS no personalizada.
Vulnerabilidades del entorno de automatización (
navigator.webdriver, presencia de CDP, rastros distintivos enError.stack).
Conclusión práctica: en 2026, los motivos principales por los que se suspenden las cuentas son los patrones de frecuencia de comportamiento (demasiadas acciones en un periodo corto, intervalos excesivamente uniformes), las huellas digitales del navegador y los proxies. Emular movimientos del mouse, el desplazamiento por la pantalla o la escritura aún no es estrictamente necesario. En la práctica, sin embargo, estas señales pueden seguir utilizándose para analizar el comportamiento e incrementar el nivel de sospecha (fraud score) de una cuenta.
Por eso, la arquitectura de nuestro script consta de:
Octo Browser: huella digital aislada, proxy y un perfil con una sesión real de Instagram.
Rebrowser-Puppeteer: una bifurcación (fork) de Puppeteer sin las fugas comunes que son relativamente fáciles de detectar.
WindMouse: un modelo de movimiento del mouse basado en la física.
Una capa de comportamiento: visualizaciones aleatorias de Stories y likes en publicaciones antes de la extracción de datos, distribuciones de pausa log-normales, sobrepasos del objetivo (perder el blanco y corregir) y reacciones a los checkpoints.
WindMouse: por qué es mejor que las curvas de Bézier cúbicas
En nuestro artículo anterior, utilizamos curvas de Bézier cúbicas para simular el movimiento del mouse. Es un enfoque básico, pero tiene debilidades: al contar solo con dos puntos de control, la curva es suave y predecible. La trayectoria siempre es similar, la velocidad se controla por separado mediante un parámetro de aceleración (easing), lo que genera un movimiento demasiado uniforme, y el microtemblor se añade como una capa independiente, haciendo evidente que el movimiento consiste en una curva suave con ruido artificial.
WindMouse, en cambio, simula la física real. La gravedad atrae el cursor hacia el objetivo, mientras que el viento introduce desviaciones aleatorias que se acumulan con la inercia. Como resultado, la trayectoria del movimiento es fluida en lugar de ser brusca. La velocidad se limita mediante un tamaño máximo de paso y disminuye de forma natural cerca del destino, igual que el cursor de una persona real al apuntar a un objeto.
El resultado es una trayectoria con velocidad variable, desaceleración natural y micromovimientos sutiles que resultan indistinguibles del comportamiento humano real. Lo más importante es que, debido al componente de viento aleatorio, la trayectoria es diferente cada vez.
Añadimos otros dos comportamientos muy humanos:
Sobrepaso (Overshoot): en el 30 % de los casos, el cursor no da directamente en el botón a propósito, sino que se pasa ligeramente y luego regresa hacia él.
Temblor de clic (Click jitter): entre
mouse.down()ymouse.up(), el cursor se desplaza entre 1 y 2 píxeles en una dirección aleatoria.
La implementación de WindMouse en el script es la función wind_mouse_trajectory. Esta genera un array de puntos de trayectoria que posteriormente utiliza page.mouse.move().
Script listo para usar para extraer seguidores de cuentas objetivo y preparar cuentas simultáneamente
Asumamos que ya tiene un array de UUIDs de perfiles de Octo con sesiones iniciadas en cuentas de Instagram. Nuestro script iniciará cada perfil, preparará la cuenta dando likes a publicaciones en el feed y viendo Stories, y luego procederá a los perfiles de Instagram especificados como objetivos de extracción. Allí, dará likes a publicaciones, seguirá cuentas y finalmente extraerá la lista de seguidores en formatos JSON y CSV. Todo esto se realiza con movimientos naturales del mouse, retrasos aleatorios y detección de checkpoints. Como resultado, Instagram no puede distinguir su automatización de la actividad de un usuario normal.
Cómo preparar perfiles
Cree uno o más perfiles en Octo y asigne un proxy a cada uno. Se recomiendan proxies residenciales o móviles.
Abra manualmente cada perfil en Octo. Inicie sesión o registre una cuenta de Instagram. Durante dos o tres días, use la cuenta como una persona normal: dé likes a publicaciones, mire Stories y siga a algunos usuarios.
Copie los UUIDs de los perfiles de Octo. Los necesitará al completar la configuración del script.
Primeros pasos
Descargue e instale VS Code.
Descargue e instale Node.js.
Cree una carpeta en cualquier lugar de su dispositivo y llámela, por ejemplo,
octo_instagram_scraper.Abra la carpeta en VS Code.
Cree un archivo .js. Es mejor nombrarlo según la acción que realiza el script para evitar confusiones. Por ejemplo,
octo_instagram_scraper.js.Pegue el código del script en el archivo.
Complete la configuración en la variable
config.
UUID — IDs de perfil de Octo.
target_accounts — las cuentas que desea extraer.
followers_per_target — el número de seguidores que desea recopilar de cada cuenta objetivo durante una sola ejecución.
Los parámetros restantes en config controlan un comportamiento del scraper realista. Puede experimentar con ellos o dejarlos tal como están.

Abra su terminal y ejecute el siguiente comando para instalar las dependencias necesarias de Node.js:
npm i rebrowser-puppeteer axios

Si VS Code muestra un error, abra Windows PowerShell como Administrador, ejecute el siguiente comando y confirme el cambio:
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSignedLuego repita el paso anterior.
Inicie Octo Browser.
. Ejecute el script en Visual Studio Code (
Ctrl/Cmd + F5) y espere a que finalice.
El scraper iniciará secuencialmente los perfiles especificados en su configuración. Luego, al igual que un usuario regular, mirará Stories, se desplazará por el feed, dará likes a publicaciones, seguirá cuentas y extraerá los seguidores de las cuentas objetivo. Puede supervisar el proceso en la consola de depuración.

Resumen de resultados del parser. En este ejemplo, se procesaron dos cuentas objetivo de cada perfil: 3 de las 4 ejecuciones se completaron con éxito.
Tenga en cuenta que Instagram puede limitar la cantidad de seguidores devueltos, dependiendo de varios factores como el nivel de preparación de la cuenta, la cantidad de conexiones mutuas, si la cuenta sigue a la cuenta objetivo, entre otros. Si algo no funciona como se espera, experimente con actividad adicional de preparación o cambie de cuentas y proxies.

Resultados del scraper en formato CSV

Resultados del scraper en formato JSON
El formato JSON es más conveniente para un procesamiento posterior en código, mientras que el formato CSV es ideal para importar a Excel, Google Sheets, sistemas CRM o subirlo directamente a Meta Ads como una Audiencia Personalizada.
Código del 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.'); })();
Escalamiento y seguridad de la cuenta
Lista de verificación antes del lanzamiento:
Los perfiles están debidamente preparados. La cuenta debe tener al menos 2–3 semanas de antigüedad, contar con una foto de perfil, publicaciones y cierta actividad de seguimientos.
No utilice proxies de centros de datos. Utilice proxies residenciales o móviles.
Verifique que los selectores estén actualizados. Antes de ejecutar el script en una gran cantidad de cuentas, pruébelo en unas cuantas cuentas y confirme que todos los selectores funcionan correctamente.
Añada pausas entre ejecuciones. Nuestro script procesa los perfiles en
config.profilesde manera secuencial, con una pausa de 60 a 120 segundos entre ellos. Si utiliza más cuentas, aumente este intervalo.
Conclusión
Este único script cubre dos de los casos de uso más populares: extracción de seguidores (para investigación de competidores y creación de audiencias similares) y likes masivos (para preparar cuentas). Se puede ampliar fácilmente con funcionalidades adicionales:
Extraer usuarios que dieron like o comentaron en una publicación específica para obtener el segmento de audiencia con mayor interacción.
Recopilar información de biografía y direcciones de correo electrónico de los comentaristas. Esto se puede trasladar a un flujo de trabajo independiente para evitar sobrecargar la cuenta del scraper con acciones adicionales.
Comparar listas de seguidores de múltiples competidores e identificar coincidencias. Esto es fácil de implementar directamente en Node.js utilizando los datos JSON recopilados.
Añadir una capa de caché para evitar extraer repetidamente a los mismos seguidores en diferentes ejecuciones.
Si trabaja a gran escala, también puede adaptar el script para una ejecución en paralelo.
La extracción de datos exitosa se basa en un sistema de múltiples capas. Octo Browser proporciona una huella digital constante y aislamiento de perfiles; rebrowser-puppeteer ofrece automatización con correcciones para las fugas comunes de detección de automatización; el algoritmo WindMouse genera movimientos de cursor realistas, y la capa de comportamiento introduce distracciones y pausas. En última instancia, la resiliencia del sistema no depende de un solo componente, sino de qué tan eficazmente trabajen todos en conjunto.
Mantente al día de las últimas noticias sobre Octo Browser
Al hacer clic en el botón, aceptas nuestra Política de privacidad.
Mantente al día de las últimas noticias sobre Octo Browser
Al hacer clic en el botón, aceptas nuestra Política de privacidad.
Mantente al día de las últimas noticias sobre Octo Browser
Al hacer clic en el botón, aceptas nuestra Política de privacidad.

Únete a Octo Browser ahora
O contacta al servicio al cliente en cualquier momento si tienes alguna pregunta.

Únete a Octo Browser ahora
O contacta al servicio al cliente en cualquier momento si tienes alguna pregunta.
Únete a Octo Browser ahora
O contacta al servicio al cliente en cualquier momento si tienes alguna pregunta.
