Back to Home
Gistify Snippet
Public
cse.js — cari di Google Programmable Search Engine (element API, pure JS)
cse.js — cari di Google Programmable Search Engine (element API, pure JS)
#github-gist#javascript#gist:01680dc12d08ead52e347b3212091a9d#gistsync:2026-09-04T02:58:45Z
4
Rawcse.jsjavascript
134 lines| 1 | #!/usr/bin/env node |
| 2 | // cse.js — cari di Google Programmable Search Engine (element API, pure JS) |
| 3 | |
| 4 | const CX = '006516753008110874046:1ugcdt3vo7z' // download/direct-link search |
| 5 | const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36' |
| 6 | const BASE = 'https://cse.google.com' |
| 7 | |
| 8 | let jar = {} // cookie jar: name -> value |
| 9 | |
| 10 | async function get(url, referer) { |
| 11 | const r = await fetch(url, { |
| 12 | headers: { |
| 13 | 'User-Agent': UA, |
| 14 | 'Accept': '*/*', |
| 15 | 'Accept-Language': 'en-US,en;q=0.9', |
| 16 | ...(referer ? { 'Referer': referer } : {}), |
| 17 | ...(Object.keys(jar).length ? { 'Cookie': Object.entries(jar).map(([k, v]) => `${k}=${v}`).join('; ') } : {}) |
| 18 | } |
| 19 | }) |
| 20 | const raw = await r.text() |
| 21 | // simpan cookie baru dari response |
| 22 | let sc |
| 23 | if (typeof r.headers.getSetCookie === 'function') sc = r.headers.getSetCookie() |
| 24 | else if (r.headers.get('set-cookie')) sc = [r.headers.get('set-cookie')] |
| 25 | if (sc) for (const c of sc) { |
| 26 | const [pair] = c.split(';') |
| 27 | const eq = pair.indexOf('=') |
| 28 | if (eq > 0) jar[pair.slice(0, eq).trim()] = pair.slice(eq + 1).trim() |
| 29 | } |
| 30 | return { status: r.status, raw } |
| 31 | } |
| 32 | |
| 33 | // ambil sca_esv dari halaman utama |
| 34 | async function mainPage(cx) { |
| 35 | const { raw } = await get(`${BASE}/cse?cx=${encodeURIComponent(cx)}`) |
| 36 | const m = /sca_esv\\x3d([a-f0-9]+)/.exec(raw) || /sca_esv=([a-f0-9]+)/.exec(raw) |
| 37 | return m ? m[1] : null |
| 38 | } |
| 39 | |
| 40 | // ambil cse_token dari cse.js |
| 41 | async function cseTok(sca, cx) { |
| 42 | const { raw } = await get(`${BASE}/cse.js?sca_esv=${sca}&hpg=1&cx=${encodeURIComponent(cx)}`, `${BASE}/cse?cx=${encodeURIComponent(cx)}`) |
| 43 | const m = /"cse_token":\s*"([^"]+)"/.exec(raw) |
| 44 | if (!m) throw new Error('cse_token tidak ditemukan') |
| 45 | return m[1] |
| 46 | } |
| 47 | |
| 48 | // ambil hasil |
| 49 | async function search(cx, q, start, num) { |
| 50 | const sca = await mainPage(cx) |
| 51 | const tok = await cseTok(sca, cx) |
| 52 | const cb = `google.search.cse.api${Date.now() % 100000}` |
| 53 | const params = new URLSearchParams({ |
| 54 | rsz: 'filtered_cse', num: String(num), hl: 'en', source: 'gcsc', |
| 55 | cselibv: '93e57cb47c499c53', cx, q, safe: 'off', cse_tok: tok, |
| 56 | filter: '1', sort: '', exp: 'cc,esbfa', fexp: '121991397,121991398,121877338,121877336', |
| 57 | oq: q, gs_l: 'partner-generic.3...3113.3113.0.3141.1.1.0.0.0.0.0.0..0.0.csems,nrl=10...0.....34.partner-generic..1.0.0.', |
| 58 | cseclient: 'hosted-page-client', callback: cb, |
| 59 | rurl: `${BASE}/cse?cx=${encodeURIComponent(cx)}` |
| 60 | }) |
| 61 | if (start) params.set('start', String(start)) |
| 62 | const { status, raw } = await get(`${BASE}/cse/element/v1?${params}`, `${BASE}/cse?cx=${encodeURIComponent(cx)}`) |
| 63 | if (status !== 200) throw new Error(`HTTP ${status}`) |
| 64 | const i = raw.indexOf('(') |
| 65 | const j = raw.lastIndexOf(')') |
| 66 | return JSON.parse(raw.slice(i + 1, j)) |
| 67 | } |
| 68 | |
| 69 | function clean(s) { |
| 70 | if (s == null) return '' |
| 71 | return String(s).replace(/<[^>]+>/g, '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, "'").trim() |
| 72 | } |
| 73 | |
| 74 | function fmt(d) { |
| 75 | return (d.results || []).map(r => ({ |
| 76 | title: clean(r.title), |
| 77 | link: r.unescapedUrl || r.url || '', |
| 78 | domain: r.visibleUrl || '', |
| 79 | snippet: clean(r.content) |
| 80 | })) |
| 81 | } |
| 82 | |
| 83 | async function run() { |
| 84 | const args = process.argv.slice(2) |
| 85 | const asJson = args.includes('--json') |
| 86 | const outIdx = args.indexOf('--out') |
| 87 | const outFile = outIdx >= 0 ? (args[outIdx + 1] && !args[outIdx + 1].startsWith('--') ? args[outIdx + 1] : 'output/cse.json') : null |
| 88 | let n = 10, start = 0 |
| 89 | const ni = args.indexOf('--n') |
| 90 | if (ni >= 0) n = parseInt(args[ni + 1], 10) || 10 |
| 91 | const pi = args.indexOf('--page') |
| 92 | if (pi >= 0) start = (parseInt(args[pi + 1], 10) || 1) * 10 |
| 93 | const q = args.find(a => !a.startsWith('--')) |
| 94 | if (!q) { |
| 95 | console.error('usage: node cse.js <kata> [--n 30] [--page 2] [--json] [--out f]') |
| 96 | process.exit(1) |
| 97 | } |
| 98 | |
| 99 | // kunjungan pertama ke cse.google.com untuk dapat NID |
| 100 | await get(`${BASE}/cse?cx=${encodeURIComponent(CX)}`) |
| 101 | |
| 102 | const all = [] |
| 103 | let total = 0 |
| 104 | for (let s = start; s < start + n && all.length < n; s += 10) { |
| 105 | const d = await search(CX, q, s, Math.min(10, n - all.length)) |
| 106 | if (s === start) total = parseInt((d.cursor || {}).estimatedResultCount, 10) || 0 |
| 107 | const items = fmt(d) |
| 108 | if (!items.length) break |
| 109 | all.push(...items) |
| 110 | if (items.length < 10) break |
| 111 | } |
| 112 | |
| 113 | const out = { query: q, total, shown: all.length, results: all } |
| 114 | if (asJson) { |
| 115 | console.log(JSON.stringify(out, null, 2)) |
| 116 | } else { |
| 117 | console.log(`${q}: ${total} hasil (${all.length} ditampilkan)`) |
| 118 | all.forEach((r, i) => { |
| 119 | console.log(`\n[${i + 1}] ${r.title}`) |
| 120 | if (r.domain) console.log(` ${r.domain}`) |
| 121 | console.log(` ${r.link}`) |
| 122 | if (r.snippet) console.log(` ${r.snippet.slice(0, 160)}`) |
| 123 | }) |
| 124 | } |
| 125 | if (outFile) { |
| 126 | const { mkdir, writeFile } = await import('node:fs/promises') |
| 127 | const { dirname } = await import('node:path') |
| 128 | await mkdir(dirname(outFile), { recursive: true }) |
| 129 | await writeFile(outFile, JSON.stringify(out, null, 2)) |
| 130 | console.error(`\nsaved: ${outFile}`) |
| 131 | } |
| 132 | } |
| 133 | |
| 134 | run().catch(e => { console.error('error:', e.message); process.exit(1) }) |
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.