7
Crawler: Pipeline Pengambilan Data
crawler/script.js adalah skrip satu kali jalan yang membangun ulang data/quran.json. Skrip ini bukan bagian dari jalur request produksi; ia dijalankan manual lewat npm run crawl, yang memetakan langsung ke node crawler/script.js (package.json:9-141). README menyebut perintah itu sebagai langkah "collect new data from the data source, then unifying it in one JSON file" (README.md:52-552).
Skrip hanya menarik empat modul: writeFile dari fs.promises, isomorphic-fetch, sleep dari crawler/helpers.js, dan dua base URL dari config.js. Selain itu ada satu sumber lokal, data/quran-tafsir.json, yang di-require sebagai temp (crawler/script.js:1-103). config.js membaca SOURCE_API_BASEURL dan SOURCE_API_BASEURL_2 dari .env lewat dotenv (config.js:3-114).
⚠️
data/quran-tafsir.jsontidak ada di repository ini. Polaquran-tafsir.jsonmasuk.gitignore(.gitignore:1-4), sehinggarequire('../data/quran-tafsir.json')gagal denganMODULE_NOT_FOUNDpada checkout bersih. Berkas itu harus disediakan sendiri sebelumnpm run crawlbisa berjalan.
Berkas temp itu memegang tafsir dan terjemahan Indonesia, jadi API sumber hanya diminta empat editions: quran-simple-enhanced, ar.alafasy, en.transliteration, dan en.sahih (crawler/script.js:12-205). Teks Indonesia per ayah diambil dari temp dengan kunci berisi nomor surah dan nomor ayah (crawler/script.js:116-1196). Rincian pemetaan setiap ayah dan preBismillah dibahas di Normalisasi Ayah dan preBismillah.
Satu kali jalan: dari daftar tafsir ke data/quran.json
main() menjalankan seluruh pipeline secara berurutan: satu fetch ke SOURCE_API_BASEURL_2 untuk daftar tafsir surah, lalu for (let i = 1; i <= 114; i++) dengan await operate(i, tafsirSurahs[i - 1]) (crawler/script.js:182-1917). Setiap iterasi di-await, jadi request berjalan serial: tidak ada Promise.all di skrip ini, dan satu-satunya pemakaian Promise adalah Promise.race di getSurah (crawler/script.js:21-388).
%%{init: {'themeVariables': {'actorBkg': '#336791', 'actorTextColor': '#ffffff', 'actorBorder': '#1f4a6d'}}}%%
sequenceDiagram
autonumber
participant main as main crawler/script.js
participant remote as SOURCE_API_BASEURL_2
participant op as operate
participant gs as getSurah
participant api as SOURCE_API_BASEURL
participant tmr as sleep helpers.js
participant out as data/quran.json
main->>remote: fetch daftar tafsir surah
remote-->>main: tafsirSurahs
loop i = 1 sampai 114
main->>op: operate i dan tafsirSurahs[i-1]
op->>gs: getSurah i
gs->>api: fetch apiUrl empat editions
gs->>tmr: sleep delay 6000 throwReject true
alt fetch selesai sebelum 6000 ms
api-->>gs: Response
else timer menang
tmr-->>gs: reject timeout true
gs->>gs: action diulang
end
gs-->>op: Response
op->>op: JSON.parse lalu gabung temp
op-->>main: result
end
main->>out: writeFile data quran.json
%% fig: Alur satu kali npm run crawl
%% desc: main mengambil daftar tafsir surah, mengiterasi 114 surah lewat operate dan getSurah dengan pengaman sleep 6000 ms, lalu menulis data/quran.json.Tahap-tahap yang dijalankan main() beserta keluarannya:
| # | Langkah | Kode yang dipakai | Keluaran |
|---|---|---|---|
| 1 | Ambil daftar tafsir surah | fetch(SOURCE_API_BASEURL_2) lalu .json() (crawler/script.js:182-1917) | array tafsirSurahs |
| 2 | Iterasi 114 surah secara serial | for dari i = 1 sampai 114 dengan await operate(i, tafsirSurahs[i - 1]) (crawler/script.js:182-1917) | satu result per surah masuk response |
| 3 | Bungkus envelope | JSON.stringify({ license, source, audioEdition, data }, null, 2) (crawler/script.js:193-2059) | string JSON berindentasi 2 spasi |
| 4 | Tulis berkas | writeFile('./data/quran.json', data.replace(/%u/g, '\\u')) (crawler/script.js:193-2059) | data/quran.json |
Indeks tafsirSurahs[i - 1] mengandalkan urutan array dari SOURCE_API_BASEURL_2 sama dengan urutan nomor surah. Kalau urutannya bergeser, nama Indonesia dan tafsir surah akan menempel pada surah yang salah tanpa error apa pun (crawler/script.js:182-1917).
Pengaman timeout: Promise.race dengan sleep
getSurah menyusun apiUrl dari SOURCE_API_BASEURL ditambah /surah/<nomor>/editions/<empat edition>, mencetak > Prepare surah: ..., lalu membungkus percobaan di dalam action() (crawler/script.js:12-205, crawler/script.js:21-388). Setiap percobaan menjalankan Promise.race antara fetch(apiUrl) dan sleep({ delay: 6000, throwReject: true }) (crawler/script.js:21-388). sleep hanya membungkus setTimeout; dengan throwReject: true ia memanggil reject({ timeout: true }), bukan resolve (crawler/helpers.js:1-610).
Begitu sleep menang, catch memeriksa err.timeout, mencetak > Request Timeout, Trying again..., lalu memanggil action() lagi (crawler/script.js:21-388). Pengulangan ini tidak punya penghitung maupun batas jumlah percobaan: selama timer selalu menang, iterasi surah itu tidak pernah selesai.
| Parameter | Default di crawler/helpers.js | Nilai yang dipakai crawler | Akibat |
|---|---|---|---|
delay | 2000 ms (crawler/helpers.js:1-610) | 6000 ms (crawler/script.js:21-388) | timer menunggu 6 detik sebelum menyerah |
throwReject | false (crawler/helpers.js:1-610) | true (crawler/script.js:21-388) | promise reject dengan { timeout: true } |
Dua lapis retry
Ada dua pengulangan terpisah dan keduanya rekursif tanpa batas. Lapis dalam ada di action() dan hanya bereaksi pada err.timeout (crawler/script.js:21-388). Lapis luar ada di catch milik operate; lapis ini hanya aktif kalau err membawa field surah, yang berasal dari throw { surah, code, status } ketika code !== 200 (crawler/script.js:49-5811). Saat aktif, operate(surah, tafsirSurah, true) dipanggil ulang, dan argumen ketiga hanya menambah log Retrying at surah (crawler/script.js:169-18012).
graph LR
op["operate(surah, tafsirSurah)"] --> act["getSurah.action()"]
act --> race{"Promise.race"}
race -->|"fetch menang"| res["Response dari API"]
race -->|"sleep 6000 ms menang"| to["reject timeout true"]
to -->|"action dipanggil lagi"| act
res --> code{"code === 200"}
code -->|"tidak"| thrown["throw surah code status"]
thrown -->|"operate dengan tryFlag true"| op
code -->|"ya"| build["susun result"]
build --> done["response.push result"]
style race fill:#f59e0b,color:#000
style code fill:#f59e0b,color:#000
style to fill:#dc2626,color:#fff
style thrown fill:#dc2626,color:#fff
style res fill:#2d7d46,color:#fff
style done fill:#2d7d46,color:#fff
style build fill:#8b5cf6,color:#fff
%% fig: Dua lapis retry di crawler
%% desc: Lapis dalam mengulang action saat sleep menang; lapis luar mengulang operate saat response non-200 membawa surah.| Lapis | Fungsi | Pemicu | Aksi | Batas |
|---|---|---|---|---|
| Dalam | action() di dalam getSurah | err.timeout dari sleep (crawler/script.js:21-388) | return await action() (crawler/script.js:21-388) | rekursi tanpa penghitung |
| Luar | operate() | err yang membawa surah (crawler/script.js:49-5811) | return await operate(surah, tafsirSurah, true) (crawler/script.js:169-18012) | rekursi tanpa penghitung |
Error yang tidak membawa surah tidak diulang. catch mencetak console.error(err) dan operate selesai dengan undefined; nilai itu tetap di-push ke response karena main tidak memeriksa hasil operate (crawler/script.js:169-18012, crawler/script.js:182-1917).
⚠️ Pemeriksaan
code !== 200berada setelahdatadi-destructure dantemp[...]dibaca (crawler/script.js:49-5811). Kalau response non-200 tidak membawadata, yang terjadi adalahTypeErrortanpa fieldsurah, sehingga jalur retry lapis luar tidak pernah aktif dan surah itu hilang dari hasil.
Satu koreksi teks dilakukan di tahap ini: untuk surah 13 dan 55, kata Makkiyyah pada tafsir diganti menjadi Madaniyyah, dan seluruh tag HTML dibuang lebih dahulu (crawler/script.js:89-9513).
Menulis ulang data/quran.json
Setelah 114 iterasi, main menyusun envelope { license, source, audioEdition, data } dengan JSON.stringify(..., null, 2), mengembalikan %u menjadi \u lewat data.replace(/%u/g, '\\u'), lalu menulis berkas dan mencetak > Writed N surah. (crawler/script.js:193-2059). Swap sebaliknya terjadi saat membaca response: responseText.replace(/\\u/g, '%u') dijalankan sebelum JSON.parse (crawler/script.js:49-5811). Jadi teks Arab yang tersimpan tetap berbentuk escape \uXXXX di dalam berkas JSON.
⚠️
writeFile('./data/quran.json', ...)memakai path relatif terhadap direktori kerja, sedangkanrequire('../data/quran-tafsir.json')relatif terhadap direktoricrawler/(crawler/script.js:193-2059, crawler/script.js:1-103). Jalankannpm run crawldari akar repository supaya keduanya menunjuk berkas yang dimaksud.
⚠️
.env.examplemendaftarkanSOURCE_SOURCE_API_BASEURLdanSOURCE_SOURCE_API_BASEURL_2dengan prefiks ganda, sedangkanconfig.jsmembacaSOURCE_API_BASEURLdanSOURCE_API_BASEURL_2(.env.example:1-314, config.js:3-114). Nama yang tidak cocok membuatSOURCE_API_BASEURLbernilaiundefineddan setiapfetch(apiUrl)gagal. Selengkapnya di Konfigurasi, Menjalankan Lokal, dan Deployment.
Bentuk akhir setiap elemen data — number, sequence, name, revelation, tafsir, dan verses — dijelaskan di Model Data quran.json dan juz.json.