# Dokumentasi Scraping Harga Cabai Dokumen ini menjelaskan proses pembaruan data harga melalui panel admin, mulai dari tombol **Perbarui data** di frontend sampai data hasil scraping disimpan ke MySQL. ## Ringkasan Fitur scraping mengambil data dari SISKAPERBAPO menggunakan: - `requests` untuk mengirim HTTP `GET` dan `POST`; - `pandas.read_html()` untuk membaca tabel HTML; - `lxml` sebagai parser HTML; - MySQL untuk menyimpan hasil ke tabel `price_history`. Implementasi saat ini **tidak menggunakan Selenium, WebDriver, Playwright, atau browser automation**. Model LSTM tidak dilatih ulang setelah scraping. Data baru hanya disimpan sebagai data historis yang dapat digunakan oleh API saat melakukan inference. ## Komponen yang terlibat | Komponen | Lokasi | Fungsi | |---|---|---| | Tombol dan panel admin | `../website/src/App.jsx` | Memulai scraping, menampilkan progress, dan memuat ulang data | | Client API frontend | `../website/src/services/api.js` | Mengirim request ke backend | | Route scraping | `app/api/routes.py` | Validasi admin, membuat job, membaca status, dan menyimpan data | | Scraper SISKAPERBAPO | `app/services/siskaperbapo_scraper.py` | Mengambil dan membersihkan tabel harga | | Konfigurasi aplikasi | `app/config.py` | Menentukan URL sumber, komoditas, timeout, dan sumber data | | Data lokasi pasar | `app/data/markets.json` | Menyimpan slug, nama, dan koordinat pasar | | Database | MySQL | Menyimpan `price_history` dan `scraping_logs` | ## Alur tombol "Perbarui data" Alur normalnya adalah: ```text Admin klik "Perbarui data" ↓ Frontend mengirim POST /api/v1/admin/scraping/jobs ↓ Backend memvalidasi role admin dan opsi scraping ↓ Backend membuat job_id dan menjalankan thread background ↓ Scraper mengambil tabel SISKAPERBAPO ↓ Harga dibersihkan dan dikonversi ke Rupiah ↓ Data di-insert atau di-update ke price_history ↓ Status scraping_logs diperbarui ↓ Frontend mengecek status job setiap 700 ms ↓ Dashboard admin memuat ulang data ``` Proses ini berbeda dari pemanggilan prediksi. Scraping hanya memperbarui data historis; artifact model di `models/` tetap digunakan seperti sebelumnya. ## Proses di frontend Komponen admin berada di `../website/src/App.jsx`. Fungsi `refreshPrices()`: 1. Mengaktifkan status `refreshing`; 2. Memanggil `dashboardService.startScraping(options)`; 3. Menerima `job_id`; 4. Meminta status job setiap 700 ms; 5. Menampilkan progress dan pesan proses; 6. Menampilkan error jika status berubah menjadi `failed`; 7. Memanggil `loadAdminData()` setelah berhasil. Request frontend didefinisikan di `../website/src/services/api.js`: ```javascript startScraping: (options) => request('/admin/scraping/jobs', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(options) }) ``` Contoh payload: ```json { "commodities": ["rawit"], "start_date": "", "end_date": "" } ``` Contoh untuk rentang tanggal: ```json { "commodities": ["rawit"], "start_date": "2025-10-01", "end_date": "2025-10-07" } ``` Jika tanggal tidak diisi, scraper mengambil tabel terbaru melalui metode `scrape()`. Jika tanggal awal dan akhir diisi, scraper menggunakan metode `scrape_range()`. ## Endpoint scraping Semua endpoint berikut memiliki prefix `/api/v1` dan membutuhkan session admin. ### Memulai scraping ```text POST /api/v1/admin/scraping/jobs ``` Payload: ```json { "commodities": ["rawit"], "start_date": "2025-10-01", "end_date": "2025-10-07", "max_records": 1000 } ``` Field: | Field | Wajib | Keterangan | |---|---:|---| | `commodities` | Tidak | Array slug: `rawit`, `merah`, atau `keriting`. Default mengikuti `SISKAPERBAPO_COMMODITIES` | | `start_date` | Tidak | Tanggal awal format `YYYY-MM-DD` | | `end_date` | Tidak | Tanggal akhir format `YYYY-MM-DD` | | `max_records` | Tidak | Batas hasil antara 1 sampai 10.000 | Response awal menggunakan HTTP `202` karena proses berjalan di background: ```json { "data": { "id": "job-id", "status": "running", "progress": 0, "message": "Mengantri scraping", "records_inserted": 0, "records_updated": 0 } } ``` ### Melihat status scraping ```text GET /api/v1/admin/scraping/jobs/{job_id} ``` Status yang mungkin: - `running`: proses masih berlangsung; - `success`: proses selesai; - `failed`: proses gagal. Field progress menunjukkan persentase proses. Field `message` menjelaskan tahap yang sedang berjalan. ### Melihat daftar riwayat ```text GET /api/v1/admin/scraping/logs ``` ### Melihat isi satu riwayat ```text GET /api/v1/admin/scraping/logs/{log_id}/records ``` ### Menghapus satu riwayat ```text DELETE /api/v1/admin/scraping/logs/{log_id} ``` Endpoint ini menghapus log yang dipilih dan record `price_history` yang memiliki `scraping_log_id` tersebut. ### Menghapus seluruh riwayat scraping ```text DELETE /api/v1/admin/scraping/history ``` Penghapusan dibatasi pada data yang bersumber dari `siskaperbapo`; data hasil import CSV tidak ikut dihapus. ## Proses backend Route `POST /admin/scraping/jobs` berada di `app/api/routes.py`. Route tersebut: 1. Memastikan user memiliki role `admin`; 2. Membaca payload JSON; 3. Memvalidasi komoditas, tanggal, dan batas record; 4. Menolak proses baru jika ada scraping atau preprocessing yang masih `running`; 5. Membuat ID job; 6. Memulai `_run_scraping_job()` di thread background. Fungsi `_run_scraping_job()` melakukan langkah berikut: 1. Membuat row baru pada `scraping_logs` dengan status `running`; 2. Membuka koneksi MySQL; 3. Memanggil `scrape()` atau `scrape_range()`; 4. Mencocokkan slug komoditas dan pasar dengan tabel master; 5. Menyimpan observasi ke `price_history`; 6. Menghitung jumlah record baru dan record yang diperbarui; 7. Mengubah status log menjadi `success`; 8. Melakukan rollback dan mencatat error jika proses gagal. ## Cara scraper mengambil data Implementasinya berada di `app/services/siskaperbapo_scraper.py`. ### Data terbaru dengan `scrape()` Untuk setiap komoditas yang dipilih, scraper mengirim: ```python requests.get( self.url, params={self.commodity_param: commodity}, timeout=self.timeout, headers={"User-Agent": "PanenHarga/1.0"}, ) ``` Response HTML dibaca dengan: ```python pd.read_html(StringIO(response.text)) ``` Scraper kemudian mencari: - kolom lokasi atau pasar; - kolom yang dapat dibaca sebagai tanggal; - baris lima pasar target; - harga pada setiap tanggal. ### Data historis dengan `scrape_range()` Untuk rentang tanggal, scraper mengakses endpoint: ```text /harga/tabel.nodesign ``` melalui HTTP `POST`, dengan data: ```python { "tanggal": "YYYY-MM-DD", "kabkota": "jemberkab", "pasar": "market_id" } ``` Proses dilakukan untuk setiap kombinasi: ```text tanggal × pasar ``` ID pasar yang digunakan: | Slug | Nama | ID sumber | |---|---|---:| | `pasar_tanjung` | Pasar Tanjung | `11` | | `pasar_kreongan` | Pasar Kreongan | `12` | | `pasar_tegal_besar` | Pasar Tegal Besar | `13` | | `pasar_mangli` | Pasar Mangli | `148` | | `pasar_wirolegi` | Pasar Wirolegi | `149` | ## Normalisasi harga Scraper memakai `_parse_price()` untuk membersihkan nilai seperti: - `Rp22`; - `22`; - `22,5`; - `22.000`. Kemudian `_price_in_rupiah()` menerapkan aturan: ```python price * 1000 if 0 < price < 1000 else price ``` Dengan demikian: | Nilai sumber | Nilai yang disimpan | |---:|---:| | `22` | `22000` | | `22.5` | `22500` | | `22000` | `22000` | Harga dalam `price_history` disimpan sebagai Rupiah penuh. ## Penyimpanan dan duplikasi data Setiap observasi memiliki bentuk: ```json { "commodity": "rawit", "market": "pasar_tanjung", "date": "2025-10-01", "price": 22000, "source": "siskaperbapo" } ``` Record disimpan ke `price_history` menggunakan pola: ```sql INSERT INTO price_history (...) VALUES (...) ON DUPLICATE KEY UPDATE price = VALUES(price), source = VALUES(source), scraping_log_id = VALUES(scraping_log_id) ``` Artinya: - kombinasi yang belum ada menjadi record baru; - kombinasi yang sudah ada diperbarui; - harga tidak disimpan sebagai duplikat jika unique key database telah dikonfigurasi. Kolom `scraping_log_id` menghubungkan setiap harga dengan riwayat proses scraping. ## `scraping_logs` Tabel `scraping_logs` dipakai untuk mencatat proses. Informasi yang disimpan meliputi: - waktu mulai; - waktu selesai; - sumber data; - status; - jumlah record baru; - jumlah record diperbarui; - pesan error atau warning. Status awal adalah `running`. Setelah proses selesai, status menjadi `success` atau `failed`. ## Scraping dan preprocessing berbeda Scraping: ```text Mengambil data dari SISKAPERBAPO ``` Preprocessing: ```text Memeriksa dan mengisi data kosong dari riwayat scraping ``` Preprocessing dijalankan melalui: ```text POST /api/v1/admin/preprocessing/jobs ``` Proses ini menggunakan interpolasi linear dan bukan bagian dari pengambilan data dari website. ## Konfigurasi Konfigurasi berada di `app/config.py` dan dapat diatur melalui environment variable: ```powershell $env:SISKAPERBAPO_URL="https://siskaperbapo.jatimprov.go.id/harga-komoditas" $env:SISKAPERBAPO_COMMODITY_PARAM="komoditas" $env:SISKAPERBAPO_TIMEOUT="30" $env:SISKAPERBAPO_COMMODITIES="rawit" ``` Nilai default: - URL: `https://siskaperbapo.jatimprov.go.id/harga-komoditas`; - parameter komoditas: `komoditas`; - timeout: 30 detik; - komoditas default: `rawit`. Tahap awal menggunakan `rawit` untuk mengurangi risiko sumber website mengabaikan parameter komoditas. Komoditas lain sebaiknya diaktifkan setelah struktur response sumber diverifikasi. ## Cara menjalankan dan memeriksa proses Jalankan backend dari folder `api`: ```powershell cd C:\PrediksiHargaCabe\api python main.py ``` Kemudian: 1. Buka website frontend; 2. Login sebagai admin; 3. Buka menu **Perbarui data**; 4. Pilih komoditas; 5. Isi rentang tanggal jika ingin mengambil data historis tertentu; 6. Klik **Mulai scraping** atau tombol **Perbarui data** di bagian atas; 7. Amati progress; 8. Periksa bagian **Riwayat pembaruan**; 9. Buka detail record untuk memeriksa hasil yang disimpan. Jika ingin melihat proses langsung dari database, periksa: ```sql SELECT * FROM scraping_logs ORDER BY id DESC; ``` dan: ```sql SELECT * FROM price_history WHERE source = 'siskaperbapo' ORDER BY price_date DESC; ``` ## Penanganan error Jika request ke sumber gagal, scraper melempar `ScrapingError`. Job akan berstatus `failed`, transaksi database di-rollback, dan pesan error dicatat di `scraping_logs`. Penyebab yang umum: - website SISKAPERBAPO tidak dapat diakses; - struktur tabel HTML berubah; - parameter komoditas tidak sesuai; - koneksi MySQL gagal; - tabel master komoditas atau pasar belum tersedia; - response tidak berisi data yang cocok. Jika proses gagal, periksa: 1. status dan pesan pada panel admin; 2. kolom error di `scraping_logs`; 3. URL dan konfigurasi SISKAPERBAPO; 4. koneksi MySQL; 5. struktur HTML response sumber. ## Catatan penting - Scraping hanya dapat dijalankan oleh admin. - Hanya satu scraping atau preprocessing yang boleh berjalan pada satu waktu di proses aplikasi. - Data baru tidak otomatis melatih ulang model. - Tidak ada Selenium pada implementasi saat ini. - Perubahan struktur HTML SISKAPERBAPO dapat membuat parser gagal. - Data kosong yang dihasilkan scraping ditangani pada tahap preprocessing terpisah.