pindah periode
This commit is contained in:
parent
23d615c570
commit
f24f8652bf
|
|
@ -158,15 +158,28 @@ public function index(Request $request)
|
|||
// ================================================================
|
||||
// CHART DATA
|
||||
// ================================================================
|
||||
$chartSentimen = $hasAnalisis
|
||||
? 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();
|
||||
// $chartSentimen = $hasAnalisis
|
||||
// ? 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'))
|
||||
->when($periodeId, fn($q) => $q->where('periode_id', $periodeId))
|
||||
->when($wisata && $wisata != 'Semua Destinasi', fn($q) => $q->where('wisata', $wisata))
|
||||
->groupBy('sentimen')
|
||||
->pluck('total', 'sentimen');
|
||||
|
||||
$chartSentimen = collect([
|
||||
['sentimen' => 'positif', 'total' => $rawSentimen['positif'] ?? 0],
|
||||
['sentimen' => 'negatif', 'total' => $rawSentimen['negatif'] ?? 0],
|
||||
['sentimen' => 'netral', 'total' => $rawSentimen['netral'] ?? 0],
|
||||
]);
|
||||
|
||||
$chartDestinasi = $hasAnalisis
|
||||
? DB::table('hasil_analisis')
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ class RekomendasiController extends Controller
|
|||
'tip' => 'Tambah tempat sampah & jadwal pembersihan rutin.',
|
||||
],
|
||||
],
|
||||
'Parkir' => [
|
||||
'Aksesibilitas' => [
|
||||
'keywords' => ['parkir', 'lahan parkir', 'tempat parkir', 'parkiran', 'motor', 'mobil'],
|
||||
'color' => 'orange',
|
||||
'icon' => '🚗',
|
||||
|
|
@ -52,16 +52,7 @@ class RekomendasiController extends Controller
|
|||
'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)
|
||||
|
|
|
|||
|
|
@ -24,13 +24,15 @@ public function index(Request $request)
|
|||
->from('hasil_analisis as h')
|
||||
->whereColumn('h.periode_id', 'p.id');
|
||||
})
|
||||
->orderBy('p.id', 'desc')
|
||||
->orderBy('p.tahun', 'desc')
|
||||
->orderBy('p.bulan', 'desc')
|
||||
// ->orderBy('p.id', 'desc')
|
||||
->select('p.*')
|
||||
->get();
|
||||
|
||||
$availablePeriodeIds = $periodeList->pluck('id')->map(fn($id) => (string) $id);
|
||||
$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')
|
||||
&& $availablePeriodeIds->contains($requestedPeriodeId)
|
||||
? $requestedPeriodeId
|
||||
|
|
|
|||
|
|
@ -110,14 +110,14 @@ public function store(Request $request)
|
|||
'password' => 'required|min:8|confirmed',
|
||||
]);
|
||||
|
||||
User::create([
|
||||
User::create([
|
||||
'name' => $request->name,
|
||||
'email' => $request->email,
|
||||
'role' => $request->role,
|
||||
'password' => bcrypt($request->password),
|
||||
]);
|
||||
|
||||
return redirect()->route('kelola-pengguna.index')
|
||||
return redirect()->route('kelola-pengguna.index')
|
||||
->with('success', 'Pengguna berhasil ditambahkan.');
|
||||
}
|
||||
}
|
||||
|
|
@ -59,85 +59,7 @@
|
|||
|
||||
</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 ================= --}}
|
||||
<form id="filter-form" class="flex items-center gap-3">
|
||||
|
|
|
|||
|
|
@ -288,6 +288,19 @@
|
|||
<img src="{{ asset('images/logo-sentara.png') }}">
|
||||
</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">
|
||||
Selamat Datang <span>Kembali!</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
@if(session('success'))
|
||||
<div class="bg-green-100 border-l-4 border-green-500 text-green-700 p-4 mb-6 rounded">
|
||||
{{ session('success') }}
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@if(session('error'))
|
||||
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mb-6 rounded whitespace-pre-line">
|
||||
|
|
@ -63,11 +63,14 @@ class="border rounded-lg px-3 py-2 text-sm bg-white shadow-sm min-w-[150px]">
|
|||
</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 -->
|
||||
<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="flex-1 py-3 rounded-lg text-sm font-medium transition">
|
||||
Dashboard Sentara
|
||||
|
|
@ -167,19 +170,31 @@ class="flex-1 py-3 rounded-lg text-sm font-medium transition">
|
|||
{{-- SCRIPTS --}}
|
||||
<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
|
||||
const pieData = JSON.parse('{!! json_encode($chartSentimen) !!}');
|
||||
const pieCanvas = document.getElementById('pieChart');
|
||||
if (window.pieChartInstance) window.pieChartInstance.destroy();
|
||||
window.pieChartInstance = new Chart(pieCanvas, {
|
||||
const pieCanvas = document.getElementById('pieChart');
|
||||
if (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',
|
||||
data: {
|
||||
labels: pieData.map(i => i.sentimen),
|
||||
datasets: [{
|
||||
data: pieData.map(i => i.total),
|
||||
data: pieData.map(i => Number(i.total)),
|
||||
backgroundColor: ['#3B82F6', '#EF4444', '#F59E0B']
|
||||
}]
|
||||
},
|
||||
|
|
@ -189,12 +204,15 @@ class="flex-1 py-3 rounded-lg text-sm font-medium transition">
|
|||
animation: false
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// BAR CHART SENTIMEN PER DESTINASI
|
||||
const destinationData = @json($chartDestinasi);
|
||||
// BAR CHART
|
||||
const destinationCanvas = document.getElementById('destinationChart');
|
||||
if (destinationCanvas) {
|
||||
if (window.destinationChartInstance) window.destinationChartInstance.destroy();
|
||||
if (window.destinationChartInstance) {
|
||||
window.destinationChartInstance.destroy();
|
||||
window.destinationChartInstance = null;
|
||||
}
|
||||
window.destinationChartInstance = new Chart(destinationCanvas, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
|
|
@ -234,12 +252,7 @@ class="flex-1 py-3 rounded-lg text-sm font-medium transition">
|
|||
legend: {
|
||||
position: 'top',
|
||||
align: 'center',
|
||||
labels: {
|
||||
boxWidth: 38,
|
||||
boxHeight: 10,
|
||||
color: '#4B5563',
|
||||
font: { size: 12 },
|
||||
},
|
||||
labels: { boxWidth: 38, boxHeight: 10, color: '#4B5563', font: { size: 12 } },
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
|
|
@ -250,19 +263,11 @@ class="flex-1 py-3 rounded-lg text-sm font-medium transition">
|
|||
scales: {
|
||||
x: {
|
||||
grid: { color: 'rgba(156, 163, 175, 0.22)' },
|
||||
ticks: {
|
||||
color: '#4B5563',
|
||||
maxRotation: 14,
|
||||
minRotation: 14,
|
||||
font: { size: 12 },
|
||||
},
|
||||
ticks: { color: '#4B5563', maxRotation: 14, minRotation: 14, font: { size: 12 } },
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
color: '#4B5563',
|
||||
precision: 0,
|
||||
},
|
||||
ticks: { color: '#4B5563', precision: 0 },
|
||||
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.
|
||||
const text = @json($allText);
|
||||
// WORD CLOUD
|
||||
const wordCloud = document.getElementById('wordCloud');
|
||||
const stopwords = new Set([
|
||||
'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('');
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<script>
|
||||
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,
|
||||
"host": env_value(env, "DB_HOST", "127.0.0.1"),
|
||||
"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"),
|
||||
"password": env_value(env, "DB_PASSWORD", ""),
|
||||
}
|
||||
|
|
@ -277,10 +277,10 @@ def get_or_create_period(cursor, conn, start_date, end_date):
|
|||
|
||||
|
||||
DESTINATIONS = {
|
||||
"Pantai Papuma": "https://www.google.com/maps/search/?api=1&query=Pantai%20Papuma%20Jember",
|
||||
"Pantai Watu Ulo": "https://www.google.com/maps/search/?api=1&query=Pantai%20Watu%20Ulo%20Jember",
|
||||
"Teluk Love": "https://www.google.com/maps/search/?api=1&query=Teluk%20Love%20Jember",
|
||||
"Kebun Teh Gunung Gambir": "https://www.google.com/maps/search/?api=1&query=Kebun%20Teh%20Gunung%20Gambir%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/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/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/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):
|
||||
from selenium.webdriver.chrome.options import Options
|
||||
import tempfile
|
||||
|
||||
options = Options()
|
||||
# options = uc.ChromeOptions()
|
||||
|
|
@ -310,7 +311,6 @@ def build_chrome_options(config):
|
|||
|
||||
options.add_argument("--lang=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-dev-shm-usage")
|
||||
options.add_argument("--no-sandbox")
|
||||
|
|
@ -326,8 +326,12 @@ def build_chrome_options(config):
|
|||
|
||||
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("--window-size=1920,1080")
|
||||
else:
|
||||
options.add_argument("--start-maximized")
|
||||
|
||||
|
|
@ -405,7 +409,7 @@ def click_first_search_result_if_needed(driver):
|
|||
for result in results:
|
||||
if result.is_displayed():
|
||||
driver.execute_script("arguments[0].click();", result)
|
||||
time.sleep(5)
|
||||
time.sleep(10)
|
||||
log("INFO", "Hasil pencarian Google Maps pertama dibuka.")
|
||||
return True
|
||||
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.support import expected_conditions as EC
|
||||
|
||||
candidates = [
|
||||
"//div[@role='tab'][contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'ulasan')]",
|
||||
"//div[@role='tab'][contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'review')]",
|
||||
"//button[@role='tab'][contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'ulasan')]",
|
||||
"//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'), 'review') and not(contains(translate(@aria-label, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'write'))]",
|
||||
]
|
||||
# candidates = [
|
||||
# "//div[@role='tab'][contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'ulasan')]",
|
||||
# "//div[@role='tab'][contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'review')]",
|
||||
# "//button[@role='tab'][contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'ulasan')]",
|
||||
# "//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'), '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:
|
||||
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:
|
||||
pass
|
||||
|
||||
def try_click_reviews_tab():
|
||||
for xpath in candidates:
|
||||
for xpath in possible_xpaths:
|
||||
try:
|
||||
elements = driver.find_elements(By.XPATH, xpath)
|
||||
for element in elements:
|
||||
|
|
@ -739,6 +752,12 @@ def find_reviews_scroll_container(driver, wait):
|
|||
"//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:
|
||||
try:
|
||||
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)
|
||||
# )
|
||||
|
||||
|
||||
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_skipped_duplicate = 0
|
||||
|
|
@ -826,6 +847,7 @@ def scrape_with_selenium(cursor, conn, periode_id, start_date, end_date, config,
|
|||
|
||||
last_height = 0
|
||||
|
||||
driver.execute_script("arguments[0].focus();", scrollable_div)
|
||||
for i in range(config["scroll_limit"]):
|
||||
try:
|
||||
driver.execute_script("arguments[0].scrollTop = arguments[0].scrollHeight", scrollable_div)
|
||||
|
|
|
|||
Loading…
Reference in New Issue