Lompat ke konten
dokumentasi.id
Commit 65055c7
v1
Star821

Kamu membaca versi lama. Halaman ini hanya bisa dibaca.

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-14). README menyebut perintah itu sebagai langkah "collect new data from the data source, then unifying it in one JSON file" (README.md:52-55).

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-10). config.js membaca SOURCE_API_BASEURL dan SOURCE_API_BASEURL_2 dari .env lewat dotenv (config.js:3-11).

⚠️ data/quran-tafsir.json tidak ada di repository ini. Pola quran-tafsir.json masuk .gitignore (.gitignore:1-4), sehingga require('../data/quran-tafsir.json') gagal dengan MODULE_NOT_FOUND pada checkout bersih. Berkas itu harus disediakan sendiri sebelum npm run crawl bisa 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-20). Teks Indonesia per ayah diambil dari temp dengan kunci berisi nomor surah dan nomor ayah (crawler/script.js:116-119). 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-191). 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-38).

%%{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.
Fig. 1: Alur satu kali npm run crawlmain 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:

#LangkahKode yang dipakaiKeluaran
1Ambil daftar tafsir surahfetch(SOURCE_API_BASEURL_2) lalu .json() (crawler/script.js:182-191)array tafsirSurahs
2Iterasi 114 surah secara serialfor dari i = 1 sampai 114 dengan await operate(i, tafsirSurahs[i - 1]) (crawler/script.js:182-191)satu result per surah masuk response
3Bungkus envelopeJSON.stringify({ license, source, audioEdition, data }, null, 2) (crawler/script.js:193-205)string JSON berindentasi 2 spasi
4Tulis berkaswriteFile('./data/quran.json', data.replace(/%u/g, '\\u')) (crawler/script.js:193-205)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-191).

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-20, crawler/script.js:21-38). Setiap percobaan menjalankan Promise.race antara fetch(apiUrl) dan sleep({ delay: 6000, throwReject: true }) (crawler/script.js:21-38). sleep hanya membungkus setTimeout; dengan throwReject: true ia memanggil reject({ timeout: true }), bukan resolve (crawler/helpers.js:1-6).

Begitu sleep menang, catch memeriksa err.timeout, mencetak > Request Timeout, Trying again..., lalu memanggil action() lagi (crawler/script.js:21-38). Pengulangan ini tidak punya penghitung maupun batas jumlah percobaan: selama timer selalu menang, iterasi surah itu tidak pernah selesai.

ParameterDefault di crawler/helpers.jsNilai yang dipakai crawlerAkibat
delay2000 ms (crawler/helpers.js:1-6)6000 ms (crawler/script.js:21-38)timer menunggu 6 detik sebelum menyerah
throwRejectfalse (crawler/helpers.js:1-6)true (crawler/script.js:21-38)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-38). 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-58). Saat aktif, operate(surah, tafsirSurah, true) dipanggil ulang, dan argumen ketiga hanya menambah log Retrying at surah (crawler/script.js:169-180).

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.
Fig. 2: Dua lapis retry di crawlerLapis dalam mengulang action saat sleep menang; lapis luar mengulang operate saat response non-200 membawa surah.
LapisFungsiPemicuAksiBatas
Dalamaction() di dalam getSuraherr.timeout dari sleep (crawler/script.js:21-38)return await action() (crawler/script.js:21-38)rekursi tanpa penghitung
Luaroperate()err yang membawa surah (crawler/script.js:49-58)return await operate(surah, tafsirSurah, true) (crawler/script.js:169-180)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-180, crawler/script.js:182-191).

⚠️ Pemeriksaan code !== 200 berada setelah data di-destructure dan temp[...] dibaca (crawler/script.js:49-58). Kalau response non-200 tidak membawa data, yang terjadi adalah TypeError tanpa field surah, 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-95).

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-205). Swap sebaliknya terjadi saat membaca response: responseText.replace(/\\u/g, '%u') dijalankan sebelum JSON.parse (crawler/script.js:49-58). Jadi teks Arab yang tersimpan tetap berbentuk escape \uXXXX di dalam berkas JSON.

⚠️ writeFile('./data/quran.json', ...) memakai path relatif terhadap direktori kerja, sedangkan require('../data/quran-tafsir.json') relatif terhadap direktori crawler/ (crawler/script.js:193-205, crawler/script.js:1-10). Jalankan npm run crawl dari akar repository supaya keduanya menunjuk berkas yang dimaksud.

⚠️ .env.example mendaftarkan SOURCE_SOURCE_API_BASEURL dan SOURCE_SOURCE_API_BASEURL_2 dengan prefiks ganda, sedangkan config.js membaca SOURCE_API_BASEURL dan SOURCE_API_BASEURL_2 (.env.example:1-3, config.js:3-11). Nama yang tidak cocok membuat SOURCE_API_BASEURL bernilai undefined dan setiap fetch(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.