TIF_E41253368/api/SCRAPING.md

459 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.