pindah periode

This commit is contained in:
MufridaFaraDiani27 2026-05-21 11:00:59 +07:00
parent 23d615c570
commit f24f8652bf
10 changed files with 315 additions and 155 deletions

View File

@ -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')

View File

@ -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)

View File

@ -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

View File

@ -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.');
}
}

View File

@ -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">

View File

@ -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>

View File

@ -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 () {

View File

@ -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()

View File

@ -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)