Back to Home
Gistify Snippet
kyiov
1h ago·3 file(s)
Public

Grupku.net Scraper — WhatsApp & Telegram Group Harvester

Zero-dependency Node.js crawler for grupku.net directory. Fetches 200+ public WhatsApp & Telegram invite links via concurrent async pool and exports to JSON & CSV.

#scraper#nodejs#whatsapp#telegram#crawler#javascript
4
Raw
grupku-scraper.jsjavascript
257 lines
1/**
2 * Grupku.net Web Scraper in Node.js
3 * Ekstraksi daftar grup WhatsApp & Telegram, URL undangan, kategori, dan deskripsi.
4 */
5
6const fs = require("fs");
7const path = require("path");
8
9const BASE_URL = "https://www.grupku.net/";
10const SITEMAP_URL = "https://www.grupku.net/sitemap.xml";
11
12// Helper HTML Entity Unescape
13function unescapeHtml(text) {
14 if (!text) return "";
15 return text
16 .replace(/&/g, "&")
17 .replace(/&lt;/g, "<")
18 .replace(/&gt;/g, ">")
19 .replace(/&quot;/g, '"')
20 .replace(/&#039;/g, "'")
21 .replace(/&#8226;/g, "•")
22 .replace(/&#9961;/g, "⛑")
23 .replace(/&#65039;/g, "")
24 .replace(/&#185;/g, "¹")
25 .replace(/&#8902;/g, "⋆")
26 .replace(/&#8881;/g, "⊵")
27 .replace(/&#9733;/g, "★")
28 .replace(/&#9829;/g, "♥")
29 .replace(/&#([0-9]{1,6});/gi, (_, num) => String.fromCharCode(parseInt(num, 10)));
30}
31
32function cleanText(text) {
33 if (!text) return "";
34 const noTags = text.replace(/<[^>]+>/g, "");
35 return unescapeHtml(noTags).replace(/\s+/g, " ").trim();
36}
37
38async function fetchWithRetry(url, retries = 2) {
39 for (let i = 0; i <= retries; i++) {
40 try {
41 const res = await fetch(url, {
42 headers: {
43 "User-Agent":
44 "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
45 },
46 signal: AbortSignal.timeout(12000),
47 });
48 if (res.ok) {
49 return await res.text();
50 }
51 } catch (err) {
52 if (i === retries) throw err;
53 await new Promise((r) => setTimeout(r, 600));
54 }
55 }
56 return "";
57}
58
59async function getAllGroupUrls() {
60 const urls = new Set();
61
62 // 1. Ambil dari sitemap.xml
63 try {
64 const sitemapXml = await fetchWithRetry(SITEMAP_URL);
65 const matches = sitemapXml.match(/<loc>(https:\/\/www\.grupku\.net\/202[0-9]\/[0-9]{2}\/[^\s<]+\.html)<\/loc>/g) || [];
66 for (const m of matches) {
67 const cleanUrl = m.replace(/<\/?loc>/g, "").trim();
68 urls.add(cleanUrl);
69 }
70 } catch (err) {
71 console.error("[!] Error membaca sitemap:", err.message);
72 }
73
74 // 2. Ambil dari homepage
75 try {
76 const homeHtml = await fetchWithRetry(BASE_URL);
77 const matches = homeHtml.match(/https:\/\/www\.grupku\.net\/202[0-9]\/[0-9]{2}\/[a-zA-Z0-9_\-]+\.html/g) || [];
78 for (const u of matches) {
79 urls.add(u.trim());
80 }
81 } catch (err) {
82 console.error("[!] Error membaca homepage:", err.message);
83 }
84
85 return Array.from(urls);
86}
87
88function parseGroupPage(pageHtml, url) {
89 // 1. Title
90 let title = "Tanpa Judul";
91 const h1Match = pageHtml.match(/<h1[^>]*class=['"]gs-single-title['"][^>]*>([\s\S]*?)<\/h1>/i);
92 if (h1Match) {
93 let raw = h1Match[1].replace(/<span[^>]*class=['"][^'"]*gs-tier-badge[^'"]*['"][^>]*>[\s\S]*?<\/span>/gi, "");
94 title = cleanText(raw);
95 } else {
96 const ogTitle = pageHtml.match(/<meta[^>]+property=['"]og:title['"][^>]+content=['"]([^'"]+)['"]/i);
97 if (ogTitle) title = cleanText(ogTitle[1]);
98 }
99
100 // 2. Direct Invite URL (WhatsApp / Telegram)
101 let inviteUrl = "";
102 const urlDiv = pageHtml.match(/<div[^>]*class=['"]group-url['"][^>]*>\s*(https?:\/\/[^\s<]+)\s*<\/div>/i);
103 if (urlDiv) {
104 inviteUrl = urlDiv[1].trim();
105 } else {
106 const direct = pageHtml.match(/https?:\/\/(?:chat\.whatsapp\.com|t\.me)\/[a-zA-Z0-9_\-\.\?&=]+/i);
107 if (direct) {
108 inviteUrl = direct[0].trim();
109 }
110 }
111
112 // 3. Platform
113 let platform = "Other";
114 if (inviteUrl.includes("chat.whatsapp.com") || inviteUrl.includes("wa.me") || /whatsapp/i.test(pageHtml)) {
115 platform = "WhatsApp";
116 } else if (inviteUrl.includes("t.me") || /telegram/i.test(pageHtml)) {
117 platform = "Telegram";
118 }
119
120 // 4. Description
121 let description = "";
122 const descMatch = pageHtml.match(/<div[^>]*class=['"]gs-single-desc['"][^>]*>[\s\S]*?<p>([\s\S]*?)<\/p>/i);
123 if (descMatch) {
124 description = cleanText(descMatch[1]);
125 }
126
127 // 5. Icon URL
128 let iconUrl = "";
129 const iconMatch = pageHtml.match(/<div[^>]*class=['"]gs-single-thumb['"][^>]*>[\s\S]*?<img[^>]+src=['"]([^'"]+)['"]/i) ||
130 pageHtml.match(/<meta[^>]+property=['"]og:image['"][^>]+content=['"]([^'"]+)['"]/i);
131 if (iconMatch) {
132 iconUrl = iconMatch[1];
133 }
134
135 // 6. Tags & Category
136 const tagMatches = pageHtml.match(/<a[^>]*class=['"]gs-tag['"][^>]*>\s*([\s\S]*?)\s*<\/a>/gi) || [];
137 const tags = tagMatches
138 .map((t) => cleanText(t))
139 .filter((t) => t && !t.includes("cat.term"));
140 const uniqueTags = Array.from(new Set(tags));
141 const category = uniqueTags[0] || "Umum";
142
143 // 7. Date
144 let date = "";
145 const dateMatch = pageHtml.match(/<i class=['"]fas fa-calendar-alt['"]><\/i>\s*([^<]+)<\/span>/i);
146 if (dateMatch) {
147 date = cleanText(dateMatch[1]);
148 }
149
150 return {
151 title,
152 platform,
153 category,
154 tags: uniqueTags,
155 invite_url: inviteUrl,
156 description,
157 date,
158 icon_url: iconUrl,
159 page_url: url,
160 };
161}
162
163// Concurrency pool helper
164async function pool(items, limit, fn) {
165 const results = [];
166 const executing = [];
167 for (const item of items) {
168 const p = Promise.resolve().then(() => fn(item));
169 results.push(p);
170 if (limit <= items.length) {
171 const e = p.then(() => executing.splice(executing.indexOf(e), 1));
172 executing.push(e);
173 if (executing.length >= limit) {
174 await Promise.race(executing);
175 }
176 }
177 }
178 return Promise.all(results);
179}
180
181async function main() {
182 console.log("[*] Mengambil link grup dari grupku.net ...");
183 const urls = await getAllGroupUrls();
184 console.log(`[+] Ditemukan ${urls.length} postingan grup.`);
185
186 if (urls.length === 0) {
187 console.error("[-] Tidak ada URL yang ditemukan.");
188 return;
189 }
190
191 console.log("[*] Mulai scraping data (concurrency 10)...");
192 let processed = 0;
193 const groups = [];
194
195 await pool(urls, 10, async (url) => {
196 try {
197 const html = await fetchWithRetry(url);
198 const data = parseGroupPage(html, url);
199 if (data.invite_url) {
200 groups.push(data);
201 }
202 } catch (err) {
203 // ignore individual failure
204 } finally {
205 processed++;
206 if (processed % 25 === 0 || processed === urls.length) {
207 console.log(` Progres: ${processed}/${urls.length} diproses (Grup valid: ${groups.length})`);
208 }
209 }
210 });
211
212 // Urutkan berdasarkan judul
213 groups.sort((a, b) => a.title.localeCompare(b.title));
214
215 // Simpan JSON
216 const jsonPath = path.resolve(process.cwd(), "grupku_data.json");
217 fs.writeFileSync(jsonPath, JSON.stringify(groups, null, 2), "utf-8");
218 console.log(`[✓] Berhasil disimpan ke JSON: ${jsonPath}`);
219
220 // Simpan CSV
221 const csvPath = path.resolve(process.cwd(), "grupku_data.csv");
222 const csvHeaders = ["Title", "Platform", "Category", "Invite URL", "Description", "Date", "Page URL"];
223 const csvRows = [csvHeaders.join(",")];
224 for (const g of groups) {
225 const escapeCsv = (str) => `"${(str || "").replace(/"/g, '""')}"`;
226 csvRows.push([
227 escapeCsv(g.title),
228 escapeCsv(g.platform),
229 escapeCsv(g.category),
230 escapeCsv(g.invite_url),
231 escapeCsv(g.description),
232 escapeCsv(g.date),
233 escapeCsv(g.page_url),
234 ].join(","));
235 }
236 fs.writeFileSync(csvPath, csvRows.join("\n"), "utf-8");
237 console.log(`[✓] Berhasil disimpan ke CSV: ${csvPath}`);
238
239 // Tampilkan Ringkasan
240 console.log("\n========================================================");
241 console.log(`HASIL SCRAPING NODE.JS (${groups.length} Grup Berhasil Diambil)`);
242 console.log("========================================================");
243
244 const stats = {};
245 for (const g of groups) {
246 stats[g.category] = (stats[g.category] || 0) + 1;
247 }
248 const topCategories = Object.entries(stats).sort((a, b) => b[1] - a[1]);
249 console.log("Kategori Terbanyak:");
250 topCategories.slice(0, 8).forEach(([cat, count], i) => {
251 console.log(` ${i + 1}. ${cat}: ${count} grup`);
252 });
253 console.log("========================================================\n");
254}
255
256main().catch(console.error);
257
Discussion & Comments0

No comments yet — be the first to leave feedback or start a code discussion.

Sign in with your GitHub account to write comments and join the discussion.