instagrab
A Rust CLI that scrapes Instagram profile metadata (full name, biography, follower / following / post counts, recent posts with captions) by attaching to a real Chrome process over the Chrome DevTools Protocol.
Designed to run on a server you own under cron, alongside a long-lived headless Chrome whose user-data-dir holds an authenticated IG session.
It can scan a fixed list of profiles, or fan out over everyone a seed profile follows (re-fetched on demand), writing one JSONL record + the first image per post into a directory.
Instagram's ToS prohibits automated scraping. Use at your own account risk. The defaults (daily cadence, jittered inter-profile sleeps, single account) are conservative for personal-scale use.
Getting started
You'll need a recent Rust toolchain and Chrome.
# 1. Launch Chrome with a dedicated dev profile and sign in to IG manually:
# 2. Build and write a sample config, then edit it
# (at minimum set output_path and seed_username):
# 3. Fetch your follows list (writes <output_path>/friends.txt):
# 4. Scan everyone on that list (writes <output_path>/runs.jsonl + images):
|
instagrab does two things:
--fetch-followspages your seed profile's Following into a follows file (run it rarely).- The default run scans everyone in that file for recent posts + first image (run it daily).
For more information, see Friends fan-out and Configuration.
Output shape
A successful run emits one JSON object per username:
source is "graphql" (the only successful kind), "not_found",
"logged_out", or "error". Per-field problems land in the errors array.
window_maybe_truncated: true means paging stopped before reaching the
oldest post in the window; bump max_scrolls if you need to go further.
A post may also carry media_count, is_carousel, and has_video (present
only when it's a carousel or a video). Due to rate limiting, only the first image is downloaded;
one can link out to Instagram for the rest.
Friends fan-out
instagrab's main mode scans everyone a seed profile follows, re-fetching that list on demand.
instagrab --fetch-follows pages seed_username's Following once, at a human
pace, and writes the handles to the follows file (<output_path>/friends.txt).
This is the only path that touches Instagram's friendship endpoint (isolated in
src/follows.rs); it is off by default and detection-sensitive, so run it
rarely. It prints progress (fetching follows... N so far) and a final count.
--seed <name> overrides seed_username ad hoc.
Commands
| Command | What it does |
|---|---|
instagrab |
Scan everyone in the follows file (the daily job). |
instagrab --fetch-follows [--seed <name>] |
Rebuild the follows file from the seed's Following (run rarely). |
instagrab --once <name> |
Scan a single profile, ignoring the follows list. |
instagrab --limit <n> |
Scan at most N profiles from the follows list (order stalest-first upstream to scan the oldest N). |
instagrab --dry-run |
Connect to Chrome and exit — a connection test. |
instagrab --write-sample-config <path> |
Write a starter config and exit. |
instagrab --debug |
Add per-profile fetch/DOM debug output on stderr. |
instagrab --config <path> |
Use a specific config (default /etc/instagrab/config.toml). |
Configuration
All settings live in the TOML file passed with --config (generate a starter
with --write-sample-config). output_path (an output directory) is
required; everything else has a default.
| Key | Default | Meaning |
|---|---|---|
browser_url |
http://127.0.0.1:9222 |
CDP endpoint of the long-lived Chrome instagrab attaches to. |
output_path |
— (required) | The one output directory. Holds runs.jsonl, images/, and friends.txt. |
seed_username |
— | Profile whose Following --fetch-follows pages (or pass --seed). |
time_window_days |
0 (no filter) |
Keep only posts within the last N days; > 0 also drives feed pagination. |
jitter_min_secs |
30 |
Minimum inter-profile sleep, seconds (anti-detection). |
jitter_max_secs |
75 |
Maximum inter-profile sleep, seconds. |
nav_timeout_sec |
20 |
Per-profile navigation/wait budget, seconds. |
max_scrolls |
8 |
Safety cap on feed pages fetched per profile. |
usernames |
[] |
Fixed scrape list, used only as a fallback when the follows file is empty. |
The canary scrape (against instagram.com/instagram) has no knob — it always
runs first and exits 5 if extraction looks broken.
Alerts
Out-of-band conditions surface as event: "alert" lines and a non-zero exit:
| Kind | Meaning | Exit |
|---|---|---|
logged_out |
IG demanded login or redirected to /accounts/login |
2 |
schema_drift |
web_profile_info parsed but expected fields are missing on every profile this run |
3 |
logged_out means: re-run the SSH-tunnel login bootstrap (see deploy/README.md).
schema_drift means: IG renamed/removed a JSON path; update
EXPECTED_PROFILE_PATHS and the parser in src/parse.rs.
Deploying
See deploy/README.md for a full deployment walkthrough,
including the SSH-tunnel login bootstrap that mints cookies
(so IG's first-login flag fires once, during setup, not during cron).
How it works
- A persistent
--headless=newChrome runs on the host with--remote-debugging-port=9222 --user-data-dir=…/CDPProfile. You log in to Instagram once into that profile. instagrabconnects to that Chrome via CDP, navigates each profile in its own tab, then actively fetches/api/v1/users/web_profile_info/from inside the page (with theX-IG-App-IDheader IG's web app sends). Passively listening for that endpoint isn't reliable — the logged-in React shell doesn't always trigger it.- Posts come from the mobile-API user-feed endpoint
(
/api/v1/feed/user/<id>/), fetched from inside the page and paged viamax_id. Iftime_window_days > 0, instagrab keeps paging until the oldest captured post is older than the window. - Each kept post's
display_urlis downloaded over plain HTTPS to<output_path>/images/<username>/<shortcode>.jpg(idempotent — existing files are kept). - One JSONL line per username is appended to
<output_path>/runs.jsonl.