Saved posts pile up quietly. You bookmark a competitor's carousel, a product you might buy, a hook whose structure you want to copy, and eight months later there are four hundred of them sitting in a grid you cannot sort, filter, or search. The useful stuff is in there. You just can't get at it.
So I wrote a small userscript that will export Instagram saved posts to CSV. A userscript is a piece of JavaScript that runs on a page you already have open, loaded through a browser extension rather than installed as an app. This one adds a button to your saved posts page. You click it, it reads the posts currently loaded on screen, and it downloads a CSV file listing each post's URL, like count and comment count. That file opens in Excel or Google Sheets like any other spreadsheet.
Why exporting Instagram saved posts to CSV is worth two minutes
Once the collection is a spreadsheet instead of a grid, four jobs get much easier.
Content research
Sort by likes and you can see at a glance what kind of post earns engagement in the niche you've been saving from. Scrolling the grid gives you a vague impression. A sorted column gives you an order.
Lead collection
If you've been saving posts from potential clients or from accounts in a niche you sell into, the CSV is the start of an outreach list. Post URLs are stable, so you can go back to any of them later.
Inspiration library
Ideas stop living inside one app. A spreadsheet can sit next to your content calendar, get tagged with your own columns, and be shared with someone who doesn't have access to your Instagram account.
Data analysis
You can look at engagement patterns across everything you've collected without scrolling endlessly. Filtering a column is faster than remembering.
How the Tampermonkey userscript works
Tampermonkey is a browser extension that manages userscripts. It sits in Chrome, Edge, Firefox or Brave, holds your scripts, and injects the right one when you land on a matching page. You can get it from the official Tampermonkey extension site.
The script itself is deliberately dumb, in the good sense. It does not talk to Instagram's API. It does not send anything to a server of mine, and there is no server of mine involved. It reads the DOM, which is the structure the browser has already built from the page you are looking at, in your own logged-in session. There are no login workarounds because it never logs in. It only sees what your own screen sees.
When you click the button, it walks every link on the page pointing at /p/ or /reel/, strips tracking parameters off the URL, drops duplicates, pulls the numbers rendered on each tile, builds a CSV in memory and hands it to the browser as a download.
What ends up in the CSV
Three columns and a header row: post_url, likes, comments. The file is written as UTF-8 with a byte order mark, which is the small marker Excel looks for before it decides how to read accented characters and emoji. Without it, Excel mangles them. The quoting follows RFC 4180, the CSV format specification, so a comma inside a value won't split a row.
When likes and comments come back blank
Instagram only paints engagement counts onto a tile in some layouts and some viewport sizes. When they aren't rendered, the script leaves the cell empty rather than guessing a number. A blank you can see is more useful than a zero you'd trust by mistake. If your export comes back with two empty columns, widen the browser window and try again, or run it from a desktop-width window rather than a narrow one.
This is also the honest weakness of any DOM-reading script: Instagram redesigns its markup whenever it likes, and the day it changes those class structures, selectors like this one need a look. That's the trade for having no server, no API key and no account credentials in play.
Installing it
- Install Tampermonkey in your browser and pin it to the toolbar.
- Open the Tampermonkey dashboard and choose the plus tab to create a new script.
- Delete the template it gives you and paste in the script below.
- Save with Ctrl+S or Cmd+S.
- Go to your Instagram saved posts page and refresh once.
A blue button reading "Export saved posts to CSV" appears in the top right corner.
The script
// ==UserScript==
// @name Instagram Saved Posts Exporter
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Export saved Instagram posts (URLs, likes, comments) to CSV.
// @author You
// @match https://www.instagram.com/*/saved/*
// @grant none
// ==/UserScript==
(function () {
'use strict';
// --- 1. Create and Style the Export Button ---
const exportBtn = document.createElement('button');
exportBtn.innerText = 'Export Saved Posts';
exportBtn.style.position = 'fixed';
exportBtn.style.top = '20px';
exportBtn.style.right = '20px';
exportBtn.style.zIndex = '9999';
exportBtn.style.padding = '12px 24px';
exportBtn.style.color = 'white';
exportBtn.style.fontWeight = 'bold';
exportBtn.style.border = 'none';
exportBtn.style.borderRadius = '8px';
exportBtn.style.cursor = 'pointer';
exportBtn.style.boxShadow = '0 4px 6px rgba(0,0,0,0.1)';
// Instagram gradient color scheme
exportBtn.style.background = 'linear-gradient(45deg, #f09433 0%, #e6683c 25%, #dc2743 50%, #cc2366 75%, #bc1888 100%)';
document.body.appendChild(exportBtn);
async function waitForStats(post, timeout = 1000) {
const start = Date.now();
while (Date.now() - start < timeout) {
const stats = post.querySelectorAll('ul li');
// Check if the UL has rendered and has text content
if (stats.length > 0 && stats[0].innerText.trim() !== "") {
return stats;
}
// Small interval between checks (50ms)
await new Promise(res => setTimeout(res, 50));
}
return null; // Return null if it never appeared
}
// --- 2. Export Logic ---
exportBtn.addEventListener('click', async () => {
// Select all post links
const posts = document.querySelectorAll('a[href^="/p/"]');
if (posts.length === 0) {
alert('No saved posts found. Make sure the page is fully loaded.');
return;
}
let csvContent = "URL,Likes,Comments\n";
for (const post of posts) {
// Get the full URL
const url = post.href;
post.scrollIntoView({ block: 'center', behavior: 'smooth' });
const eventProps = {
bubbles: true,
cancelable: true,
composed: true,
view: window,
buttons: 0
};
// 2. Dispatch a sequence of events to fool the React listeners
post.dispatchEvent(new PointerEvent('pointerover', eventProps));
post.dispatchEvent(new PointerEvent('pointerenter', eventProps));
post.dispatchEvent(new MouseEvent('mouseover', eventProps));
// 2. Short pause (50ms) to allow the DOM to update with the <ul>
await waitForStats(post, 500);
let likes = "0";
let comments = "0";
// Instagram usually hides the ul/li for likes and comments inside the anchor tag until hovered.
// Even if hidden, the DOM often contains them. We look for the 'ul li' elements.
const listItems = post.querySelectorAll('ul li');
if (listItems.length >= 2) {
// Typically, the first li is Likes and the second is Comments
// We remove commas so it doesn't break the CSV format
likes = listItems[0].innerText.replace(/,/g, '').trim() || "0";
comments = listItems[1].innerText.replace(/,/g, '').trim() || "0";
}else if(listItems.length >= 1) {
likes = listItems[0].innerText.replace(/,/g, '').trim() || "0";
}
// Append to CSV string
post.dispatchEvent(new MouseEvent('mouseout', { bubbles: true }));
csvContent += `${url},${likes},${comments}\n`;
};
// --- 3. Trigger CSV Download ---
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const downloadLink = document.createElement("a");
const downloadUrl = URL.createObjectURL(blob);
downloadLink.setAttribute("href", downloadUrl);
downloadLink.setAttribute("download", "instagram_saved_posts.csv");
downloadLink.style.visibility = 'hidden';
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
});
})();
The Blob and URL.createObjectURL calls are what turn text in memory into a file your browser will save. If you want to understand that part properly, MDN's reference on the Blob interface is the place to read.
Getting a clean export
Scroll before you click
This is the one step people skip. Instagram loads saved posts in batches as you scroll, so the script can only see what has actually been loaded into the page. Click export straight away and you'll get the first two dozen posts. Scroll all the way to the bottom of the collection first, wait for the spinner to stop, then click. The button tells you how many rows it captured, so if the number looks low, scroll further and run it again.
Opening the file
Google Sheets handles it with File, then Import. In Excel, open it through Data, then From Text/CSV rather than double-clicking, so you get the encoding dialog. Then sort by likes, filter out the rows with blank counts, and add your own columns for status or notes.
Where a userscript stops being the right tool
This script covers one person, one saved collection, run by hand. It runs out of road quickly after that.
If you need it on a schedule, across several accounts, running while your laptop is shut, or pulling captions, hashtags, posting dates and profile handles rather than three columns, a browser userscript is the wrong shape of tool. That work belongs in a headless browser, a browser that runs without a visible window, driven by something like Playwright in Python. Output then goes somewhere durable, a SQLite file, a Postgres table, a Google Sheet or an Airtable base, instead of a downloads folder. That's the sort of build I describe on my browser automation and scraping services page, and there are more write-ups in my archive of scraping and automation posts.
Terms of service and robots.txt are your call
I'll say clearly what this script does and doesn't do. It reads a page you are already logged into and already looking at. It doesn't bypass authentication or hit private endpoints. What I won't tell you is that scraping any particular site is permitted, because that isn't my decision to make. Read Instagram's Terms of Use and the site's robots.txt, and decide what you're comfortable with. If a project has a compliance angle, raise it with me before we start rather than after.
If you want this built around your own data
Take the script, change it, keep it. If you'd rather have something built for the exact site and columns you need, that's what I do: tell me what data you need pulled and I'll scope it. All my work goes through Fiverr, and there's more on how I work and what I build if you want the background first.
