pindah periode
This commit is contained in:
parent
23d615c570
commit
f24f8652bf
|
|
@ -158,15 +158,28 @@ public function index(Request $request)
|
||||||
// ================================================================
|
// ================================================================
|
||||||
// CHART DATA
|
// CHART DATA
|
||||||
// ================================================================
|
// ================================================================
|
||||||
$chartSentimen = $hasAnalisis
|
// $chartSentimen = $hasAnalisis
|
||||||
? DB::table('hasil_analisis')
|
// ? DB::table('hasil_analisis')
|
||||||
|
// ->select('sentimen', DB::raw('count(*) as total'))
|
||||||
|
// ->when($periodeId, fn($q) => $q->where('periode_id', $periodeId))
|
||||||
|
// ->when($wisata && $wisata != 'Semua Destinasi', fn($q) => $q->where('wisata', $wisata))
|
||||||
|
// ->groupBy('sentimen')
|
||||||
|
// ->orderByRaw("FIELD(sentimen, 'positif', 'negatif', 'netral')")
|
||||||
|
// ->get()
|
||||||
|
// : collect();
|
||||||
|
|
||||||
|
$rawSentimen = DB::table('hasil_analisis')
|
||||||
->select('sentimen', DB::raw('count(*) as total'))
|
->select('sentimen', DB::raw('count(*) as total'))
|
||||||
->when($periodeId, fn($q) => $q->where('periode_id', $periodeId))
|
->when($periodeId, fn($q) => $q->where('periode_id', $periodeId))
|
||||||
->when($wisata && $wisata != 'Semua Destinasi', fn($q) => $q->where('wisata', $wisata))
|
->when($wisata && $wisata != 'Semua Destinasi', fn($q) => $q->where('wisata', $wisata))
|
||||||
->groupBy('sentimen')
|
->groupBy('sentimen')
|
||||||
->orderByRaw("FIELD(sentimen, 'positif', 'negatif', 'netral')")
|
->pluck('total', 'sentimen');
|
||||||
->get()
|
|
||||||
: collect();
|
$chartSentimen = collect([
|
||||||
|
['sentimen' => 'positif', 'total' => $rawSentimen['positif'] ?? 0],
|
||||||
|
['sentimen' => 'negatif', 'total' => $rawSentimen['negatif'] ?? 0],
|
||||||
|
['sentimen' => 'netral', 'total' => $rawSentimen['netral'] ?? 0],
|
||||||
|
]);
|
||||||
|
|
||||||
$chartDestinasi = $hasAnalisis
|
$chartDestinasi = $hasAnalisis
|
||||||
? DB::table('hasil_analisis')
|
? DB::table('hasil_analisis')
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ class RekomendasiController extends Controller
|
||||||
'tip' => 'Tambah tempat sampah & jadwal pembersihan rutin.',
|
'tip' => 'Tambah tempat sampah & jadwal pembersihan rutin.',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
'Parkir' => [
|
'Aksesibilitas' => [
|
||||||
'keywords' => ['parkir', 'lahan parkir', 'tempat parkir', 'parkiran', 'motor', 'mobil'],
|
'keywords' => ['parkir', 'lahan parkir', 'tempat parkir', 'parkiran', 'motor', 'mobil'],
|
||||||
'color' => 'orange',
|
'color' => 'orange',
|
||||||
'icon' => '🚗',
|
'icon' => '🚗',
|
||||||
|
|
@ -52,16 +52,7 @@ class RekomendasiController extends Controller
|
||||||
'tip' => 'Fasilitas lengkap & terawat meningkatkan kepuasan.',
|
'tip' => 'Fasilitas lengkap & terawat meningkatkan kepuasan.',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
'Keramaian' => [
|
|
||||||
'keywords' => ['ramai', 'macet', 'antri', 'sesak', 'penuh', 'padat', 'berdesakan'],
|
|
||||||
'color' => 'blue',
|
|
||||||
'icon' => '👥',
|
|
||||||
'saran' => [
|
|
||||||
'actions' => ['Atur kapasitas pengunjung', 'Sistem antrean', 'Jam kunjungan fleksibel'],
|
|
||||||
'dampak' => 'Pengalaman pengunjung lebih nyaman',
|
|
||||||
'tip' => 'Manajemen kapasitas pengunjung lebih baik.',
|
|
||||||
],
|
|
||||||
],
|
|
||||||
];
|
];
|
||||||
|
|
||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
|
|
|
||||||
|
|
@ -24,13 +24,15 @@ public function index(Request $request)
|
||||||
->from('hasil_analisis as h')
|
->from('hasil_analisis as h')
|
||||||
->whereColumn('h.periode_id', 'p.id');
|
->whereColumn('h.periode_id', 'p.id');
|
||||||
})
|
})
|
||||||
->orderBy('p.id', 'desc')
|
->orderBy('p.tahun', 'desc')
|
||||||
|
->orderBy('p.bulan', 'desc')
|
||||||
|
// ->orderBy('p.id', 'desc')
|
||||||
->select('p.*')
|
->select('p.*')
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
$availablePeriodeIds = $periodeList->pluck('id')->map(fn($id) => (string) $id);
|
$availablePeriodeIds = $periodeList->pluck('id')->map(fn($id) => (string) $id);
|
||||||
$requestedPeriodeId = $request->filled('periode_id') ? (string) $request->periode_id : null;
|
$requestedPeriodeId = $request->filled('periode_id') ? (string) $request->periode_id : null;
|
||||||
$latestFilledPeriodeId = $periodeList->first()->id ?? null;
|
$latestFilledPeriodeId = $periodeList->sortByDesc('id')->first()->id ?? null;
|
||||||
$periodeId = $request->filled('periode_id')
|
$periodeId = $request->filled('periode_id')
|
||||||
&& $availablePeriodeIds->contains($requestedPeriodeId)
|
&& $availablePeriodeIds->contains($requestedPeriodeId)
|
||||||
? $requestedPeriodeId
|
? $requestedPeriodeId
|
||||||
|
|
|
||||||
|
|
@ -59,85 +59,7 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- ================= MATRIX + PERFORMA ================= --}}
|
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
||||||
|
|
||||||
{{-- CONFUSION MATRIX --}}
|
|
||||||
<div class="bg-white p-6 rounded-xl shadow">
|
|
||||||
<h3 class="font-semibold mb-4">Confusion Matrix</h3>
|
|
||||||
<p class="text-xs text-gray-500 mb-3">
|
|
||||||
Kolom tp/tn/fp/fn sederhana tidak representatif untuk 3 kelas, sehingga disimpan 0.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<table class="w-full text-center border rounded-lg overflow-hidden">
|
|
||||||
<thead class="bg-gray-100">
|
|
||||||
<tr>
|
|
||||||
<th></th>
|
|
||||||
<th>Positif</th>
|
|
||||||
<th>Negatif</th>
|
|
||||||
<th>Netral</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<th class="bg-gray-50">Positif</th>
|
|
||||||
<td class="bg-green-100">{{ $evaluasi->tp ?? 0 }}</td>
|
|
||||||
<td class="bg-red-100">{{ $evaluasi->fp ?? 0 }}</td>
|
|
||||||
<td class="bg-yellow-100">{{ $evaluasi->fn ?? 0 }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th class="bg-gray-50">Negatif</th>
|
|
||||||
<td class="bg-red-100">{{ $evaluasi->fp ?? 0 }}</td>
|
|
||||||
<td class="bg-green-100">{{ $evaluasi->tn ?? 0 }}</td>
|
|
||||||
<td class="bg-yellow-100">0</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th class="bg-gray-50">Netral</th>
|
|
||||||
<td class="bg-yellow-100">0</td>
|
|
||||||
<td class="bg-yellow-100">0</td>
|
|
||||||
<td class="bg-green-100">0</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- PERFORMA --}}
|
|
||||||
<div class="bg-white p-6 rounded-xl shadow">
|
|
||||||
<h3 class="font-semibold mb-4">Performa per Kelas</h3>
|
|
||||||
|
|
||||||
<table class="w-full text-sm border">
|
|
||||||
<thead class="bg-gray-100">
|
|
||||||
<tr>
|
|
||||||
<th>Kelas</th>
|
|
||||||
<th>Precision</th>
|
|
||||||
<th>Recall</th>
|
|
||||||
<th>F1</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td class="text-green-600 font-semibold">Positif</td>
|
|
||||||
<td>{{ $evaluasiTersedia ? number_format($evaluasi->precision ?? 0, 2) : '-' }}</td>
|
|
||||||
<td>{{ $evaluasiTersedia ? number_format($evaluasi->recall ?? 0, 2) : '-' }}</td>
|
|
||||||
<td>{{ $evaluasiTersedia ? number_format($evaluasi->f1_score ?? 0, 2) : '-' }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="text-red-600 font-semibold">Negatif</td>
|
|
||||||
<td>{{ $evaluasiTersedia ? number_format($evaluasi->precision ?? 0, 2) : '-' }}</td>
|
|
||||||
<td>{{ $evaluasiTersedia ? number_format($evaluasi->recall ?? 0, 2) : '-' }}</td>
|
|
||||||
<td>{{ $evaluasiTersedia ? number_format($evaluasi->f1_score ?? 0, 2) : '-' }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="text-yellow-600 font-semibold">Netral</td>
|
|
||||||
<td>{{ $evaluasiTersedia ? number_format($evaluasi->precision ?? 0, 2) : '-' }}</td>
|
|
||||||
<td>{{ $evaluasiTersedia ? number_format($evaluasi->recall ?? 0, 2) : '-' }}</td>
|
|
||||||
<td>{{ $evaluasiTersedia ? number_format($evaluasi->f1_score ?? 0, 2) : '-' }}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- ================= FILTER ================= --}}
|
{{-- ================= FILTER ================= --}}
|
||||||
<form id="filter-form" class="flex items-center gap-3">
|
<form id="filter-form" class="flex items-center gap-3">
|
||||||
|
|
|
||||||
|
|
@ -288,6 +288,19 @@
|
||||||
<img src="{{ asset('images/logo-sentara.png') }}">
|
<img src="{{ asset('images/logo-sentara.png') }}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if ($errors->any())
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'Login Gagal',
|
||||||
|
text: 'Email atau password yang dimasukkan salah.',
|
||||||
|
confirmButtonColor: '#2d5be3'
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endif
|
||||||
|
|
||||||
<div class="title">
|
<div class="title">
|
||||||
Selamat Datang <span>Kembali!</span>
|
Selamat Datang <span>Kembali!</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -63,11 +63,14 @@ class="border rounded-lg px-3 py-2 text-sm bg-white shadow-sm min-w-[150px]">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div x-data="{ tab: new URLSearchParams(window.location.search).get('tab') || 'dashboard' }" class="mb-6">
|
|
||||||
|
<div x-data="{ tab: new URLSearchParams(window.location.search).get('tab') || 'dashboard' }"
|
||||||
|
x-init="$nextTick(() => { if (tab === 'dashboard') renderAllCharts() })"
|
||||||
|
class="mb-6">
|
||||||
|
|
||||||
<!-- TAB -->
|
<!-- TAB -->
|
||||||
<div class="bg-blue-700 rounded-xl p-1 flex space-x-2 shadow mb-6">
|
<div class="bg-blue-700 rounded-xl p-1 flex space-x-2 shadow mb-6">
|
||||||
<button @click="tab='dashboard'"
|
<button @click="tab='dashboard'; $nextTick(() => renderAllCharts())"
|
||||||
:class="tab==='dashboard' ? 'bg-white text-blue-700 shadow' : 'text-white hover:bg-blue-600'"
|
:class="tab==='dashboard' ? 'bg-white text-blue-700 shadow' : 'text-white hover:bg-blue-600'"
|
||||||
class="flex-1 py-3 rounded-lg text-sm font-medium transition">
|
class="flex-1 py-3 rounded-lg text-sm font-medium transition">
|
||||||
Dashboard Sentara
|
Dashboard Sentara
|
||||||
|
|
@ -167,19 +170,31 @@ class="flex-1 py-3 rounded-lg text-sm font-medium transition">
|
||||||
{{-- SCRIPTS --}}
|
{{-- SCRIPTS --}}
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
|
||||||
<script>
|
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const pieData = @json($chartSentimen);
|
||||||
|
const destinationData = @json($chartDestinasi);
|
||||||
|
const text = @json($allText);
|
||||||
|
|
||||||
|
function renderAllCharts() {
|
||||||
// PIE CHART
|
// PIE CHART
|
||||||
const pieData = JSON.parse('{!! json_encode($chartSentimen) !!}');
|
|
||||||
const pieCanvas = document.getElementById('pieChart');
|
const pieCanvas = document.getElementById('pieChart');
|
||||||
if (window.pieChartInstance) window.pieChartInstance.destroy();
|
if (pieCanvas) {
|
||||||
window.pieChartInstance = new Chart(pieCanvas, {
|
if (window.pieChartInstance) {
|
||||||
|
window.pieChartInstance.destroy();
|
||||||
|
window.pieChartInstance = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset canvas dengan clone agar benar-benar bersih
|
||||||
|
const newPie = pieCanvas.cloneNode(false);
|
||||||
|
pieCanvas.parentNode.replaceChild(newPie, pieCanvas);
|
||||||
|
|
||||||
|
window.pieChartInstance = new Chart(newPie, { // <-- newPie, bukan pieCanvas
|
||||||
type: 'pie',
|
type: 'pie',
|
||||||
data: {
|
data: {
|
||||||
labels: pieData.map(i => i.sentimen),
|
labels: pieData.map(i => i.sentimen),
|
||||||
datasets: [{
|
datasets: [{
|
||||||
data: pieData.map(i => i.total),
|
data: pieData.map(i => Number(i.total)),
|
||||||
backgroundColor: ['#3B82F6', '#EF4444', '#F59E0B']
|
backgroundColor: ['#3B82F6', '#EF4444', '#F59E0B']
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
|
|
@ -189,12 +204,15 @@ class="flex-1 py-3 rounded-lg text-sm font-medium transition">
|
||||||
animation: false
|
animation: false
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// BAR CHART SENTIMEN PER DESTINASI
|
// BAR CHART
|
||||||
const destinationData = @json($chartDestinasi);
|
|
||||||
const destinationCanvas = document.getElementById('destinationChart');
|
const destinationCanvas = document.getElementById('destinationChart');
|
||||||
if (destinationCanvas) {
|
if (destinationCanvas) {
|
||||||
if (window.destinationChartInstance) window.destinationChartInstance.destroy();
|
if (window.destinationChartInstance) {
|
||||||
|
window.destinationChartInstance.destroy();
|
||||||
|
window.destinationChartInstance = null;
|
||||||
|
}
|
||||||
window.destinationChartInstance = new Chart(destinationCanvas, {
|
window.destinationChartInstance = new Chart(destinationCanvas, {
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
data: {
|
data: {
|
||||||
|
|
@ -234,12 +252,7 @@ class="flex-1 py-3 rounded-lg text-sm font-medium transition">
|
||||||
legend: {
|
legend: {
|
||||||
position: 'top',
|
position: 'top',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
labels: {
|
labels: { boxWidth: 38, boxHeight: 10, color: '#4B5563', font: { size: 12 } },
|
||||||
boxWidth: 38,
|
|
||||||
boxHeight: 10,
|
|
||||||
color: '#4B5563',
|
|
||||||
font: { size: 12 },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
callbacks: {
|
callbacks: {
|
||||||
|
|
@ -250,19 +263,11 @@ class="flex-1 py-3 rounded-lg text-sm font-medium transition">
|
||||||
scales: {
|
scales: {
|
||||||
x: {
|
x: {
|
||||||
grid: { color: 'rgba(156, 163, 175, 0.22)' },
|
grid: { color: 'rgba(156, 163, 175, 0.22)' },
|
||||||
ticks: {
|
ticks: { color: '#4B5563', maxRotation: 14, minRotation: 14, font: { size: 12 } },
|
||||||
color: '#4B5563',
|
|
||||||
maxRotation: 14,
|
|
||||||
minRotation: 14,
|
|
||||||
font: { size: 12 },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
y: {
|
y: {
|
||||||
beginAtZero: true,
|
beginAtZero: true,
|
||||||
ticks: {
|
ticks: { color: '#4B5563', precision: 0 },
|
||||||
color: '#4B5563',
|
|
||||||
precision: 0,
|
|
||||||
},
|
|
||||||
grid: { color: 'rgba(156, 163, 175, 0.28)' },
|
grid: { color: 'rgba(156, 163, 175, 0.28)' },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -272,8 +277,7 @@ class="flex-1 py-3 rounded-lg text-sm font-medium transition">
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// WORD CLOUD tanpa library eksternal agar tetap tampil stabil.
|
// WORD CLOUD
|
||||||
const text = @json($allText);
|
|
||||||
const wordCloud = document.getElementById('wordCloud');
|
const wordCloud = document.getElementById('wordCloud');
|
||||||
const stopwords = new Set([
|
const stopwords = new Set([
|
||||||
'yang','dan','dari','untuk','dengan','ini','itu','tidak','ada','juga',
|
'yang','dan','dari','untuk','dengan','ini','itu','tidak','ada','juga',
|
||||||
|
|
@ -304,10 +308,10 @@ class="flex-1 py-3 rounded-lg text-sm font-medium transition">
|
||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -0,0 +1,193 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
redistribute_periode.py
|
||||||
|
-----------------------
|
||||||
|
Memisahkan ulasan 2025 yang salah masuk ke periode Mei 2026,
|
||||||
|
lalu memindahkan ke periode yang benar berdasarkan kolom `tanggal`.
|
||||||
|
|
||||||
|
Jalankan dari root project Laravel:
|
||||||
|
python scripts/redistribute_periode.py
|
||||||
|
|
||||||
|
Atau dengan --dry-run untuk preview tanpa mengubah data:
|
||||||
|
python scripts/redistribute_periode.py --dry-run
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import argparse
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# ── path setup ──────────────────────────────────────────────────────────────
|
||||||
|
BASE_DIR = Path(__file__).resolve().parents[1]
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
|
|
||||||
|
# re-use helper dari scraping_pipeline
|
||||||
|
from scraping_pipeline import (
|
||||||
|
db_config,
|
||||||
|
make_connection,
|
||||||
|
prepare_sql,
|
||||||
|
execute,
|
||||||
|
is_sqlite_connection,
|
||||||
|
)
|
||||||
|
|
||||||
|
NAMA_BULAN = [
|
||||||
|
"Januari", "Februari", "Maret", "April", "Mei", "Juni",
|
||||||
|
"Juli", "Agustus", "September", "Oktober", "November", "Desember",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def log(level, msg):
|
||||||
|
print(f"[{level}] {msg}", flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args():
|
||||||
|
p = argparse.ArgumentParser()
|
||||||
|
p.add_argument(
|
||||||
|
"--dry-run",
|
||||||
|
action="store_true",
|
||||||
|
help="Preview perubahan tanpa benar-benar mengubah database.",
|
||||||
|
)
|
||||||
|
p.add_argument(
|
||||||
|
"--tahun",
|
||||||
|
type=int,
|
||||||
|
default=2025,
|
||||||
|
help="Tahun ulasan yang akan dipisah. Default: 2025.",
|
||||||
|
)
|
||||||
|
return p.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def get_or_create_periode(cursor, conn, bulan, tahun, dry_run):
|
||||||
|
"""Ambil id periode yang ada, atau buat baru jika belum ada."""
|
||||||
|
execute(
|
||||||
|
cursor, conn,
|
||||||
|
"SELECT id FROM periode_analisis WHERE bulan = %s AND tahun = %s LIMIT 1",
|
||||||
|
(bulan, tahun),
|
||||||
|
)
|
||||||
|
row = cursor.fetchone()
|
||||||
|
if row:
|
||||||
|
return row[0]
|
||||||
|
|
||||||
|
nama = f"{NAMA_BULAN[bulan - 1]} {tahun}"
|
||||||
|
if dry_run:
|
||||||
|
log("DRY-RUN", f"Akan buat periode baru: {nama}")
|
||||||
|
return None # belum ada id nyata
|
||||||
|
|
||||||
|
execute(
|
||||||
|
cursor, conn,
|
||||||
|
"""
|
||||||
|
INSERT INTO periode_analisis (nama, bulan, tahun, created_at, updated_at)
|
||||||
|
VALUES (%s, %s, %s, NOW(), NOW())
|
||||||
|
""",
|
||||||
|
(nama, bulan, tahun),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
new_id = cursor.lastrowid
|
||||||
|
log("OK", f"Periode baru dibuat: {nama} (id={new_id})")
|
||||||
|
return new_id
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = parse_args()
|
||||||
|
dry_run = args.dry_run
|
||||||
|
tahun = args.tahun
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
log("INFO", "Mode DRY-RUN — tidak ada perubahan yang disimpan.")
|
||||||
|
|
||||||
|
config = db_config()
|
||||||
|
conn = make_connection(config)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# ── 1. Ambil semua ulasan tahun target yang periode-nya salah ───────────
|
||||||
|
# "Salah" = bulan/tahun di kolom tanggal tidak cocok dengan periode_analisis
|
||||||
|
if is_sqlite_connection(conn):
|
||||||
|
sql_fetch = """
|
||||||
|
SELECT u.id, u.tanggal, u.wisata, u.periode_id,
|
||||||
|
p.bulan AS p_bulan, p.tahun AS p_tahun
|
||||||
|
FROM ulasan u
|
||||||
|
JOIN periode_analisis p ON u.periode_id = p.id
|
||||||
|
WHERE strftime('%Y', u.tanggal) = ?
|
||||||
|
AND (
|
||||||
|
CAST(strftime('%m', u.tanggal) AS INTEGER) != p.bulan
|
||||||
|
OR CAST(strftime('%Y', u.tanggal) AS INTEGER) != p.tahun
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
cursor.execute(sql_fetch, (str(tahun),))
|
||||||
|
else:
|
||||||
|
sql_fetch = """
|
||||||
|
SELECT u.id, u.tanggal, u.wisata, u.periode_id,
|
||||||
|
p.bulan AS p_bulan, p.tahun AS p_tahun
|
||||||
|
FROM ulasan u
|
||||||
|
JOIN periode_analisis p ON u.periode_id = p.id
|
||||||
|
WHERE YEAR(u.tanggal) = %s
|
||||||
|
AND (
|
||||||
|
MONTH(u.tanggal) != p.bulan
|
||||||
|
OR YEAR(u.tanggal) != p.tahun
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
cursor.execute(sql_fetch, (tahun,))
|
||||||
|
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
log("INFO", f"Ditemukan {len(rows)} ulasan {tahun} dengan periode tidak sesuai tanggal.")
|
||||||
|
|
||||||
|
if not rows:
|
||||||
|
log("OK", "Tidak ada data yang perlu dipindahkan.")
|
||||||
|
conn.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
# ── 2. Kelompokkan per bulan ─────────────────────────────────────────────
|
||||||
|
from collections import defaultdict
|
||||||
|
grouped = defaultdict(list) # (bulan, tahun) → [id, ...]
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
ulasan_id, tanggal, wisata, periode_id, p_bulan, p_tahun = row
|
||||||
|
try:
|
||||||
|
if isinstance(tanggal, str):
|
||||||
|
dt = datetime.strptime(tanggal[:10], "%Y-%m-%d")
|
||||||
|
else:
|
||||||
|
dt = tanggal
|
||||||
|
grouped[(dt.month, dt.year)].append(ulasan_id)
|
||||||
|
except Exception as e:
|
||||||
|
log("WARNING", f"Skip id={ulasan_id} tanggal='{tanggal}': {e}")
|
||||||
|
|
||||||
|
# ── 3. Preview ───────────────────────────────────────────────────────────
|
||||||
|
log("INFO", "Rencana pemindahan:")
|
||||||
|
for (bulan, thn), ids in sorted(grouped.items()):
|
||||||
|
nama = f"{NAMA_BULAN[bulan - 1]} {thn}"
|
||||||
|
log("INFO", f" → {nama}: {len(ids)} ulasan")
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
log("DRY-RUN", "Selesai preview. Jalankan tanpa --dry-run untuk terapkan perubahan.")
|
||||||
|
conn.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
# ── 4. Terapkan pemindahan ───────────────────────────────────────────────
|
||||||
|
total_moved = 0
|
||||||
|
for (bulan, thn), ids in sorted(grouped.items()):
|
||||||
|
periode_id = get_or_create_periode(cursor, conn, bulan, thn, dry_run=False)
|
||||||
|
if periode_id is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Update per batch
|
||||||
|
placeholders = ",".join(["%s"] * len(ids))
|
||||||
|
if is_sqlite_connection(conn):
|
||||||
|
placeholders = ",".join(["?"] * len(ids))
|
||||||
|
|
||||||
|
cursor.execute(
|
||||||
|
f"UPDATE ulasan SET periode_id = %s, updated_at = NOW() WHERE id IN ({placeholders})".replace(
|
||||||
|
"%s", "?" if is_sqlite_connection(conn) else "%s"
|
||||||
|
),
|
||||||
|
[periode_id] + ids,
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
nama = f"{NAMA_BULAN[bulan - 1]} {thn}"
|
||||||
|
log("OK", f"{nama}: {len(ids)} ulasan dipindahkan ke periode_id={periode_id}")
|
||||||
|
total_moved += len(ids)
|
||||||
|
|
||||||
|
log("OK", f"Selesai. Total {total_moved} ulasan berhasil dipindahkan ke periode yang benar.")
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -114,7 +114,7 @@ def db_config():
|
||||||
"connection": connection,
|
"connection": connection,
|
||||||
"host": env_value(env, "DB_HOST", "127.0.0.1"),
|
"host": env_value(env, "DB_HOST", "127.0.0.1"),
|
||||||
"port": int(env_value(env, "DB_PORT", "3306")),
|
"port": int(env_value(env, "DB_PORT", "3306")),
|
||||||
"database": env_value(env, "DB_DATABASE", "sentara"),
|
"database": env_value(env, "DB_DATABASE", "analisis_sentimen"),
|
||||||
"user": env_value(env, "DB_USERNAME", "root"),
|
"user": env_value(env, "DB_USERNAME", "root"),
|
||||||
"password": env_value(env, "DB_PASSWORD", ""),
|
"password": env_value(env, "DB_PASSWORD", ""),
|
||||||
}
|
}
|
||||||
|
|
@ -277,10 +277,10 @@ def get_or_create_period(cursor, conn, start_date, end_date):
|
||||||
|
|
||||||
|
|
||||||
DESTINATIONS = {
|
DESTINATIONS = {
|
||||||
"Pantai Papuma": "https://www.google.com/maps/search/?api=1&query=Pantai%20Papuma%20Jember",
|
"Pantai Papuma": "https://www.google.com/maps/place/Pantai+Papuma/@-8.4310054,113.5508204,16z/data=!4m7!3m6!1s0x2dd682a6a4b5cd8d:0xb9c242f3a09e2d2e!4b1!8m2!3d-8.4300871!4d113.5536464!16s%2Fg%2F11bwy_gg5k?authuser=0&entry=ttu&g_ep=EgoyMDI2MDUxMy4wIKXMDSoASAFQAw%3D%3D",
|
||||||
"Pantai Watu Ulo": "https://www.google.com/maps/search/?api=1&query=Pantai%20Watu%20Ulo%20Jember",
|
"Pantai Watu Ulo": "https://www.google.com/maps/place/Pantai+Watu+Ulo/@-8.4252967,113.5515972,15z/data=!4m8!3m7!1s0x2dd69d2fffffffff:0x6f3d7097accf3209!8m2!3d-8.425297!4d113.561897!9m1!1b1!16s%2Fg%2F120m19h0?authuser=0&entry=ttu&g_ep=EgoyMDI2MDUxMy4wIKXMDSoASAFQAw%3D%3D",
|
||||||
"Teluk Love": "https://www.google.com/maps/search/?api=1&query=Teluk%20Love%20Jember",
|
"Teluk Love": "https://www.google.com/maps/place/Teluk+Love/@-8.4410549,113.581323,17z/data=!3m1!4b1!4m6!3m5!1s0x2dd6942520c45715:0x3cabb1f5dd90a01b!8m2!3d-8.4410549!4d113.5838979!16s%2Fg%2F11ckkqq_1b?authuser=0&entry=ttu&g_ep=EgoyMDI2MDUxMy4wIKXMDSoASAFQAw%3D%3D",
|
||||||
"Kebun Teh Gunung Gambir": "https://www.google.com/maps/search/?api=1&query=Kebun%20Teh%20Gunung%20Gambir%20Jember",
|
"Kebun Teh Gunung Gambir": "https://www.google.com/maps/place/Wisata+kebun+teh+gunung+gambir/@-8.0351906,113.4389961,17z/data=!4m7!3m6!1s0x2dd6f551f90c0c8f:0x3d1a62be1e269d12!4b1!8m2!3d-8.0351906!4d113.441571!16s%2Fg%2F11tjmzk404?authuser=0&entry=ttu&g_ep=EgoyMDI2MDUxMy4wIKXMDSoASAFQAw%3D%3D",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -303,6 +303,7 @@ def selected_destinations(wisata_name):
|
||||||
|
|
||||||
def build_chrome_options(config):
|
def build_chrome_options(config):
|
||||||
from selenium.webdriver.chrome.options import Options
|
from selenium.webdriver.chrome.options import Options
|
||||||
|
import tempfile
|
||||||
|
|
||||||
options = Options()
|
options = Options()
|
||||||
# options = uc.ChromeOptions()
|
# options = uc.ChromeOptions()
|
||||||
|
|
@ -310,7 +311,6 @@ def build_chrome_options(config):
|
||||||
|
|
||||||
options.add_argument("--lang=id")
|
options.add_argument("--lang=id")
|
||||||
options.add_argument("--accept-language=id-ID,id")
|
options.add_argument("--accept-language=id-ID,id")
|
||||||
options.add_argument("--window-size=1366,900")
|
|
||||||
options.add_argument("--disable-blink-features=AutomationControlled")
|
options.add_argument("--disable-blink-features=AutomationControlled")
|
||||||
options.add_argument("--disable-dev-shm-usage")
|
options.add_argument("--disable-dev-shm-usage")
|
||||||
options.add_argument("--no-sandbox")
|
options.add_argument("--no-sandbox")
|
||||||
|
|
@ -326,8 +326,12 @@ def build_chrome_options(config):
|
||||||
|
|
||||||
options.add_argument(f"--user-data-dir={tempfile.mkdtemp()}")
|
options.add_argument(f"--user-data-dir={tempfile.mkdtemp()}")
|
||||||
|
|
||||||
if config["headless"]:
|
# User agent supaya dianggap browser asli
|
||||||
|
options.add_argument("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
|
||||||
|
|
||||||
|
if config.get("headless"):
|
||||||
options.add_argument("--headless=new")
|
options.add_argument("--headless=new")
|
||||||
|
options.add_argument("--window-size=1920,1080")
|
||||||
else:
|
else:
|
||||||
options.add_argument("--start-maximized")
|
options.add_argument("--start-maximized")
|
||||||
|
|
||||||
|
|
@ -405,7 +409,7 @@ def click_first_search_result_if_needed(driver):
|
||||||
for result in results:
|
for result in results:
|
||||||
if result.is_displayed():
|
if result.is_displayed():
|
||||||
driver.execute_script("arguments[0].click();", result)
|
driver.execute_script("arguments[0].click();", result)
|
||||||
time.sleep(5)
|
time.sleep(10)
|
||||||
log("INFO", "Hasil pencarian Google Maps pertama dibuka.")
|
log("INFO", "Hasil pencarian Google Maps pertama dibuka.")
|
||||||
return True
|
return True
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
|
@ -669,22 +673,31 @@ def click_reviews_tab(driver, wait, wisata, manual_login_timeout=0):
|
||||||
from selenium.webdriver.common.by import By
|
from selenium.webdriver.common.by import By
|
||||||
from selenium.webdriver.support import expected_conditions as EC
|
from selenium.webdriver.support import expected_conditions as EC
|
||||||
|
|
||||||
candidates = [
|
# candidates = [
|
||||||
"//div[@role='tab'][contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'ulasan')]",
|
# "//div[@role='tab'][contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'ulasan')]",
|
||||||
"//div[@role='tab'][contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'review')]",
|
# "//div[@role='tab'][contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'review')]",
|
||||||
"//button[@role='tab'][contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'ulasan')]",
|
# "//button[@role='tab'][contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'ulasan')]",
|
||||||
"//button[@role='tab'][contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'review')]",
|
# "//button[@role='tab'][contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'review')]",
|
||||||
"//button[contains(translate(@aria-label, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'ulasan') and not(contains(translate(@aria-label, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'tulis')) and not(contains(translate(@aria-label, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'write'))]",
|
# "//button[contains(translate(@aria-label, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'ulasan') and not(contains(translate(@aria-label, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'tulis')) and not(contains(translate(@aria-label, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'write'))]",
|
||||||
"//button[contains(translate(@aria-label, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'review') and not(contains(translate(@aria-label, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'write'))]",
|
# "//button[contains(translate(@aria-label, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'review') and not(contains(translate(@aria-label, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'write'))]",
|
||||||
|
# ]
|
||||||
|
|
||||||
|
possible_xpaths = [
|
||||||
|
"//button[contains(@aria-label, 'Ulasan untuk')]",
|
||||||
|
"//button[contains(., 'Ulasan')]",
|
||||||
|
"//div[@role='tab'][contains(., 'Ulasan')]",
|
||||||
|
"//button[contains(@aria-label, 'Reviews')]"
|
||||||
]
|
]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
wait.until(EC.presence_of_all_elements_located((By.XPATH, "//button | //div[@role='tab']")))
|
# wait.until(EC.presence_of_all_elements_located((By.XPATH, "//button | //div[@role='tab']")))
|
||||||
|
all_btns = driver.find_elements(By.XPATH, "//button | //div[@role='tab']")
|
||||||
|
time.sleep(3)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def try_click_reviews_tab():
|
def try_click_reviews_tab():
|
||||||
for xpath in candidates:
|
for xpath in possible_xpaths:
|
||||||
try:
|
try:
|
||||||
elements = driver.find_elements(By.XPATH, xpath)
|
elements = driver.find_elements(By.XPATH, xpath)
|
||||||
for element in elements:
|
for element in elements:
|
||||||
|
|
@ -739,6 +752,12 @@ def find_reviews_scroll_container(driver, wait):
|
||||||
"//div[.//div[@data-review-id]]",
|
"//div[.//div[@data-review-id]]",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# xpaths = [
|
||||||
|
# "//div[contains(@class, 'm6U624-v79jQ-le0U9b')]", # Class kontainer ulasan umum
|
||||||
|
# "//div[@role='main' and contains(@aria-label, 'Ulasan')]",
|
||||||
|
# "//div[contains(@aria-label, 'Ulasan untuk')]"
|
||||||
|
# ]
|
||||||
|
|
||||||
for xpath in xpaths:
|
for xpath in xpaths:
|
||||||
try:
|
try:
|
||||||
return wait.until(EC.presence_of_element_located((By.XPATH, xpath)))
|
return wait.until(EC.presence_of_element_located((By.XPATH, xpath)))
|
||||||
|
|
@ -772,8 +791,10 @@ def scrape_with_selenium(cursor, conn, periode_id, start_date, end_date, config,
|
||||||
# options=build_chrome_options(config)
|
# options=build_chrome_options(config)
|
||||||
# )
|
# )
|
||||||
|
|
||||||
|
|
||||||
service = Service(r"C:\Users\Mufrida Farah\Downloads\chromedriver-win64\chromedriver-win64\chromedriver.exe")
|
service = Service(r"C:\Users\Mufrida Farah\Downloads\chromedriver-win64\chromedriver-win64\chromedriver.exe")
|
||||||
driver = webdriver.Chrome(service=service, options=build_chrome_options(config))
|
opts = build_chrome_options(config)
|
||||||
|
driver = webdriver.Chrome(service=service, options=opts)
|
||||||
|
|
||||||
total_saved = 0
|
total_saved = 0
|
||||||
total_skipped_duplicate = 0
|
total_skipped_duplicate = 0
|
||||||
|
|
@ -826,6 +847,7 @@ def scrape_with_selenium(cursor, conn, periode_id, start_date, end_date, config,
|
||||||
|
|
||||||
last_height = 0
|
last_height = 0
|
||||||
|
|
||||||
|
driver.execute_script("arguments[0].focus();", scrollable_div)
|
||||||
for i in range(config["scroll_limit"]):
|
for i in range(config["scroll_limit"]):
|
||||||
try:
|
try:
|
||||||
driver.execute_script("arguments[0].scrollTop = arguments[0].scrollHeight", scrollable_div)
|
driver.execute_script("arguments[0].scrollTop = arguments[0].scrollHeight", scrollable_div)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue