如何在 2026 年抓取 Instagram 数据
2026/6/3


Artur Hvalei
Technical Support Specialist, Octo Browser
在我们的上一篇文章中,我们介绍了使用 Octo Browser 和 Puppeteer 抓取 Google 搜索结果的方法。现在,让我们提高标准,向 Instagram 发起挑战,这是 2026 年拥有最严格反机器人系统之一的平台。我们将研究人们通常抓取的数据类型以及他们这样做的原因。我们将解释为什么标准的机器人会迅速遇到限制和封禁。我们还将分析可行解决方案的架构、我们在测试过程中遇到的问题,以及在不经常遇到账号问题的情况下扩展数据收集的方法。
内容
保持匿名,充分利用多账户功能,借助市面上最优质的反检测浏览器实现您的目标。
人们会从 Instagram 抓取哪些数据,以及出于什么原因
在分析了 2025-2026 年 Instagram 工具和服务的市场后,我们梳理出了六种常见的被抓取数据类型:
数据 | 字段 | 目的 |
账号粉丝 | username, full_name, verified, is_private, bio | 竞品研究、挖掘高质量销售线索、Meta 广告受众分析 |
账号关注 | username, full_name, verified, is_private, bio | 竞品和 KOL 目标受众分析 |
点赞帖子的用户 | username, full_name | 寻找比普通粉丝互动度更高的用户 |
评论者 | username, comment text | 私信触达和舆情分析 |
按标签/定位排序的帖子 | post_url, likes, caption | 内容分析和 UGC 发现 |
完整个人主页 | bio, posts, ER | 合作前的 KOL 评估 |
主要应用场景:
竞品研究。 抓取 3-5 个竞品的粉丝,去重并找出重合部分。同时关注多个竞品的用户往往是最优质的潜在客户。
KOL/博主评估。 在支付合作费用前,收集其粉丝数据并评估受众的真实性。
私信外链与冷电子邮件。 在 SaaS、营销和电商等细分领域,有 15-35% 的个人主页在其简介中包含公开电子邮件地址。
Meta 广告中的类似受众 (Lookalike Audiences)。 用户名可以转化为自定义受众种子,以创建类似受众。
通过大量点赞养号。 用于增加曝光率并建立正常账号活动的模式。
为什么普通的机器人程序在 2026 年会被检测到
近几年最大的变化在于 Instagram 现在对未登录用户隐藏了内容。在以前,你可以向诸如 /?__a=1 的公开端点请求数据而无需会话。但是在 2026 年,除非登录,否则即使是公开账号的粉丝列表也无法访问。
您的脚本需要准备什么:
1. 一个真实登录的账号。最好是经过静置养号的账号,而不是刚刚创建的新账号。Octo Browser 可以在这方面提供帮助:你可以创建账号,手动登录,让它自然累积权重,然后再进行自动化逻辑。
2. 一个账号对应一个指纹。在单个 Chrome 实例上运行 20 个 Instagram 账号百分百会被封号。使用 Octo,您可以使用具有不同指纹(WebGL 参数、屏幕分辨率、字体、用户代理等)隔离的配置文件。
3. 每个 Profile 配备单独的主机。 移动或住宅代理是最理想的选择。使用数据中心 IP 地址极其冒险。
然而,即使拥有适当的浏览器指纹和可靠的代理,如果没有某种程度的人类行为模拟,Puppeteer 机器人也无法存活。反爬虫系统会分析:
频率模式。每小时点赞数、每天关注数以及操作之间的间隔。如果机器人每 30 秒点赞一次帖子,服务器端分析很快就会标记它。
浏览器指纹识别。这就是为什么使用反检测浏览器至关重要的原因。
TLS/网络指纹识别。这就是高质量代理和非自定义 TLS 协议栈发挥作用的地方。
自动化框架漏洞 (
navigator.webdriver、CDP 的存在、Error.stack中的特征性痕迹)。
实践总结:在 2026 年,账号被封的主要原因是行为频率模式(短时间内进行过多操作、间隔过于一致)、浏览器指纹和代理。模拟鼠标移动、滚动或输入在目前还不是严格必须的。然而,在实际应用中,这些信号仍可能被用于分析行为并增加账号的 欺诈评分。
这就是为什么我们的脚本架构由以下部分组成:
Octo Browser:隔离的指纹、代理以及包含真实 Instagram 会话的配置文件。
Rebrowser-Puppeteer:不包含常见泄露、且相对不易被检测的 Puppeteer 分支。
WindMouse:基于物理机制的鼠标移动模型。
行为层:抓取前随机浏览故事和点赞帖子、对数正态暂停分布、超调(未击中目标并进行校正)以及对检查点的反应。
WindMouse:为什么它比三次贝塞尔曲线更好
在我们的前一篇文章中,我们使用三次贝塞尔曲线来模拟鼠标移动。这是一种基础方法,但它存在弱点:仅带有两个控制点,曲线不仅平滑而且可预测。其轨迹总是高度相似,速度通过缓动参数单独控制,这导致移动过于均匀,微抖动则是作为单独的层加入的,容易暴露其平滑曲线加上人工噪声本质。
相比之下,WindMouse 模拟的是真实的物理效果。重力将光标拉向目标,而风力则带来随惯性累积的随机偏差。因此,移动路径既平滑又无突兀。速度受最大步长的限制,并会在接近目的地时自然减速,就像真人在操作光标对准物体一样。
由此产生的轨迹速度多变、减速自然并带有细微的微动,和人类在现实中的操作无异。最关键的是,由于随机风力组件的存在,每次移动的轨迹都是独特的。
我们另外加上了两个更接近人类习惯的行为:
超调:在 30% 的情况下,光标会故意稍微偏离按钮,然后再移回按钮上。
点击抖动:在
mouse.down()和mouse.up()之间,光标会在随机方向上偏移 1-2 个像素。
脚本中的 WindMouse 实现代码是 wind_mouse_trajectory 函数。它会生成后续供 page.mouse.move() 使用的轨迹点数组。
用于在养号的同时抓取目标账号粉丝的现成脚本
我们假设您已经拥有一个已登录 Instagram 账号的 Octo 配置文件 UUID 数组。我们的脚本将启动每个配置文件,通过点赞动态中的帖子和查看故事来对账号进行养号,然后前往指定为抓取目标账号的 Instagram 个人主页。在这些主页上它将点赞帖子、关注账号,并最终获取粉丝列表并导出为 JSON 和 CSV 格式。这一切都是通过自然的鼠标移动、随机延迟和检查点检测完成的。如此一来,Instagram 便无法向系统发出将其与普通用户活动区分开的信号。
准备配置文件
在 Octo 中创建一个或多个配置文件,并为每个配置文件分配一个代理。推荐使用住宅或移动代理。
在 Octo 中手动打开每个配置文件。登录或注册 Instagram 账号。维持 2 到 3 天像普通人一样使用它:点赞帖子、看故事,并关注其他用户。
从 Octo 复制配置文件的 UUID。在填写脚本配置时您需要用到它们。
开始使用
下载并安装 VS Code。
下载并安装 Node.js。
在您设备的任意位置创建一个文件夹,并将其命名为:例如
octo_instagram_scraper。在 VS Code 中打开该文件夹。
创建一个 .js 文件。最好根据脚本执行的操作为其命名,以免混淆。例如:
octo_instagram_scraper.js。将脚本代码粘贴到该文件中。
在
config变量中填写配置。
UUID — Octo 配置文件 ID。
target_accounts — 您想要抓取的目标账号。
followers_per_target — 每次运行想要从每个目标账号收集的粉丝数量。config 中的其余参数用于控制抓取器表现出真实的真人行为。您可以保留默认设置或进行调整。

打开终端并运行以下命令,安装所需的 Node.js 依赖项:
npm i rebrowser-puppeteer axios

如果 VS Code 报错,请以管理员身份打开 Windows PowerShell,运行以下命令并确认更改:
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned然后重试上一步的操作。
启动 Octo Browser。
在 Visual Studio Code 中运行脚本 (
Ctrl/Cmd + F5) 并等待其执行完毕。
爬虫将依序启动您的配置中所指定的配置文件。接着,它就像普通用户一样查看故事、滚动浏览 feed、点赞帖子、关注账号,并从目标账号中获取粉丝。您可以在调试控制台中监控整个过程。

解析器结果汇总。在此示例中,每个配置文件处理了两个目标账号:4 次运行中成功完成了 3 次。
请记住,Instagram 可能会根据各种因素限制返回的粉丝数量,例如账号的养号水平、共同好友数量、是否关注了目标账号等。如果未能按预期运行,请尝试增加养号活动或是切换账号和代理。

CSV 格式的抓取结果

JSON 格式的抓取结果
JSON 更方便后续代码处理,而 CSV 则非常适合导入到 Excel、Google 表格、CRM 系统中,或者直接上传至 Meta 广告作为自定义受众。
脚本代码
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.'); })();
扩充规模与账号安全
启动前检查清单:
配置文件已适当养号。 账号注册时间应至少达到 2 到 3 周、有个人头像和帖子、并带有一定频次的关注行为。
切勿使用数据中心代理。 建议使用住宅或移动代理。
确保选择器处于最新状态。 在对大量账号批量运行脚本前,应先在少量账号上测试确认所有选择器都可以正常工作。
在每次运行间隔添加延迟。我们的脚本会顺序处理
config.profiles中的配置文件,并在它们之间提供 60 到 180 秒的暂停。如果您需要使用更多账号,请进一步调大此间隔时间。
结语
这套单一脚本涵盖了两种最热门的使用场景:粉丝抓取(用于竞品研究和类似受众构建)以及批量点赞(用于养号)。您可以轻松对它进行更多功能性质的拓展:
抓取对特定帖子点赞或评论的用户,精准获取互动率最高的受众群组。
收集评论者的简介信息和电子邮箱地址。此功能建议设计成独立的流水线,免得爬行账号因为操作过多引发警告。
对比多个竞品的粉丝列表并找出重合部分。使用采集好的 JSON 数据,在原生的 Node.js 中非常容易实现这一功能。
增设缓存层以规避在多次运行中重复抓取相同粉丝造成的性能耗损。
在需要大量执行任务的情况下,您还可以将脚本改造成并行执行模式。
成功的爬取有赖于多维度支撑的体系架构。Octo Browser 提供统一的指纹和配置文件隔离,rebrowser-puppeteer 的运用实现了规避常见自动化检测工具特征的程序化交互,WindMouse 算法生成近乎真人的光标移动路径,而行为层则负责插入随机动作和时钟停顿。最终的实操稳定性并不取决于单一的组件,而是取决于所有组件协作发挥功效的复合成果。
保持匿名,充分利用多账户功能,借助市面上最优质的反检测浏览器实现您的目标。
人们会从 Instagram 抓取哪些数据,以及出于什么原因
在分析了 2025-2026 年 Instagram 工具和服务的市场后,我们梳理出了六种常见的被抓取数据类型:
数据 | 字段 | 目的 |
账号粉丝 | username, full_name, verified, is_private, bio | 竞品研究、挖掘高质量销售线索、Meta 广告受众分析 |
账号关注 | username, full_name, verified, is_private, bio | 竞品和 KOL 目标受众分析 |
点赞帖子的用户 | username, full_name | 寻找比普通粉丝互动度更高的用户 |
评论者 | username, comment text | 私信触达和舆情分析 |
按标签/定位排序的帖子 | post_url, likes, caption | 内容分析和 UGC 发现 |
完整个人主页 | bio, posts, ER | 合作前的 KOL 评估 |
主要应用场景:
竞品研究。 抓取 3-5 个竞品的粉丝,去重并找出重合部分。同时关注多个竞品的用户往往是最优质的潜在客户。
KOL/博主评估。 在支付合作费用前,收集其粉丝数据并评估受众的真实性。
私信外链与冷电子邮件。 在 SaaS、营销和电商等细分领域,有 15-35% 的个人主页在其简介中包含公开电子邮件地址。
Meta 广告中的类似受众 (Lookalike Audiences)。 用户名可以转化为自定义受众种子,以创建类似受众。
通过大量点赞养号。 用于增加曝光率并建立正常账号活动的模式。
为什么普通的机器人程序在 2026 年会被检测到
近几年最大的变化在于 Instagram 现在对未登录用户隐藏了内容。在以前,你可以向诸如 /?__a=1 的公开端点请求数据而无需会话。但是在 2026 年,除非登录,否则即使是公开账号的粉丝列表也无法访问。
您的脚本需要准备什么:
1. 一个真实登录的账号。最好是经过静置养号的账号,而不是刚刚创建的新账号。Octo Browser 可以在这方面提供帮助:你可以创建账号,手动登录,让它自然累积权重,然后再进行自动化逻辑。
2. 一个账号对应一个指纹。在单个 Chrome 实例上运行 20 个 Instagram 账号百分百会被封号。使用 Octo,您可以使用具有不同指纹(WebGL 参数、屏幕分辨率、字体、用户代理等)隔离的配置文件。
3. 每个 Profile 配备单独的主机。 移动或住宅代理是最理想的选择。使用数据中心 IP 地址极其冒险。
然而,即使拥有适当的浏览器指纹和可靠的代理,如果没有某种程度的人类行为模拟,Puppeteer 机器人也无法存活。反爬虫系统会分析:
频率模式。每小时点赞数、每天关注数以及操作之间的间隔。如果机器人每 30 秒点赞一次帖子,服务器端分析很快就会标记它。
浏览器指纹识别。这就是为什么使用反检测浏览器至关重要的原因。
TLS/网络指纹识别。这就是高质量代理和非自定义 TLS 协议栈发挥作用的地方。
自动化框架漏洞 (
navigator.webdriver、CDP 的存在、Error.stack中的特征性痕迹)。
实践总结:在 2026 年,账号被封的主要原因是行为频率模式(短时间内进行过多操作、间隔过于一致)、浏览器指纹和代理。模拟鼠标移动、滚动或输入在目前还不是严格必须的。然而,在实际应用中,这些信号仍可能被用于分析行为并增加账号的 欺诈评分。
这就是为什么我们的脚本架构由以下部分组成:
Octo Browser:隔离的指纹、代理以及包含真实 Instagram 会话的配置文件。
Rebrowser-Puppeteer:不包含常见泄露、且相对不易被检测的 Puppeteer 分支。
WindMouse:基于物理机制的鼠标移动模型。
行为层:抓取前随机浏览故事和点赞帖子、对数正态暂停分布、超调(未击中目标并进行校正)以及对检查点的反应。
WindMouse:为什么它比三次贝塞尔曲线更好
在我们的前一篇文章中,我们使用三次贝塞尔曲线来模拟鼠标移动。这是一种基础方法,但它存在弱点:仅带有两个控制点,曲线不仅平滑而且可预测。其轨迹总是高度相似,速度通过缓动参数单独控制,这导致移动过于均匀,微抖动则是作为单独的层加入的,容易暴露其平滑曲线加上人工噪声本质。
相比之下,WindMouse 模拟的是真实的物理效果。重力将光标拉向目标,而风力则带来随惯性累积的随机偏差。因此,移动路径既平滑又无突兀。速度受最大步长的限制,并会在接近目的地时自然减速,就像真人在操作光标对准物体一样。
由此产生的轨迹速度多变、减速自然并带有细微的微动,和人类在现实中的操作无异。最关键的是,由于随机风力组件的存在,每次移动的轨迹都是独特的。
我们另外加上了两个更接近人类习惯的行为:
超调:在 30% 的情况下,光标会故意稍微偏离按钮,然后再移回按钮上。
点击抖动:在
mouse.down()和mouse.up()之间,光标会在随机方向上偏移 1-2 个像素。
脚本中的 WindMouse 实现代码是 wind_mouse_trajectory 函数。它会生成后续供 page.mouse.move() 使用的轨迹点数组。
用于在养号的同时抓取目标账号粉丝的现成脚本
我们假设您已经拥有一个已登录 Instagram 账号的 Octo 配置文件 UUID 数组。我们的脚本将启动每个配置文件,通过点赞动态中的帖子和查看故事来对账号进行养号,然后前往指定为抓取目标账号的 Instagram 个人主页。在这些主页上它将点赞帖子、关注账号,并最终获取粉丝列表并导出为 JSON 和 CSV 格式。这一切都是通过自然的鼠标移动、随机延迟和检查点检测完成的。如此一来,Instagram 便无法向系统发出将其与普通用户活动区分开的信号。
准备配置文件
在 Octo 中创建一个或多个配置文件,并为每个配置文件分配一个代理。推荐使用住宅或移动代理。
在 Octo 中手动打开每个配置文件。登录或注册 Instagram 账号。维持 2 到 3 天像普通人一样使用它:点赞帖子、看故事,并关注其他用户。
从 Octo 复制配置文件的 UUID。在填写脚本配置时您需要用到它们。
开始使用
下载并安装 VS Code。
下载并安装 Node.js。
在您设备的任意位置创建一个文件夹,并将其命名为:例如
octo_instagram_scraper。在 VS Code 中打开该文件夹。
创建一个 .js 文件。最好根据脚本执行的操作为其命名,以免混淆。例如:
octo_instagram_scraper.js。将脚本代码粘贴到该文件中。
在
config变量中填写配置。
UUID — Octo 配置文件 ID。
target_accounts — 您想要抓取的目标账号。
followers_per_target — 每次运行想要从每个目标账号收集的粉丝数量。config 中的其余参数用于控制抓取器表现出真实的真人行为。您可以保留默认设置或进行调整。

打开终端并运行以下命令,安装所需的 Node.js 依赖项:
npm i rebrowser-puppeteer axios

如果 VS Code 报错,请以管理员身份打开 Windows PowerShell,运行以下命令并确认更改:
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned然后重试上一步的操作。
启动 Octo Browser。
在 Visual Studio Code 中运行脚本 (
Ctrl/Cmd + F5) 并等待其执行完毕。
爬虫将依序启动您的配置中所指定的配置文件。接着,它就像普通用户一样查看故事、滚动浏览 feed、点赞帖子、关注账号,并从目标账号中获取粉丝。您可以在调试控制台中监控整个过程。

解析器结果汇总。在此示例中,每个配置文件处理了两个目标账号:4 次运行中成功完成了 3 次。
请记住,Instagram 可能会根据各种因素限制返回的粉丝数量,例如账号的养号水平、共同好友数量、是否关注了目标账号等。如果未能按预期运行,请尝试增加养号活动或是切换账号和代理。

CSV 格式的抓取结果

JSON 格式的抓取结果
JSON 更方便后续代码处理,而 CSV 则非常适合导入到 Excel、Google 表格、CRM 系统中,或者直接上传至 Meta 广告作为自定义受众。
脚本代码
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.'); })();
扩充规模与账号安全
启动前检查清单:
配置文件已适当养号。 账号注册时间应至少达到 2 到 3 周、有个人头像和帖子、并带有一定频次的关注行为。
切勿使用数据中心代理。 建议使用住宅或移动代理。
确保选择器处于最新状态。 在对大量账号批量运行脚本前,应先在少量账号上测试确认所有选择器都可以正常工作。
在每次运行间隔添加延迟。我们的脚本会顺序处理
config.profiles中的配置文件,并在它们之间提供 60 到 180 秒的暂停。如果您需要使用更多账号,请进一步调大此间隔时间。
结语
这套单一脚本涵盖了两种最热门的使用场景:粉丝抓取(用于竞品研究和类似受众构建)以及批量点赞(用于养号)。您可以轻松对它进行更多功能性质的拓展:
抓取对特定帖子点赞或评论的用户,精准获取互动率最高的受众群组。
收集评论者的简介信息和电子邮箱地址。此功能建议设计成独立的流水线,免得爬行账号因为操作过多引发警告。
对比多个竞品的粉丝列表并找出重合部分。使用采集好的 JSON 数据,在原生的 Node.js 中非常容易实现这一功能。
增设缓存层以规避在多次运行中重复抓取相同粉丝造成的性能耗损。
在需要大量执行任务的情况下,您还可以将脚本改造成并行执行模式。
成功的爬取有赖于多维度支撑的体系架构。Octo Browser 提供统一的指纹和配置文件隔离,rebrowser-puppeteer 的运用实现了规避常见自动化检测工具特征的程序化交互,WindMouse 算法生成近乎真人的光标移动路径,而行为层则负责插入随机动作和时钟停顿。最终的实操稳定性并不取决于单一的组件,而是取决于所有组件协作发挥功效的复合成果。
随时获取最新的Octo Browser新闻
通过点击按钮,您同意我们的 隐私政策。
随时获取最新的Octo Browser新闻
通过点击按钮,您同意我们的 隐私政策。
随时获取最新的Octo Browser新闻
通过点击按钮,您同意我们的 隐私政策。

