-
-
Notifications
You must be signed in to change notification settings - Fork 86
Expand file tree
/
Copy pathscrapeAnalytics.js
More file actions
144 lines (116 loc) · 5.35 KB
/
Copy pathscrapeAnalytics.js
File metadata and controls
144 lines (116 loc) · 5.35 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
// Copyright (c) 2024-2026 nich (@nichxbt). Licensed under the Apache License, Version 2.0.
// scripts/scrapeAnalytics.js
// Browser console script to scrape X/Twitter analytics data
// Paste in DevTools console on x.com/i/account_analytics or any post
// by nichxbt
(() => {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const CONFIG = {
mode: 'account', // 'account' or 'post'
// For post mode, navigate to a specific tweet first
scrapeRecentPosts: true,
maxPosts: 20,
};
const scrapeAccountAnalytics = async () => {
console.log('📊 Scraping account analytics...');
// Try to extract analytics from the analytics page
const metrics = {};
// Generic metric extraction
document.querySelectorAll('[role="listitem"], [data-testid*="stat"], [data-testid*="metric"]').forEach(el => {
const text = el.textContent?.trim();
if (text && text.length < 200) {
const parts = text.split('\n').filter(Boolean);
if (parts.length >= 2) {
metrics[parts[0].trim()] = parts[1].trim();
}
}
});
// Try specific selectors
const specificMetrics = {
impressions: document.querySelector('[data-testid="impressions"]')?.textContent,
engagements: document.querySelector('[data-testid="engagements"]')?.textContent,
followers: document.querySelector('a[href$="/followers"] span')?.textContent,
following: document.querySelector('a[href$="/following"] span')?.textContent,
};
Object.entries(specificMetrics).forEach(([k, v]) => {
if (v) metrics[k] = v.trim();
});
return metrics;
};
const scrapePostAnalytics = async () => {
console.log('📈 Scraping post analytics...');
const tweet = document.querySelector('article[data-testid="tweet"]');
if (!tweet) return { error: 'No tweet found on this page' };
return {
text: tweet.querySelector('[data-testid="tweetText"]')?.textContent || '',
likes: tweet.querySelector('[data-testid="like"] span, [data-testid="unlike"] span')?.textContent || '0',
reposts: tweet.querySelector('[data-testid="retweet"] span')?.textContent || '0',
replies: tweet.querySelector('[data-testid="reply"] span')?.textContent || '0',
views: tweet.querySelector('[data-testid="analyticsButton"] span')?.textContent || '0',
bookmarks: tweet.querySelector('[data-testid="bookmark"] span')?.textContent || '0',
time: tweet.querySelector('time')?.getAttribute('datetime') || '',
};
};
const scrapeRecentPosts = async () => {
console.log('📝 Scraping recent posts analytics...');
const posts = [];
let scrollAttempts = 0;
while (posts.length < CONFIG.maxPosts && scrollAttempts < CONFIG.maxPosts) {
document.querySelectorAll('article[data-testid="tweet"]').forEach(tweet => {
const link = tweet.querySelector('a[href*="/status/"]')?.href || '';
if (!link || posts.find(p => p.link === link)) return;
posts.push({
text: (tweet.querySelector('[data-testid="tweetText"]')?.textContent || '').substring(0, 200),
likes: tweet.querySelector('[data-testid="like"] span, [data-testid="unlike"] span')?.textContent || '0',
reposts: tweet.querySelector('[data-testid="retweet"] span')?.textContent || '0',
replies: tweet.querySelector('[data-testid="reply"] span')?.textContent || '0',
views: tweet.querySelector('[data-testid="analyticsButton"] span')?.textContent || '',
time: tweet.querySelector('time')?.getAttribute('datetime') || '',
link,
});
});
window.scrollBy(0, 800);
await sleep(1500);
scrollAttempts++;
}
return posts.slice(0, CONFIG.maxPosts);
};
const run = async () => {
console.log('📊 XActions Analytics Scraper');
console.log('============================');
let result = {};
if (CONFIG.mode === 'post') {
result.postAnalytics = await scrapePostAnalytics();
} else {
result.accountMetrics = await scrapeAccountAnalytics();
}
if (CONFIG.scrapeRecentPosts) {
result.recentPosts = await scrapeRecentPosts();
// Calculate engagement summary
const totalLikes = result.recentPosts.reduce((sum, p) => sum + parseInt(p.likes.replace(/[,K]/g, '')) || 0, 0);
const totalReposts = result.recentPosts.reduce((sum, p) => sum + parseInt(p.reposts.replace(/[,K]/g, '')) || 0, 0);
result.summary = {
totalPosts: result.recentPosts.length,
totalLikes,
totalReposts,
avgLikes: Math.round(totalLikes / result.recentPosts.length),
avgReposts: Math.round(totalReposts / result.recentPosts.length),
topPost: result.recentPosts.sort((a, b) =>
(parseInt(b.likes.replace(/[,K]/g, '')) || 0) - (parseInt(a.likes.replace(/[,K]/g, '')) || 0)
)[0],
};
console.log(`\n📈 Summary (${result.recentPosts.length} posts):`);
console.log(` ❤️ Total likes: ${totalLikes}`);
console.log(` 🔁 Total reposts: ${totalReposts}`);
console.log(` 📊 Avg likes/post: ${result.summary.avgLikes}`);
}
result.scrapedAt = new Date().toISOString();
console.log('\n📦 Full JSON:');
console.log(JSON.stringify(result, null, 2));
try {
await navigator.clipboard.writeText(JSON.stringify(result, null, 2));
console.log('\n✅ Copied to clipboard!');
} catch (e) {}
};
run();
})();