Back to Home
Gistify Snippet
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
Rawgrupku-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 | |
| 6 | const fs = require("fs"); |
| 7 | const path = require("path"); |
| 8 | |
| 9 | const BASE_URL = "https://www.grupku.net/"; |
| 10 | const SITEMAP_URL = "https://www.grupku.net/sitemap.xml"; |
| 11 | |
| 12 | // Helper HTML Entity Unescape |
| 13 | function unescapeHtml(text) { |
| 14 | if (!text) return ""; |
| 15 | return text |
| 16 | .replace(/&/g, "&") |
| 17 | .replace(/</g, "<") |
| 18 | .replace(/>/g, ">") |
| 19 | .replace(/"/g, '"') |
| 20 | .replace(/'/g, "'") |
| 21 | .replace(/•/g, "•") |
| 22 | .replace(/⛩/g, "⛑") |
| 23 | .replace(/️/g, "") |
| 24 | .replace(/¹/g, "¹") |
| 25 | .replace(/⋆/g, "⋆") |
| 26 | .replace(/⊱/g, "⊵") |
| 27 | .replace(/★/g, "★") |
| 28 | .replace(/♥/g, "♥") |
| 29 | .replace(/&#([0-9]{1,6});/gi, (_, num) => String.fromCharCode(parseInt(num, 10))); |
| 30 | } |
| 31 | |
| 32 | function cleanText(text) { |
| 33 | if (!text) return ""; |
| 34 | const noTags = text.replace(/<[^>]+>/g, ""); |
| 35 | return unescapeHtml(noTags).replace(/\s+/g, " ").trim(); |
| 36 | } |
| 37 | |
| 38 | async 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 | |
| 59 | async 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 | |
| 88 | function 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 |
| 164 | async 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 | |
| 181 | async 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 | |
| 256 | main().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.