Export Instagram posts with images, captions, and metadata to JSON or CSV format
Size
13.7 KB
Version
1.0.1
Created
Mar 21, 2026
Updated
25 days ago
1// ==UserScript==
2// @name Instagram Post Exporter
3// @description Export Instagram posts with images, captions, and metadata to JSON or CSV format
4// @version 1.0.1
5// @match https://*.instagram.com/*
6// @icon https://static.cdninstagram.com/rsrc.php/y4/r/QaBlI0OZiks.ico
7// @grant GM.xmlhttpRequest
8// @grant GM.setValue
9// @grant GM.getValue
10// ==/UserScript==
11(function() {
12 'use strict';
13
14 console.log('Instagram Post Exporter loaded');
15
16 // Utility function to wait for element
17 function waitForElement(selector, timeout = 10000) {
18 return new Promise((resolve, reject) => {
19 if (document.querySelector(selector)) {
20 return resolve(document.querySelector(selector));
21 }
22
23 const observer = new MutationObserver(() => {
24 if (document.querySelector(selector)) {
25 observer.disconnect();
26 resolve(document.querySelector(selector));
27 }
28 });
29
30 observer.observe(document.body, {
31 childList: true,
32 subtree: true
33 });
34
35 setTimeout(() => {
36 observer.disconnect();
37 reject(new Error('Element not found: ' + selector));
38 }, timeout);
39 });
40 }
41
42 // Debounce function
43 function debounce(func, wait) {
44 let timeout;
45 return function executedFunction(...args) {
46 const later = () => {
47 clearTimeout(timeout);
48 func(...args);
49 };
50 clearTimeout(timeout);
51 timeout = setTimeout(later, wait);
52 };
53 }
54
55 // Extract post data from a post URL
56 async function extractPostData(postUrl) {
57 try {
58 console.log('Extracting data from:', postUrl);
59
60 const response = await GM.xmlhttpRequest({
61 method: 'GET',
62 url: postUrl,
63 headers: {
64 'User-Agent': navigator.userAgent
65 }
66 });
67
68 const parser = new DOMParser();
69 const doc = parser.parseFromString(response.responseText, 'text/html');
70
71 // Extract data from meta tags and page content
72 const postData = {
73 url: postUrl,
74 timestamp: new Date().toISOString(),
75 caption: '',
76 images: [],
77 likes: 0,
78 comments: 0,
79 date: '',
80 author: ''
81 };
82
83 // Get caption from meta description
84 const metaDescription = doc.querySelector('meta[property="og:description"]');
85 if (metaDescription) {
86 postData.caption = metaDescription.content;
87 }
88
89 // Get image from meta tag
90 const metaImage = doc.querySelector('meta[property="og:image"]');
91 if (metaImage) {
92 postData.images.push(metaImage.content);
93 }
94
95 // Get author from meta tag
96 const metaTitle = doc.querySelector('meta[property="og:title"]');
97 if (metaTitle) {
98 postData.author = metaTitle.content.split(' on Instagram')[0];
99 }
100
101 // Try to extract additional images from the page
102 const imgElements = doc.querySelectorAll('img[src*="instagram"]');
103 imgElements.forEach(img => {
104 const src = img.src || img.getAttribute('src');
105 if (src && src.includes('scontent') && !postData.images.includes(src)) {
106 postData.images.push(src);
107 }
108 });
109
110 return postData;
111 } catch (error) {
112 console.error('Error extracting post data:', error);
113 return null;
114 }
115 }
116
117 // Get all post links from current page
118 function getPostLinks() {
119 const links = new Set();
120 const postLinks = document.querySelectorAll('a[href*="/p/"], a[href*="/reel/"]');
121
122 postLinks.forEach(link => {
123 const href = link.href;
124 if (href && (href.includes('/p/') || href.includes('/reel/'))) {
125 links.add(href);
126 }
127 });
128
129 return Array.from(links);
130 }
131
132 // Export to JSON
133 function exportToJSON(data) {
134 const jsonStr = JSON.stringify(data, null, 2);
135 const blob = new Blob([jsonStr], { type: 'application/json' });
136 const url = URL.createObjectURL(blob);
137 const a = document.createElement('a');
138 a.href = url;
139 a.download = `instagram_posts_${Date.now()}.json`;
140 document.body.appendChild(a);
141 a.click();
142 document.body.removeChild(a);
143 URL.revokeObjectURL(url);
144 }
145
146 // Export to CSV
147 function exportToCSV(data) {
148 const headers = ['URL', 'Author', 'Caption', 'Images', 'Date', 'Timestamp'];
149 const rows = data.map(post => [
150 post.url,
151 post.author,
152 post.caption.replace(/"/g, '""'),
153 post.images.join(' | '),
154 post.date,
155 post.timestamp
156 ]);
157
158 const csvContent = [
159 headers.join(','),
160 ...rows.map(row => row.map(cell => `"${cell}"`).join(','))
161 ].join('\n');
162
163 const blob = new Blob([csvContent], { type: 'text/csv' });
164 const url = URL.createObjectURL(blob);
165 const a = document.createElement('a');
166 a.href = url;
167 a.download = `instagram_posts_${Date.now()}.csv`;
168 document.body.appendChild(a);
169 a.click();
170 document.body.removeChild(a);
171 URL.revokeObjectURL(url);
172 }
173
174 // Create export UI
175 function createExportUI() {
176 // Check if button already exists
177 if (document.getElementById('ig-export-btn')) {
178 return;
179 }
180
181 // Create export button container
182 const buttonContainer = document.createElement('div');
183 buttonContainer.id = 'ig-export-btn';
184 buttonContainer.style.cssText = `
185 position: fixed;
186 bottom: 20px;
187 right: 20px;
188 z-index: 9999;
189 display: flex;
190 flex-direction: column;
191 gap: 10px;
192 `;
193
194 // Create main export button
195 const exportBtn = document.createElement('button');
196 exportBtn.textContent = '📥 Export Posts';
197 exportBtn.style.cssText = `
198 background: linear-gradient(45deg, #f09433 0%, #e6683c 25%, #dc2743 50%, #cc2366 75%, #bc1888 100%);
199 color: white;
200 border: none;
201 padding: 12px 24px;
202 border-radius: 8px;
203 font-size: 14px;
204 font-weight: 600;
205 cursor: pointer;
206 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
207 transition: transform 0.2s, box-shadow 0.2s;
208 `;
209
210 exportBtn.addEventListener('mouseenter', () => {
211 exportBtn.style.transform = 'translateY(-2px)';
212 exportBtn.style.boxShadow = '0 6px 16px rgba(0, 0, 0, 0.2)';
213 });
214
215 exportBtn.addEventListener('mouseleave', () => {
216 exportBtn.style.transform = 'translateY(0)';
217 exportBtn.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.15)';
218 });
219
220 exportBtn.addEventListener('click', showExportModal);
221
222 buttonContainer.appendChild(exportBtn);
223 document.body.appendChild(buttonContainer);
224
225 console.log('Export button created');
226 }
227
228 // Show export modal
229 function showExportModal() {
230 // Create modal overlay
231 const modal = document.createElement('div');
232 modal.id = 'ig-export-modal';
233 modal.style.cssText = `
234 position: fixed;
235 top: 0;
236 left: 0;
237 width: 100%;
238 height: 100%;
239 background: rgba(0, 0, 0, 0.7);
240 z-index: 10000;
241 display: flex;
242 align-items: center;
243 justify-content: center;
244 `;
245
246 // Create modal content
247 const modalContent = document.createElement('div');
248 modalContent.style.cssText = `
249 background: white;
250 border-radius: 12px;
251 padding: 24px;
252 max-width: 500px;
253 width: 90%;
254 box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
255 `;
256
257 const postLinks = getPostLinks();
258
259 modalContent.innerHTML = `
260 <h2 style="margin: 0 0 16px 0; font-size: 24px; color: #262626;">Export Instagram Posts</h2>
261 <p style="margin: 0 0 20px 0; color: #8e8e8e; font-size: 14px;">Found ${postLinks.length} posts on this page</p>
262
263 <div style="margin-bottom: 20px;">
264 <label style="display: block; margin-bottom: 8px; color: #262626; font-weight: 600;">Export Format:</label>
265 <select id="export-format" style="width: 100%; padding: 10px; border: 1px solid #dbdbdb; border-radius: 6px; font-size: 14px;">
266 <option value="json">JSON</option>
267 <option value="csv">CSV</option>
268 </select>
269 </div>
270
271 <div style="margin-bottom: 20px;">
272 <label style="display: block; margin-bottom: 8px; color: #262626; font-weight: 600;">Number of Posts:</label>
273 <input type="number" id="post-count" value="${Math.min(postLinks.length, 10)}" min="1" max="${postLinks.length}"
274 style="width: 100%; padding: 10px; border: 1px solid #dbdbdb; border-radius: 6px; font-size: 14px;">
275 </div>
276
277 <div id="export-progress" style="display: none; margin-bottom: 20px;">
278 <div style="background: #efefef; border-radius: 4px; height: 8px; overflow: hidden;">
279 <div id="progress-bar" style="background: linear-gradient(45deg, #f09433 0%, #e6683c 25%, #dc2743 50%, #cc2366 75%, #bc1888 100%); height: 100%; width: 0%; transition: width 0.3s;"></div>
280 </div>
281 <p id="progress-text" style="margin: 8px 0 0 0; color: #8e8e8e; font-size: 12px; text-align: center;">Processing...</p>
282 </div>
283
284 <div style="display: flex; gap: 12px; justify-content: flex-end;">
285 <button id="cancel-btn" style="padding: 10px 20px; border: 1px solid #dbdbdb; background: white; color: #262626; border-radius: 6px; font-size: 14px; font-weight: 600; cursor: pointer;">Cancel</button>
286 <button id="export-btn" style="padding: 10px 20px; border: none; background: linear-gradient(45deg, #f09433 0%, #e6683c 25%, #dc2743 50%, #cc2366 75%, #bc1888 100%); color: white; border-radius: 6px; font-size: 14px; font-weight: 600; cursor: pointer;">Export</button>
287 </div>
288 `;
289
290 modal.appendChild(modalContent);
291 document.body.appendChild(modal);
292
293 // Event listeners
294 document.getElementById('cancel-btn').addEventListener('click', () => {
295 modal.remove();
296 });
297
298 modal.addEventListener('click', (e) => {
299 if (e.target === modal) {
300 modal.remove();
301 }
302 });
303
304 document.getElementById('export-btn').addEventListener('click', async () => {
305 const format = document.getElementById('export-format').value;
306 const count = parseInt(document.getElementById('post-count').value);
307 const exportBtn = document.getElementById('export-btn');
308 const cancelBtn = document.getElementById('cancel-btn');
309 const progressDiv = document.getElementById('export-progress');
310 const progressBar = document.getElementById('progress-bar');
311 const progressText = document.getElementById('progress-text');
312
313 exportBtn.disabled = true;
314 cancelBtn.disabled = true;
315 progressDiv.style.display = 'block';
316
317 const postsToExport = postLinks.slice(0, count);
318 const exportedData = [];
319
320 for (let i = 0; i < postsToExport.length; i++) {
321 const postUrl = postsToExport[i];
322 progressText.textContent = `Processing post ${i + 1} of ${postsToExport.length}...`;
323 progressBar.style.width = `${((i + 1) / postsToExport.length) * 100}%`;
324
325 const postData = await extractPostData(postUrl);
326 if (postData) {
327 exportedData.push(postData);
328 }
329
330 // Small delay to avoid rate limiting
331 await new Promise(resolve => setTimeout(resolve, 500));
332 }
333
334 progressText.textContent = 'Generating file...';
335
336 if (format === 'json') {
337 exportToJSON(exportedData);
338 } else {
339 exportToCSV(exportedData);
340 }
341
342 progressText.textContent = `Successfully exported ${exportedData.length} posts!`;
343 progressBar.style.width = '100%';
344
345 setTimeout(() => {
346 modal.remove();
347 }, 2000);
348 });
349 }
350
351 // Initialize
352 async function init() {
353 console.log('Initializing Instagram Post Exporter');
354
355 // Wait for page to load
356 await waitForElement('header, main').catch(() => {
357 console.log('Timeout waiting for Instagram to load, proceeding anyway');
358 });
359
360 // Create export button
361 createExportUI();
362
363 // Re-create button on navigation (Instagram is SPA)
364 const observer = new MutationObserver(debounce(() => {
365 if (!document.getElementById('ig-export-btn')) {
366 createExportUI();
367 }
368 }, 1000));
369
370 observer.observe(document.body, {
371 childList: true,
372 subtree: true
373 });
374
375 console.log('Instagram Post Exporter initialized');
376 }
377
378 // Start the extension
379 if (document.readyState === 'loading') {
380 document.addEventListener('DOMContentLoaded', init);
381 } else {
382 init();
383 }
384})();