Cách cào dữ liệu Instagram vào năm 2026
3/6/26


Artur Hvalei
Technical Support Specialist, Octo Browser
Trong bài viết trước, chúng tôi đã hướng dẫn thu thập kết quả tìm kiếm của Google bằng cách sử dụng Octo Browser và Puppeteer. Bây giờ, hãy nâng cao thử thách và tiếp cận Instagram, một nền tảng có một trong những hệ thống chống bot nghiêm ngặt nhất vào năm 2026. Chúng ta sẽ tìm hiểu các loại dữ liệu mà mọi người thường thu thập và lý do tại sao họ làm điều đó. Chúng tôi sẽ giải thích lý do tại sao các bot thông thường nhanh chóng gặp phải các hạn chế và lệnh cấm. Chúng tôi cũng sẽ xem xét kiến trúc của một giải pháp hoạt động hiệu quả, các vấn đề chúng tôi gặp phải trong quá trình thử nghiệm và các cách để mở rộng quy mô thu thập dữ liệu mà không liên tục gặp phải các sự cố về tài khoản.
Nội dung
Giữ kín danh tính, tận dụng tính năng nhiều tài khoản và đạt được mục tiêu của bạn với trình duyệt chống phát hiện chất lượng cao nhất trên thị trường.
Mọi người cạo dữ liệu gì từ Instagram và tại sao
Sau khi phân tích thị trường cho các công cụ và dịch vụ Instagram trong năm 2025–2026, chúng tôi có thể xác định sáu loại dữ liệu chính thường được cạo:
Dữ liệu | Trường thông tin | Mục đích |
Người theo dõi tài khoản | username, full_name, verified, is_private, bio | Nghiên cứu đối thủ cạnh tranh, tìm kiếm khách hàng tiềm năng chất lượng cao, phân tích đối tượng cho Meta Ads |
Tài khoản đang theo dõi | username, full_name, verified, is_private, bio | Phân tích đối tượng mục tiêu của đối thủ cạnh tranh và người có ảnh hưởng |
Người dùng đã thích một bài viết | username, full_name | Tìm kiếm người dùng có mức độ tương tác cao hơn mức trung bình của người theo dõi |
Người bình luận | username, comment text | Tiếp cận qua tin nhắn trực tiếp (DM) và phân tích cảm xúc |
Bài viết theo hashtag/vị trí | post_url, likes, caption | Phân tích nội dung và khám phá UGC (nội dung do người dùng tạo) |
Hồ sơ đầy đủ | bio, posts, ER | Đánh giá người có ảnh hưởng trước khi hợp tác |
Các trường hợp sử dụng chính:
Nghiên cứu đối thủ cạnh tranh. Cạo dữ liệu người theo dõi của 3–5 đối thủ cạnh tranh, loại bỏ các trùng lặp và xác định các điểm giao thoa. Những khách hàng tiềm năng chất lượng cao nhất thường là những người dùng theo dõi nhiều đối thủ cạnh tranh cùng một lúc.
Đánh giá người có ảnh hưởng/blogger. Trước khi trả tiền cho một sự hợp tác, hãy thu thập dữ liệu người theo dõi và đánh giá mức độ chân thực của đối tượng người xem.
Tiếp cận qua DM và email lạnh. Trong các thị trường ngách SaaS, marketing và thương mại điện tử, có 15–35% hồ sơ bao gồm địa chỉ email công khai trong phần giới thiệu (bio) của họ.
Đối tượng tương tự (Lookalike Audiences) trong Meta Ads. Tên người dùng có thể được chuyển đổi thành nhóm hạt giống Đối tượng tùy chỉnh để tạo Đối tượng tương tự.
Chuẩn bị tài khoản thông qua tương tác thích hàng loạt. Được sử dụng để tăng phạm vi tiếp cận và thiết lập một mô hình hoạt động tài khoản bình thường.
Tại sao các bot thông thường bị phát hiện vào năm 2026
Thay đổi lớn nhất trong những năm gần đây là Instagram hiện ẩn nội dung đối với những người dùng chưa xác thực. Trước đây, bạn có thể truy vấn các điểm cuối công khai như /?__a=1 mà không cần một phiên làm việc. Vào năm 2026, ngay cả những người theo dõi của một tài khoản công khai cũng không khả dụng trừ khi bạn đã đăng nhập.
Những gì bạn cần cho tập lệnh của mình:
1. Một tài khoản đã đăng nhập thực tế. Ưu tiên một tài khoản đã được chuẩn bị kỹ lưỡng thay vì một tài khoản vừa mới được tạo. Octo Browser giúp ích ở đây: bạn có thể tạo một tài khoản, đăng nhập thủ công, để nó hoạt động tự nhiên một thời gian, và chỉ sau đó mới tự động hóa nó.
2. Một tài khoản có nghĩa là một vân tay. Chạy 20 tài khoản Instagram từ một phiên bản Chrome duy nhất là cách chắc chắn nhất để bị khóa tài khoản. Với Octo, bạn có thể sử dụng các hồ sơ cô lập với các vân tay khác nhau (các tham số Canvas, độ phân giải màn hình, phông chữ, user-agent, v.v.).
3. Một proxy cho mỗi hồ sơ. Proxy di động hoặc proxy dân cư là lý tưởng nhất. Sử dụng các địa chỉ IP của trung tâm dữ liệu là vô cùng rủi ro.
Tuy nhiên, ngay cả với một vân tay trình duyệt chuẩn và một proxy đáng tin cậy, một bot Puppeteer vẫn sẽ không thể tồn tại nếu không có một mức độ mô phỏng hành vi của con người. Các hệ thống chống bot sẽ phân tích:
Mô hình tần suất. Có bao nhiêu lượt thích mỗi giờ, bao nhiêu lượt theo dõi mỗi ngày và khoảng thời gian giữa các hành động. Hệ thống phân tích phía máy chủ sẽ nhanh chóng gắn cờ một bot thích một bài viết cứ sau mỗi 30 giây.
Tạo vân tay trình duyệt. Đây là lý do tại sao việc sử dụng một trình duyệt chống phát hiện là vô cùng quan trọng.
Vân tay TLS/mạng. Đây là lý do tại sao các proxy chất lượng và một ngăn xếp TLS không tùy biến lại quan trọng.
Các lỗ hổng của khung tự động hóa (
navigator.webdriver, sự hiện diện của CDP, các dấu vết đặc trưng trongError.stack).
Bài học thực tế: vào năm 2026, những lý do chính khiến tài khoản bị khóa là các mô hình tần suất hành vi (quá nhiều hành động trong một khoảng thời gian ngắn, các khoảng thời gian quá đồng đều), vân tay trình duyệt và các proxy. Việc mô phỏng chuyển động chuột, cuộn trang hoặc gõ phím chưa hoàn toàn bắt buộc phải có. Tuy nhiên, trên thực tế, các tín hiệu này vẫn có thể được sử dụng để phân tích hành vi và tăng điểm gian lận của tài khoản.
Đó là lý do tại sao kiến trúc tập lệnh của chúng tôi bao gồm:
Octo Browser: vân tay bị cô lập, proxy và một hồ sơ với một phiên hoạt động thực tế trên Instagram.
Rebrowser-Puppeteer: một nhánh của Puppeteer không có các rò rỉ phổ biến, những thứ tương đối dễ bị phát hiện.
WindMouse: một mô hình chuyển động chuột dựa trên vật lý thực tế.
Một lớp hành vi: xem tin (story) ngẫu nhiên và thích bài viết trước khi cạo, phân phối tạm dừng log-normal, di chuyển quá mục tiêu (di chuyển lệch mục tiêu rồi điều chỉnh lại) và phản hồi đối với các điểm kiểm tra ứng dụng.
WindMouse: tại sao nó tốt hơn các đường cong Bézier bậc ba
Trong bài viết trước của chúng tôi, chúng tôi đã sử dụng các đường cong Bézier bậc ba để mô phỏng chuyển động chuột. Đây là một cách tiếp cận cơ bản, nhưng nó có những điểm yếu: chỉ với hai điểm kiểm soát, đường cong sẽ quá mượt mà và dễ đoán. Hình dạng luôn tương tự nhau, tốc độ được kiểm soát riêng biệt thông qua một tham số làm mượt, điều này tạo ra chuyển động quá đồng nhất, và các rung động nhỏ được thêm vào như một lớp riêng biệt, khiến nó lộ rõ rằng chuyển động bao gồm một đường cong mượt mà cộng với tiếng nhiễu nhân tạo.
Thay vào đó, WindMouse mô phỏng vật lý thực tế. Trọng lực kéo con trỏ về phía mục tiêu, trong khi gió tạo ra những sai lệch ngẫu nhiên tích tụ theo quán tính. Kết quả là, đường di chuyển sẽ mượt mà chứ không bị giật. Tốc độ bị giới hạn bởi kích thước bước tối đa và giảm dần một cách tự nhiên khi đến gần điểm đích, giống như con trỏ của một người thực sự khi nhắm vào một vật thể.
Kết quả thu được là một quỹ đạo với tốc độ thay đổi, sự giảm tốc tự nhiên và các vi chuyển động tinh tế không thể phân biệt được với hành vi thực của con người. Quan trọng nhất, do thành phần gió ngẫu nhiên, đường đi của con trỏ mỗi lần là hoàn toàn khác nhau.
Chúng tôi thêm hai hành vi giống con người hơn nữa:
Di chuyển quá mục tiêu (Overshoot): trong 30% trường hợp, con trỏ sẽ cố ý di chuyển lệch một chút so với nút bấm, sau đó mới di chuyển ngược lại nút đó.
Rung khi nhấp chuột (Click jitter): giữa
mouse.down()vàmouse.up(), con trỏ dịch chuyển 1–2 pixel theo hướng ngẫu nhiên.
Nền tảng của WindMouse trong tập lệnh là hàm wind_mouse_trajectory. Nó tạo ra một mảng các điểm quỹ đạo mà sau đó sẽ được sử dụng bởi page.mouse.move().
Tập lệnh có sẵn để cạo dữ liệu người theo dõi tài khoản mục tiêu và chuẩn bị tài khoản đồng thời
Giả sử bạn đã có một mảng các UUID của hồ sơ Octo đã đăng nhập vào các tài khoản Instagram. Tập lệnh của chúng tôi sẽ khởi chạy từng hồ sơ, chuẩn bị tài khoản bằng cách thích các bài viết trên bảng tin và xem Tin (Stories), sau đó tiến hành truy cập các hồ sơ Instagram được chỉ định làm mục tiêu cạo dữ liệu. Tại đó, nó sẽ thích các bài viết, theo dõi tài khoản và cuối cùng là cạo danh sách người theo dõi sang cả định dạng JSON và CSV. Tất cả những điều này được thực hiện với chuyển động chuột tự nhiên, thời gian trễ ngẫu nhiên và phát hiện điểm kiểm tra. Kết quả là, Instagram không thể phân biệt hoạt động tự động hóa của bạn với hoạt động thông thường của người dùng.
Chuẩn bị hồ sơ
Tạo một hoặc nhiều hồ sơ trong Octo và gán một proxy cho mỗi hồ sơ. Khuyến nghị sử dụng các proxy dân cư hoặc proxy di động.
Mở từng hồ sơ trong Octo bằng cách thủ công. Đăng nhập hoặc đăng ký một tài khoản Instagram. Trong vòng hai đến ba ngày, hãy sử dụng tài khoản như một người bình thường: thích các bài viết, xem Tin và theo dõi một số người dùng.
Sao chép các UUID của hồ sơ từ Octo. Bạn sẽ cần chúng khi điền cấu hình tập lệnh.
Bắt đầu
Tải xuống và cài đặt VS Code.
Tải xuống và cài đặt Node.js.
Tạo một thư mục ở bất kỳ đâu trên thiết bị của bạn và đặt tên cho nó, ví dụ:
octo_instagram_scraper.Mở thư mục đó trong VS Code.
Tạo một tệp .js. Tốt nhất là đặt tên tệp theo hành động mà tập lệnh thực hiện để tránh nhầm lẫn. Ví dụ:
octo_instagram_scraper.js.Dán mã tập lệnh vào tệp.
Điền thông tin cấu hình vào biến
config.
UUID — Các ID của hồ sơ Octo.
target_accounts — các tài khoản bạn muốn cạo dữ liệu.
followers_per_target — số lượng người theo dõi bạn muốn thu thập từ mỗi tài khoản mục tiêu trong một lần chạy duy nhất.
Các tham số còn lại trong config kiểm soát hành vi thực tế của công cụ cạo dữ liệu. Bạn có thể thử nghiệm với chúng hoặc giữ nguyên không đổi.

Mở terminal của bạn và chạy lệnh sau để cài đặt các phụ thuộc Node.js cần thiết:
npm i rebrowser-puppeteer axios

Nếu VS Code hiển thị lỗi, hãy mở Windows PowerShell với quyền Administrator, chạy lệnh sau và xác nhận thay đổi:
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSignedSau đó lặp lại bước trước đó.
Khởi chạy Octo Browser.
Chạy tập lệnh trong Visual Studio Code (
Ctrl/Cmd + F5) và đợi cho đến khi hoàn thành.
Công cụ cạo dữ liệu sẽ lần lượt khởi chạy các hồ sơ được chỉ định trong cấu hình của bạn. Sau đó, giống như một người dùng thông thường, nó sẽ xem Tin, cuộn bảng tin, thích các bài viết, theo dõi các tài khoản và cạo dữ liệu người theo dõi từ các tài khoản mục tiêu. Bạn có thể theo dõi quá trình này trong giao diện bảng điều khiển gỡ lỗi.

Tóm tắt kết quả phân tích. Trong ví dụ này, hai tài khoản mục tiêu đã được xử lý từ mỗi hồ sơ: 3 trong số 4 lần chạy đã hoàn thành thành công.
Hãy lưu ý rằng Instagram có thể giới hạn số lượng người theo dõi được trả về, tùy thuộc vào nhiều yếu tố khác nhau như mức độ chuẩn bị của tài khoản, số lượng kết nối chung, liệu tài khoản có theo dõi tài khoản mục tiêu hay không, và nhiều yếu tố khác. Nếu điều gì đó không hoạt động như mong đợi, hãy thử nghiệm thêm với một số hoạt động tương tác chuẩn bị hoặc chuyển đổi tài khoản và proxy.

Kết quả cạo dữ liệu ở định dạng CSV

Kết quả cạo dữ liệu ở định dạng JSON
Json sẽ thuận tiện hơn cho việc xử lý sâu hơn trong mã nguồn, trong khi CSV là lý tưởng để nhập vào Excel, Google Sheets, hệ thống CRM hoặc tải trực tiếp lên Meta Ads làm Đối tượng tùy chỉnh.
Mã tập lệnh
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 ratelimit, 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 ratelimit, 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.'); })();
Mở rộng quy mô và an toàn tài khoản
Danh sách kiểm tra trước khi khởi chạy:
Các hồ sơ được chuẩn bị đúng cách. Tài khoản phải có tuổi thọ ít nhất 2–3 tuần, có ảnh đại diện, bài viết và một số hoạt động theo dõi.
Không sử dụng các proxy trung tâm dữ liệu. Hãy sử dụng proxy dân cư hoặc proxy di động.
Xác minh rằng các bộ chọn (selectors) đã được cập nhật. Trước khi chạy tập lệnh trên một số lượng lớn tài khoản, hãy kiểm tra thử trên một vài tài khoản để xác nhận rằng tất cả các bộ chọn hoạt động chính xác.
Thêm thời gian trễ giữa các lần chạy. Tập lệnh của chúng tôi xử lý các hồ sơ trong
config.profilesmột cách tuần tự, với thời gian tạm dừng 60-180 giây giữa các tài khoản đó. Nếu bạn sử dụng nhiều tài khoản hơn, hãy tăng khoảng thời gian này lên.
Kết luận
Chỉ riêng tập lệnh này đã đáp ứng hai trong số các trường hợp sử dụng phổ biến nhất: cạo dữ liệu người theo dõi (để nghiên cứu đối thủ cạnh tranh và xây dựng đối tượng tương tự) và thích hàng loạt (để chuẩn bị tài khoản). Nó có thể dễ dàng mở rộng thêm các tính năng bổ sung rộng hơn:
Cạo dữ liệu người dùng đã thích hoặc bình luận trên một bài viết cụ thể để có được phân khúc đối tượng tương tác tốt nhất.
Thu thập thông tin giới thiệu (bio) và địa chỉ email từ những người bình luận. Tính năng này có thể được chuyển sang một quy trình riêng biệt để tránh làm quá tải tài khoản cạo dữ liệu với các hành động bổ sung.
So sánh danh sách người theo dõi của nhiều đối thủ cạnh tranh và xác định các điểm trùng lặp. Việc này có thể triển khai dễ dàng bằng mã Node.js thuần túy từ dữ liệu JSON thu thập được.
Thêm một lớp lưu trữ tạm (cache) để tránh cạo dữ liệu lặp lại cùng một nhóm người theo dõi qua nhiều lần chạy.
Nếu bạn làm việc ở quy mô lớn, bạn cũng có thể điều chỉnh tập lệnh để thực thi song song.
Cạo dữ liệu thành công phụ thuộc vào một hệ thống nhiều lớp. Octo Browser cung cấp vân tay nhất quán và sự cô lập hồ sơ, rebrowser-puppeteer mang lại khả năng tự động hóa cùng các bản sửa lỗi liên quan đến rò rỉ phát hiện tự động hóa phổ biến, thuật toán WindMouse tạo ra chuyển động con trỏ thực tế và lớp hành vi thêm vào các hoạt động xao nhãng và thời gian tạm dừng. Cuối cùng, khả năng phục hồi của hệ thống không phụ thuộc vào bất kỳ thành phần đơn lẻ nào, mà phụ thuộc vào mức độ phối hợp hiệu quả của tất cả các thành phần đó hoạt động cùng nhau.
Giữ kín danh tính, tận dụng tính năng nhiều tài khoản và đạt được mục tiêu của bạn với trình duyệt chống phát hiện chất lượng cao nhất trên thị trường.
Mọi người cạo dữ liệu gì từ Instagram và tại sao
Sau khi phân tích thị trường cho các công cụ và dịch vụ Instagram trong năm 2025–2026, chúng tôi có thể xác định sáu loại dữ liệu chính thường được cạo:
Dữ liệu | Trường thông tin | Mục đích |
Người theo dõi tài khoản | username, full_name, verified, is_private, bio | Nghiên cứu đối thủ cạnh tranh, tìm kiếm khách hàng tiềm năng chất lượng cao, phân tích đối tượng cho Meta Ads |
Tài khoản đang theo dõi | username, full_name, verified, is_private, bio | Phân tích đối tượng mục tiêu của đối thủ cạnh tranh và người có ảnh hưởng |
Người dùng đã thích một bài viết | username, full_name | Tìm kiếm người dùng có mức độ tương tác cao hơn mức trung bình của người theo dõi |
Người bình luận | username, comment text | Tiếp cận qua tin nhắn trực tiếp (DM) và phân tích cảm xúc |
Bài viết theo hashtag/vị trí | post_url, likes, caption | Phân tích nội dung và khám phá UGC (nội dung do người dùng tạo) |
Hồ sơ đầy đủ | bio, posts, ER | Đánh giá người có ảnh hưởng trước khi hợp tác |
Các trường hợp sử dụng chính:
Nghiên cứu đối thủ cạnh tranh. Cạo dữ liệu người theo dõi của 3–5 đối thủ cạnh tranh, loại bỏ các trùng lặp và xác định các điểm giao thoa. Những khách hàng tiềm năng chất lượng cao nhất thường là những người dùng theo dõi nhiều đối thủ cạnh tranh cùng một lúc.
Đánh giá người có ảnh hưởng/blogger. Trước khi trả tiền cho một sự hợp tác, hãy thu thập dữ liệu người theo dõi và đánh giá mức độ chân thực của đối tượng người xem.
Tiếp cận qua DM và email lạnh. Trong các thị trường ngách SaaS, marketing và thương mại điện tử, có 15–35% hồ sơ bao gồm địa chỉ email công khai trong phần giới thiệu (bio) của họ.
Đối tượng tương tự (Lookalike Audiences) trong Meta Ads. Tên người dùng có thể được chuyển đổi thành nhóm hạt giống Đối tượng tùy chỉnh để tạo Đối tượng tương tự.
Chuẩn bị tài khoản thông qua tương tác thích hàng loạt. Được sử dụng để tăng phạm vi tiếp cận và thiết lập một mô hình hoạt động tài khoản bình thường.
Tại sao các bot thông thường bị phát hiện vào năm 2026
Thay đổi lớn nhất trong những năm gần đây là Instagram hiện ẩn nội dung đối với những người dùng chưa xác thực. Trước đây, bạn có thể truy vấn các điểm cuối công khai như /?__a=1 mà không cần một phiên làm việc. Vào năm 2026, ngay cả những người theo dõi của một tài khoản công khai cũng không khả dụng trừ khi bạn đã đăng nhập.
Những gì bạn cần cho tập lệnh của mình:
1. Một tài khoản đã đăng nhập thực tế. Ưu tiên một tài khoản đã được chuẩn bị kỹ lưỡng thay vì một tài khoản vừa mới được tạo. Octo Browser giúp ích ở đây: bạn có thể tạo một tài khoản, đăng nhập thủ công, để nó hoạt động tự nhiên một thời gian, và chỉ sau đó mới tự động hóa nó.
2. Một tài khoản có nghĩa là một vân tay. Chạy 20 tài khoản Instagram từ một phiên bản Chrome duy nhất là cách chắc chắn nhất để bị khóa tài khoản. Với Octo, bạn có thể sử dụng các hồ sơ cô lập với các vân tay khác nhau (các tham số Canvas, độ phân giải màn hình, phông chữ, user-agent, v.v.).
3. Một proxy cho mỗi hồ sơ. Proxy di động hoặc proxy dân cư là lý tưởng nhất. Sử dụng các địa chỉ IP của trung tâm dữ liệu là vô cùng rủi ro.
Tuy nhiên, ngay cả với một vân tay trình duyệt chuẩn và một proxy đáng tin cậy, một bot Puppeteer vẫn sẽ không thể tồn tại nếu không có một mức độ mô phỏng hành vi của con người. Các hệ thống chống bot sẽ phân tích:
Mô hình tần suất. Có bao nhiêu lượt thích mỗi giờ, bao nhiêu lượt theo dõi mỗi ngày và khoảng thời gian giữa các hành động. Hệ thống phân tích phía máy chủ sẽ nhanh chóng gắn cờ một bot thích một bài viết cứ sau mỗi 30 giây.
Tạo vân tay trình duyệt. Đây là lý do tại sao việc sử dụng một trình duyệt chống phát hiện là vô cùng quan trọng.
Vân tay TLS/mạng. Đây là lý do tại sao các proxy chất lượng và một ngăn xếp TLS không tùy biến lại quan trọng.
Các lỗ hổng của khung tự động hóa (
navigator.webdriver, sự hiện diện của CDP, các dấu vết đặc trưng trongError.stack).
Bài học thực tế: vào năm 2026, những lý do chính khiến tài khoản bị khóa là các mô hình tần suất hành vi (quá nhiều hành động trong một khoảng thời gian ngắn, các khoảng thời gian quá đồng đều), vân tay trình duyệt và các proxy. Việc mô phỏng chuyển động chuột, cuộn trang hoặc gõ phím chưa hoàn toàn bắt buộc phải có. Tuy nhiên, trên thực tế, các tín hiệu này vẫn có thể được sử dụng để phân tích hành vi và tăng điểm gian lận của tài khoản.
Đó là lý do tại sao kiến trúc tập lệnh của chúng tôi bao gồm:
Octo Browser: vân tay bị cô lập, proxy và một hồ sơ với một phiên hoạt động thực tế trên Instagram.
Rebrowser-Puppeteer: một nhánh của Puppeteer không có các rò rỉ phổ biến, những thứ tương đối dễ bị phát hiện.
WindMouse: một mô hình chuyển động chuột dựa trên vật lý thực tế.
Một lớp hành vi: xem tin (story) ngẫu nhiên và thích bài viết trước khi cạo, phân phối tạm dừng log-normal, di chuyển quá mục tiêu (di chuyển lệch mục tiêu rồi điều chỉnh lại) và phản hồi đối với các điểm kiểm tra ứng dụng.
WindMouse: tại sao nó tốt hơn các đường cong Bézier bậc ba
Trong bài viết trước của chúng tôi, chúng tôi đã sử dụng các đường cong Bézier bậc ba để mô phỏng chuyển động chuột. Đây là một cách tiếp cận cơ bản, nhưng nó có những điểm yếu: chỉ với hai điểm kiểm soát, đường cong sẽ quá mượt mà và dễ đoán. Hình dạng luôn tương tự nhau, tốc độ được kiểm soát riêng biệt thông qua một tham số làm mượt, điều này tạo ra chuyển động quá đồng nhất, và các rung động nhỏ được thêm vào như một lớp riêng biệt, khiến nó lộ rõ rằng chuyển động bao gồm một đường cong mượt mà cộng với tiếng nhiễu nhân tạo.
Thay vào đó, WindMouse mô phỏng vật lý thực tế. Trọng lực kéo con trỏ về phía mục tiêu, trong khi gió tạo ra những sai lệch ngẫu nhiên tích tụ theo quán tính. Kết quả là, đường di chuyển sẽ mượt mà chứ không bị giật. Tốc độ bị giới hạn bởi kích thước bước tối đa và giảm dần một cách tự nhiên khi đến gần điểm đích, giống như con trỏ của một người thực sự khi nhắm vào một vật thể.
Kết quả thu được là một quỹ đạo với tốc độ thay đổi, sự giảm tốc tự nhiên và các vi chuyển động tinh tế không thể phân biệt được với hành vi thực của con người. Quan trọng nhất, do thành phần gió ngẫu nhiên, đường đi của con trỏ mỗi lần là hoàn toàn khác nhau.
Chúng tôi thêm hai hành vi giống con người hơn nữa:
Di chuyển quá mục tiêu (Overshoot): trong 30% trường hợp, con trỏ sẽ cố ý di chuyển lệch một chút so với nút bấm, sau đó mới di chuyển ngược lại nút đó.
Rung khi nhấp chuột (Click jitter): giữa
mouse.down()vàmouse.up(), con trỏ dịch chuyển 1–2 pixel theo hướng ngẫu nhiên.
Nền tảng của WindMouse trong tập lệnh là hàm wind_mouse_trajectory. Nó tạo ra một mảng các điểm quỹ đạo mà sau đó sẽ được sử dụng bởi page.mouse.move().
Tập lệnh có sẵn để cạo dữ liệu người theo dõi tài khoản mục tiêu và chuẩn bị tài khoản đồng thời
Giả sử bạn đã có một mảng các UUID của hồ sơ Octo đã đăng nhập vào các tài khoản Instagram. Tập lệnh của chúng tôi sẽ khởi chạy từng hồ sơ, chuẩn bị tài khoản bằng cách thích các bài viết trên bảng tin và xem Tin (Stories), sau đó tiến hành truy cập các hồ sơ Instagram được chỉ định làm mục tiêu cạo dữ liệu. Tại đó, nó sẽ thích các bài viết, theo dõi tài khoản và cuối cùng là cạo danh sách người theo dõi sang cả định dạng JSON và CSV. Tất cả những điều này được thực hiện với chuyển động chuột tự nhiên, thời gian trễ ngẫu nhiên và phát hiện điểm kiểm tra. Kết quả là, Instagram không thể phân biệt hoạt động tự động hóa của bạn với hoạt động thông thường của người dùng.
Chuẩn bị hồ sơ
Tạo một hoặc nhiều hồ sơ trong Octo và gán một proxy cho mỗi hồ sơ. Khuyến nghị sử dụng các proxy dân cư hoặc proxy di động.
Mở từng hồ sơ trong Octo bằng cách thủ công. Đăng nhập hoặc đăng ký một tài khoản Instagram. Trong vòng hai đến ba ngày, hãy sử dụng tài khoản như một người bình thường: thích các bài viết, xem Tin và theo dõi một số người dùng.
Sao chép các UUID của hồ sơ từ Octo. Bạn sẽ cần chúng khi điền cấu hình tập lệnh.
Bắt đầu
Tải xuống và cài đặt VS Code.
Tải xuống và cài đặt Node.js.
Tạo một thư mục ở bất kỳ đâu trên thiết bị của bạn và đặt tên cho nó, ví dụ:
octo_instagram_scraper.Mở thư mục đó trong VS Code.
Tạo một tệp .js. Tốt nhất là đặt tên tệp theo hành động mà tập lệnh thực hiện để tránh nhầm lẫn. Ví dụ:
octo_instagram_scraper.js.Dán mã tập lệnh vào tệp.
Điền thông tin cấu hình vào biến
config.
UUID — Các ID của hồ sơ Octo.
target_accounts — các tài khoản bạn muốn cạo dữ liệu.
followers_per_target — số lượng người theo dõi bạn muốn thu thập từ mỗi tài khoản mục tiêu trong một lần chạy duy nhất.
Các tham số còn lại trong config kiểm soát hành vi thực tế của công cụ cạo dữ liệu. Bạn có thể thử nghiệm với chúng hoặc giữ nguyên không đổi.

Mở terminal của bạn và chạy lệnh sau để cài đặt các phụ thuộc Node.js cần thiết:
npm i rebrowser-puppeteer axios

Nếu VS Code hiển thị lỗi, hãy mở Windows PowerShell với quyền Administrator, chạy lệnh sau và xác nhận thay đổi:
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSignedSau đó lặp lại bước trước đó.
Khởi chạy Octo Browser.
Chạy tập lệnh trong Visual Studio Code (
Ctrl/Cmd + F5) và đợi cho đến khi hoàn thành.
Công cụ cạo dữ liệu sẽ lần lượt khởi chạy các hồ sơ được chỉ định trong cấu hình của bạn. Sau đó, giống như một người dùng thông thường, nó sẽ xem Tin, cuộn bảng tin, thích các bài viết, theo dõi các tài khoản và cạo dữ liệu người theo dõi từ các tài khoản mục tiêu. Bạn có thể theo dõi quá trình này trong giao diện bảng điều khiển gỡ lỗi.

Tóm tắt kết quả phân tích. Trong ví dụ này, hai tài khoản mục tiêu đã được xử lý từ mỗi hồ sơ: 3 trong số 4 lần chạy đã hoàn thành thành công.
Hãy lưu ý rằng Instagram có thể giới hạn số lượng người theo dõi được trả về, tùy thuộc vào nhiều yếu tố khác nhau như mức độ chuẩn bị của tài khoản, số lượng kết nối chung, liệu tài khoản có theo dõi tài khoản mục tiêu hay không, và nhiều yếu tố khác. Nếu điều gì đó không hoạt động như mong đợi, hãy thử nghiệm thêm với một số hoạt động tương tác chuẩn bị hoặc chuyển đổi tài khoản và proxy.

Kết quả cạo dữ liệu ở định dạng CSV

Kết quả cạo dữ liệu ở định dạng JSON
Json sẽ thuận tiện hơn cho việc xử lý sâu hơn trong mã nguồn, trong khi CSV là lý tưởng để nhập vào Excel, Google Sheets, hệ thống CRM hoặc tải trực tiếp lên Meta Ads làm Đối tượng tùy chỉnh.
Mã tập lệnh
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 ratelimit, 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.'); })();
Mở rộng quy mô và an toàn tài khoản
Danh sách kiểm tra trước khi khởi chạy:
Các hồ sơ được chuẩn bị đúng cách. Tài khoản phải có tuổi thọ ít nhất 2–3 tuần, có ảnh đại diện, bài viết và một số hoạt động theo dõi.
Không sử dụng các proxy trung tâm dữ liệu. Hãy sử dụng proxy dân cư hoặc proxy di động.
Xác minh rằng các bộ chọn (selectors) đã được cập nhật. Trước khi chạy tập lệnh trên một số lượng lớn tài khoản, hãy kiểm tra thử trên một vài tài khoản để xác nhận rằng tất cả các bộ chọn hoạt động chính xác.
Thêm thời gian trễ giữa các lần chạy. Tập lệnh của chúng tôi xử lý các hồ sơ trong
config.profilesmột cách tuần tự, với thời gian tạm dừng 60-180 giây giữa các tài khoản đó. Nếu bạn sử dụng nhiều tài khoản hơn, hãy tăng khoảng thời gian này lên.
Kết luận
Chỉ riêng tập lệnh này đã đáp ứng hai trong số các trường hợp sử dụng phổ biến nhất: cạo dữ liệu người theo dõi (để nghiên cứu đối thủ cạnh tranh và xây dựng đối tượng tương tự) và thích hàng loạt (để chuẩn bị tài khoản). Nó có thể dễ dàng mở rộng thêm các tính năng bổ sung rộng hơn:
Cạo dữ liệu người dùng đã thích hoặc bình luận trên một bài viết cụ thể để có được phân khúc đối tượng tương tác tốt nhất.
Thu thập thông tin giới thiệu (bio) và địa chỉ email từ những người bình luận. Tính năng này có thể được chuyển sang một quy trình riêng biệt để tránh làm quá tải tài khoản cạo dữ liệu với các hành động bổ sung.
So sánh danh sách người theo dõi của nhiều đối thủ cạnh tranh và xác định các điểm trùng lặp. Việc này có thể triển khai dễ dàng bằng mã Node.js thuần túy từ dữ liệu JSON thu thập được.
Thêm một lớp lưu trữ tạm (cache) để tránh cạo dữ liệu lặp lại cùng một nhóm người theo dõi qua nhiều lần chạy.
Nếu bạn làm việc ở quy mô lớn, bạn cũng có thể điều chỉnh tập lệnh để thực thi song song.
Cạo dữ liệu thành công phụ thuộc vào một hệ thống nhiều lớp. Octo Browser cung cấp vân tay nhất quán và sự cô lập hồ sơ, rebrowser-puppeteer mang lại khả năng tự động hóa cùng các bản sửa lỗi liên quan đến rò rỉ phát hiện tự động hóa phổ biến, thuật toán WindMouse tạo ra chuyển động con trỏ thực tế và lớp hành vi thêm vào các hoạt động xao nhãng và thời gian tạm dừng. Cuối cùng, khả năng phục hồi của hệ thống không phụ thuộc vào bất kỳ thành phần đơn lẻ nào, mà phụ thuộc vào mức độ phối hợp hiệu quả của tất cả các thành phần đó hoạt động cùng nhau.
Cập nhật với các tin tức Octo Browser mới nhất
Khi nhấp vào nút này, bạn sẽ đồng ý với Chính sách Quyền riêng tư của chúng tôi.
Cập nhật với các tin tức Octo Browser mới nhất
Khi nhấp vào nút này, bạn sẽ đồng ý với Chính sách Quyền riêng tư của chúng tôi.
Cập nhật với các tin tức Octo Browser mới nhất
Khi nhấp vào nút này, bạn sẽ đồng ý với Chính sách Quyền riêng tư của chúng tôi.

Tham gia Octo Browser ngay
Hoặc liên hệ với Dịch vụ khách hàng bất kì lúc nào nếu bạn có bất cứ thắc mắc nào.

Tham gia Octo Browser ngay
Hoặc liên hệ với Dịch vụ khách hàng bất kì lúc nào nếu bạn có bất cứ thắc mắc nào.
Tham gia Octo Browser ngay
Hoặc liên hệ với Dịch vụ khách hàng bất kì lúc nào nếu bạn có bất cứ thắc mắc nào.
