459 lines
11 KiB
Markdown
459 lines
11 KiB
Markdown
# 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.
|