update fitur

This commit is contained in:
MufridaFaraDiani27 2026-03-13 22:40:08 +07:00
parent 591962379a
commit 5fa8005eb1
14 changed files with 507 additions and 253 deletions

View File

@ -14,7 +14,8 @@ public function index()
'negatif_persen' => 20, 'negatif_persen' => 20,
'netral_persen' => 12, 'netral_persen' => 12,
]; ];
$lastUpdate = '27 Februari 2026, 22:45';
return view('dashboard', compact('stats')); return view('dashboard', compact('stats', 'lastUpdate'));
} }
} }

View File

@ -31,18 +31,31 @@ public function index()
// Tombol ambil data terbaru // Tombol ambil data terbaru
public function ambilData() public function ambilData()
{ {
// nanti scraping otomatis disini (sementara dummy) $pythonPath = '"C:\\Users\\Mufrida Farah\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"';
return redirect()->route('ulasan.index'); $scriptPath = base_path('scraper/scraping_pipeline.py');
}
$command = $pythonPath . " " . $scriptPath . " 2>&1";
shell_exec($command);
return redirect()->route('ulasan.index')
->with('success', 'Data berhasil diambil dari Google Maps');
}
// Tombol analisis data // Tombol analisis data
public function analisisData() public function analisisData()
{ {
// jalankan python naive bayes $pythonPath = '"C:\\Users\\Mufrida Farah\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"';
shell_exec("python sentimen.py");
return redirect()->route('ulasan.index'); $scriptPath = base_path('scraper/preprocessing.py');
}
$command = $pythonPath . " " . $scriptPath . " 2>&1";
shell_exec($command);
return redirect()->route('ulasan.index')
->with('success','Preprocessing berhasil dilakukan');
}
} }

View File

@ -9,11 +9,11 @@ class Ulasan extends Model
{ {
use HasFactory; use HasFactory;
protected $table = 'ulasan_mentah'; protected $table = 'ulasan';
protected $fillable = [ protected $fillable = [
'nama_wisata', 'nama_wisata',
'reviewer_name', 'reviewer',
'rating', 'rating',
'ulasan', 'ulasan',
'tanggal', 'tanggal',

View File

@ -18,7 +18,7 @@ public function up(): void
$table->integer('rating')->nullable(); $table->integer('rating')->nullable();
$table->text('ulasan'); $table->text('ulasan');
$table->string('tanggal')->nullable(); $table->string('tanggal')->nullable();
$table->datetime('scraping_date')->nullable(); $table->datetime('gununggambir_date')->nullable();
$table->boolean('is_processed')->default(0); $table->boolean('is_processed')->default(0);
$table->timestamps(); $table->timestamps();
}); });

View File

@ -43,8 +43,8 @@ document.addEventListener("DOMContentLoaded", function () {
labels: [ labels: [
"Papuma", "Papuma",
"Watu Ulo", "Watu Ulo",
"Sukorambi", "teluklove",
"Air Terjun Tancak Tulis", "Kebun Teh Gunung Gambir ",
], ],
datasets: [ datasets: [
{ {

View File

@ -81,7 +81,7 @@ class="slide hidden absolute inset-0 w-full h-full object-cover">
<img src="{{ asset('images/botani.jpg') }}" <img src="{{ asset('images/botani.jpg') }}"
class="slide hidden absolute inset-0 w-full h-full object-cover"> class="slide hidden absolute inset-0 w-full h-full object-cover">
<img src="{{ asset('images/tancak.jpeg') }}" <img src="{{ asset('images/gununggambir.jpeg') }}"
class="slide hidden absolute inset-0 w-full h-full object-cover"> class="slide hidden absolute inset-0 w-full h-full object-cover">

View File

@ -100,7 +100,7 @@ class="slide hidden absolute inset-0 w-full h-full object-cover">
<img src="{{ asset('images/botani.jpg') }}" <img src="{{ asset('images/botani.jpg') }}"
class="slide hidden absolute inset-0 w-full h-full object-cover"> class="slide hidden absolute inset-0 w-full h-full object-cover">
<img src="{{ asset('images/tancak.jpeg') }}" <img src="{{ asset('images/gununggambir.jpeg') }}"
class="slide hidden absolute inset-0 w-full h-full object-cover"> class="slide hidden absolute inset-0 w-full h-full object-cover">
<!-- Overlay --> <!-- Overlay -->

View File

@ -5,20 +5,35 @@
<div class="max-w-7xl mx-auto"> <div class="max-w-7xl mx-auto">
{{-- Header --}} {{-- Header --}}
<div class="flex justify-between items-center mb-8"> <div class="flex justify-between items-start mb-8">
<div>
<h2 class="text-2xl font-semibold text-gray-800"> <h2 class="text-2xl font-semibold text-gray-800">
Dashboard Dashboard
</h2> </h2>
<!-- <select <!-- <p class="text-sm text-gray-500 mt-1">
Terakhir diperbarui: 27 Februari 2026, 22:45
</p>
<p class="text-xs text-gray-400">
Periode Data: Februari 2026
</p> -->
</div>
<!-- Filter Destinasi -->
<form method="GET" action="" class="ml-4">
<select name="destinasi"
class="bg-white border border-gray-300 text-sm rounded-lg px-4 py-2 shadow-sm class="bg-white border border-gray-300 text-sm rounded-lg px-4 py-2 shadow-sm
focus:ring-2 focus:ring-blue-400 focus:outline-none"> focus:ring-2 focus:ring-blue-400 focus:outline-none">
<option>Semua Destinasi</option>
<option>Pantai Papuma</option> <option value="">Semua Destinasi</option>
<option>Pantai Watu Ulo</option> <option value="papuma">Pantai Papuma</option>
<option>Taman Botani Sukorambi</option> <option value="watuulo">Pantai Watu Ulo</option>
<option>Air Terjun Tancak Tulis</option> <option value="teluklove">Teluk Love (Payangan)</option>
</select> --> <option value="gununggambir">Kebun Teh Gunung Gambir </option>
</select>
</form>
</div> </div>
{{-- Statistik Cards --}} {{-- Statistik Cards --}}
@ -55,14 +70,13 @@ class="bg-white border border-gray-300 text-sm rounded-lg px-4 py-2 shadow-sm
</div> </div>
{{-- Chart Section --}} {{-- Chart Section --}}
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6"> <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
<!-- Pie Chart --> <!-- Pie Chart -->
<div class="bg-white rounded-xl shadow p-6"> <div class="bg-white rounded-xl shadow p-6">
<h3 class="font-semibold mb-4 text-gray-700"> <h3 class="font-semibold mb-4 text-gray-700">
Distribusi Sentimen Distribusi Sentimen
</h3> </h3>
<div class="h-[280px] flex justify-center items-center"> <div class="h-[280px] flex justify-center items-center">
<canvas id="pieChart"></canvas> <canvas id="pieChart"></canvas>
</div> </div>
@ -73,12 +87,60 @@ class="bg-white border border-gray-300 text-sm rounded-lg px-4 py-2 shadow-sm
<h3 class="font-semibold mb-4 text-gray-700"> <h3 class="font-semibold mb-4 text-gray-700">
Sentimen per Destinasi Sentimen per Destinasi
</h3> </h3>
<div class="h-[280px]"> <div class="h-[280px]">
<canvas id="barChart"></canvas> <canvas id="barChart"></canvas>
</div> </div>
</div> </div>
</div> {{-- TUTUP GRID CHART --}}
{{-- Section Bawah --}}
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Total Data per Destinasi -->
<div class="bg-white rounded-xl shadow p-6 h-[320px]">
<h3 class="font-semibold mb-6 text-gray-700">
Total Data per Destinasi
</h3>
<div class="space-y-4 text-sm text-gray-600">
<div class="flex justify-between border-b pb-3">
<span>Pantai Papuma</span>
<span class="font-semibold text-gray-800">120 ulasan</span>
</div>
<div class="flex justify-between border-b pb-3">
<span>Pantai Watu Ulo</span>
<span class="font-semibold text-gray-800">85 ulasan</span>
</div>
<div class="flex justify-between border-b pb-3">
<span>Teluk Love (Payangan)</span>
<span class="font-semibold text-gray-800">95 ulasan</span>
</div>
<div class="flex justify-between">
<span>Kebun Teh Gunung Gambir</span>
<span class="font-semibold text-gray-800">60 ulasan</span>
</div>
</div>
</div>
<!-- Tren Sentimen per Bulan -->
<!-- <div class="bg-white rounded-xl shadow p-6 h-[320px]">
<h3 class="font-semibold mb-6 text-gray-700">
Tren Sentimen per Bulan
</h3>
<div class="h-[220px] flex items-center justify-center text-gray-400">
Grafik tren akan ditampilkan setelah data periode tersedia.
</div>
</div> -->
</div>
</div>
</div>
</div> </div>

View File

@ -1,153 +0,0 @@
@extends('layouts.sentara')
@section('content')
<div class="max-w-7xl mx-auto space-y-8">
{{-- HEADER --}}
<div class="flex justify-between items-center">
<h2 class="text-2xl font-bold text-gray-800">
Data Ulasan Mentah
</h2>
<select
class="bg-white border border-gray-300 text-sm rounded-lg px-4 py-2 shadow-sm
focus:ring-2 focus:ring-blue-400 focus:outline-none">
<option>Semua Destinasi</option>
<option>Pantai Papuma</option>
<option>Sukorambi</option>
<option>Watu Ulo</option>
</select>
</div>
{{-- BUTTON SCRAPING --}}
<div>
<a href="{{ route('scrape.ulasan') }}"
class="bg-blue-700 hover:bg-blue-800 text-white px-6 py-2 rounded-lg shadow">
Ambil Data Terbaru
</a>
</div>
{{-- DATA MENTAH TABLE --}}
<div class="bg-white rounded-xl shadow p-6">
<h3 class="font-semibold text-gray-700 mb-4">
Hasil Data Scraping
</h3>
<div class="overflow-x-auto">
<table class="min-w-full text-sm text-left">
<thead>
<tr class="bg-gray-100 text-gray-600">
<th class="px-4 py-3">Destinasi</th>
<th class="px-4 py-3">Reviewer</th>
<th class="px-4 py-3">Ulasan</th>
<th class="px-4 py-3">Tanggal</th>
</tr>
</thead>
<tbody>
@forelse($ulasan as $item)
<tr class="border-b">
<td class="px-4 py-3 font-medium">
{{ $item->nama_wisata }}
</td>
<td class="px-4 py-3">
{{ $item->reviewer_name }}
</td>
<td class="px-4 py-3 text-gray-700">
{{ Str::limit($item->ulasan, 80) }}
</td>
<td class="px-4 py-3">
{{ $item->tanggal }}
</td>
</tr>
@empty
<tr>
<td colspan="4" class="text-center py-6 text-gray-500">
Data belum tersedia. Silakan scraping dulu.
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
{{-- BUTTON ANALISIS --}}
<!-- <div>
<form action="{{ route('analisis.ulasan') }}" method="POST">
@csrf
<button type="submit"
class="bg-green-600 hover:bg-green-700 text-white px-6 py-2 rounded-lg shadow">
Analisis Data
</button>
</form>
</div> -->
{{-- HASIL ANALISIS --}}
@if(isset($hasilAnalisis))
<!-- <div class="bg-white rounded-xl shadow p-6">
<h3 class="font-semibold text-gray-700 mb-4">
Hasil Analisis Sentimen
</h3>
<div class="overflow-x-auto">
<table class="min-w-full text-sm text-left">
<thead>
<tr class="bg-gray-100 text-gray-600">
<th class="px-4 py-3">Destinasi</th>
<th class="px-4 py-3">Ulasan</th>
<th class="px-4 py-3">Sentimen</th>
</tr>
</thead>
<tbody>
@foreach($hasilAnalisis as $hasil)
<tr class="border-b">
<td class="px-4 py-3 font-medium">
{{ $hasil->nama_wisata }}
</td>
<td class="px-4 py-3">
{{ Str::limit($hasil->ulasan, 80) }}
</td>
<td class="px-4 py-3">
@if($hasil->sentimen == "positif")
<span class="px-3 py-1 rounded-full bg-green-100 text-green-700 text-xs font-semibold">
Positif
</span>
@elseif($hasil->sentimen == "negatif")
<span class="px-3 py-1 rounded-full bg-red-100 text-red-700 text-xs font-semibold">
Negatif
</span>
@else
<span class="px-3 py-1 rounded-full bg-yellow-100 text-yellow-700 text-xs font-semibold">
Netral
</span>
@endif
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div> -->
@endif
</div>
@endsection

View File

@ -30,40 +30,60 @@ class="bg-white border border-gray-300 text-sm rounded-lg px-4 py-2 shadow-sm
</div> </div>
<!-- CARD GRID --> <!-- CARD GRID -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6"> <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<!-- ISU UTAMA --> <!-- Isu Utama -->
<div class="bg-white rounded-xl shadow p-6 text-center"> <div class="bg-white rounded-xl shadow p-6">
<h3 class="text-gray-600 font-semibold mb-2"> <h3 class="text-gray-700 font-semibold mb-4">
Isu Utama: Isu Utama
</h3> </h3>
<p class="text-lg font-bold text-gray-800">
{{ $isuUtama }} <div class="text-sm text-gray-600 space-y-2">
</p> <span class="inline-block bg-red-100 text-red-700 px-3 py-1 rounded-full text-xs font-medium">
Kebersihan
</span>
<span class="inline-block bg-red-100 text-red-700 px-3 py-1 rounded-full text-xs font-medium">
Parkir
</span>
</div> </div>
</div>
<!-- KEYWORD --> <!-- Kata Kunci Dominan -->
<div class="bg-white rounded-xl shadow p-6 text-center"> <div class="bg-white rounded-xl shadow p-6">
<h3 class="text-gray-600 font-semibold mb-2"> <h3 class="text-gray-700 font-semibold mb-4">
Kata Kunci Dominan: Kata Kunci Dominan
</h3> </h3>
<p class="text-lg font-bold text-gray-800">
{{ $kataDominan }}
</p>
</div>
<!-- SARAN --> <ul class="text-sm text-gray-600 space-y-2">
<div class="bg-white rounded-xl shadow p-6 text-center"> <li class="flex justify-between">
<h3 class="text-gray-600 font-semibold mb-2"> <span>kotor</span>
Saran: <span class="font-semibold">15x</span>
</h3> </li>
<p class="text-lg font-bold text-gray-800"> <li class="flex justify-between">
{{ $saran }} <span>parkir</span>
</p> <span class="font-semibold">10x</span>
</div> </li>
<li class="flex justify-between">
<span>mahal</span>
<span class="font-semibold">7x</span>
</li>
</ul>
</div>
<!-- Saran -->
<div class="bg-white rounded-xl shadow p-6">
<h3 class="text-gray-700 font-semibold mb-4">
Saran Perbaikan
</h3>
<p class="text-sm text-gray-600 leading-relaxed">
Perlu peningkatan kebersihan area wisata serta evaluasi sistem
pengelolaan parkir untuk meningkatkan kenyamanan pengunjung.
</p>
</div> </div>
</div> </div>
</div>
@endsection @endsection

View File

@ -16,8 +16,8 @@ class="bg-white border border-gray-300 text-sm rounded-lg px-4 py-2 shadow-sm
<option>Semua Destinasi</option> <option>Semua Destinasi</option>
<option>Pantai Papuma</option> <option>Pantai Papuma</option>
<option>Pantai Watu Ulo</option> <option>Pantai Watu Ulo</option>
<option>Taman Botani Sukorambi</option> <option>Teluk Love (Payangan)</option>
<option>Air Terjun Tancak Tulis</option> <option>Kebun Teh Gunung Gambir </option>
</select> </select>
</div> </div>
@ -39,33 +39,37 @@ class="bg-blue-600 hover:bg-blue-700 text-white font-semibold
Data Ulasan Mentah Data Ulasan Mentah
</h3> </h3>
<div class="overflow-x-auto"> <div class="overflow-x-auto max-h-[400px] overflow-y-auto">
<table class="w-full text-sm text-left"> <table class="w-full text-sm text-left">
<thead> <thead>
<tr class="bg-gray-100 text-gray-600"> <tr class="bg-gray-100 text-gray-600">
<th class="px-4 py-3">Destinasi</th> <th class="px-4 py-3">Destinasi</th>
<th class="px-4 py-3">Reviewer</th> <th class="px-4 py-3">Reviewer</th>
<th class="px-4 py-3">Rating</th>
<th class="px-4 py-3">Ulasan</th> <th class="px-4 py-3">Ulasan</th>
<th class="px-4 py-3">Tanggal</th> <th class="px-4 py-3">Tanggal</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@forelse($mentah as $row) @forelse($mentah as $data)
<tr class="border-b"> <tr>
<td class="px-4 py-3">{{ $row->nama_wisata }}</td> <td class="px-4 py-3">{{ $data->destinasi }}</td>
<td class="px-4 py-3">{{ $row->reviewer_name }}</td> <td class="px-4 py-3">{{ $data->reviewer }}</td>
<td class="px-4 py-3">{{ $row->ulasan }}</td> <td class="px-4 py-3">{{ $data->rating }}</td>
<td class="px-4 py-3">{{ $row->tanggal }}</td> <td class="px-4 py-3">{{ $data->ulasan }}</td>
</tr> <td class="px-4 py-3">{{ $data->tanggal }}</td>
@empty
<tr> </tr>
<td colspan="4" class="text-center py-6 text-gray-400"> @empty
Belum ada data mentah. <tr>
</td> <td colspan="6" class="text-center py-6 text-gray-400">
</tr> Belum ada data mentah.
@endforelse </td>
</tbody> </tr>
@endforelse
</tbody>
</table> </table>
</div> </div>
@ -78,7 +82,7 @@ class="bg-blue-600 hover:bg-blue-700 text-white font-semibold
<button <button
class="bg-green-600 hover:bg-green-700 text-white font-semibold class="bg-green-600 hover:bg-green-700 text-white font-semibold
px-6 py-2 rounded-lg shadow"> px-6 py-2 rounded-lg shadow">
Preprocessing Data Proses Analisis
</button> </button>
</form> </form>
</div> </div>
@ -91,33 +95,42 @@ class="bg-green-600 hover:bg-green-700 text-white font-semibold
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="w-full text-sm text-left"> <table class="w-full text-sm text-left">
<thead> <thead class="sticky top-0 bg-gray-100">
<tr class="bg-gray-100 text-gray-600"> <tr class="bg-gray-100 text-gray-600">
<th class="px-4 py-3">Ulasan Asli</th> <th class="px-4 py-3">Destinasi</th>
<th class="px-4 py-3">Cleaning + Case Folding</th> <th class="px-4 py-3">Ulasan Mentah</th>
<th class="px-4 py-3">Ulasan Bersih Final (Stemming)</th> <th class="px-4 py-3">Ulasan Bersih</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@forelse($preprocessing as $row) @forelse($preprocessing as $row)
<tr class="border-b"> <tr class="border-b">
<td class="px-4 py-3">{{ $row->ulasan_asli }}</td>
<!-- gabungan cleaning + lowercase --> <td class="px-4 py-3">
<td class="px-4 py-3">{{ $row->cleaning }}</td> {{ $row->destinasi }}
</td>
<!-- hasil akhir setelah stopword + slang + stemming --> <td class="px-4 py-3">
<td class="px-4 py-3">{{ $row->stemming }}</td> {{ $row->ulasan_asli }}
</tr> </td>
@empty
<tr> <td class="px-4 py-3">
<td colspan="3" class="text-center py-6 text-gray-400"> {{ $row->stemming }}
Belum ada hasil preprocessing. </td>
</td>
</tr> </tr>
@endforelse
</tbody> @empty
<tr>
<td colspan="3" class="text-center py-6 text-gray-400">
Belum ada hasil preprocessing.
</td>
</tr>
@endforelse
</tbody>
</table> </table>
</div> </div>

View File

@ -31,7 +31,7 @@
Route::get('/ulasan', [UlasanController::class, 'index'])->name('ulasan.index'); Route::get('/ulasan', [UlasanController::class, 'index'])->name('ulasan.index');
Route::post('/ulasan/ambil', [UlasanController::class, 'ambilData'])->name('ulasan.ambil'); Route::post('/ulasan/ambil', [UlasanController::class, 'ambilData'])->name('ulasan.ambil');
Route::post('/ulasan/analisis', [UlasanController::class, 'analisisData'])->name('ulasan.analisis'); Route::post('/ulasan/analisis', [UlasanController::class, 'analisisData'])->name('ulasan.analisis');
Route::get('/hasil-analisis', [AnalisisController::class, 'index']) Route::get('/hasil-analisis', [AnalisisController::class, 'index'])
->name('analisis.index'); ->name('analisis.index');
Route::get('/rekomendasi', [RekomendasiController::class, 'index']) Route::get('/rekomendasi', [RekomendasiController::class, 'index'])
->name('rekomendasi.index');; ->name('rekomendasi.index');;

70
scraper/prepocessing.py Normal file
View File

@ -0,0 +1,70 @@
import mysql.connector
import re
from Sastrawi.Stemmer.StemmerFactory import StemmerFactory
# =============================
# DATABASE
# =============================
conn = mysql.connector.connect(
host="localhost",
database="sentara",
user="root",
password=""
)
cursor = conn.cursor()
# =============================
# STEMMER
# =============================
factory = StemmerFactory()
stemmer = factory.create_stemmer()
# =============================
# AMBIL DATA ULASAN
# =============================
cursor.execute("SELECT id, ulasan FROM ulasan")
data = cursor.fetchall()
print("Total ulasan:", len(data))
for row in data:
id_ulasan = row[0]
text = row[1]
# =============================
# CLEANING
# =============================
text_clean = re.sub(r'[^a-zA-Z\s]', '', text)
# =============================
# CASE FOLDING
# =============================
text_clean = text_clean.lower()
# =============================
# STEMMING
# =============================
text_stem = stemmer.stem(text_clean)
# =============================
# INSERT KE DATABASE
# =============================
cursor.execute("""
INSERT INTO preprocessing_data
(ulasan_asli, cleaning, stemming)
VALUES (%s,%s,%s)
""",
(
text,
text_clean,
text_stem
))
conn.commit()
print("Preprocessing selesai")
conn.close()

View File

@ -0,0 +1,228 @@
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import mysql.connector
import time
from datetime import datetime
# =============================
# DATABASE
# =============================
conn = mysql.connector.connect(
host="localhost",
database="sentara",
user="root",
password=""
)
cursor = conn.cursor()
# =============================
# CHROME
# =============================
options = Options()
options.add_argument("--start-maximized")
driver = webdriver.Chrome(options=options)
wait = WebDriverWait(driver, 15)
# =============================
# DESTINASI
# =============================
destinations = {
"Pantai Papuma": "Pantai Papuma Jember",
"Pantai Watu Ulo": "Pantai Watu Ulo Jember",
"Teluk Love": "Teluk Love Jember",
"Kebun Teh Gunung Gambir": "Kebun Teh Gunung Gambir Jember"
}
# =============================
# LOOP DESTINASI
# =============================
for nama_wisata, keyword in destinations.items():
saved = 0
print("\n====================")
print("Scraping:", nama_wisata)
print("====================")
driver.get("https://www.google.com/maps")
time.sleep(5)
# =============================
# CARI TEMPAT
# =============================
try:
search = driver.find_element(By.ID, "searchboxinput")
except:
search = driver.find_element(By.XPATH, "//input[@aria-label='Search Google Maps']")
search.clear()
search.send_keys(keyword)
driver.find_element(By.ID, "searchbox-searchbutton").click()
time.sleep(6)
# =============================
# KLIK HASIL PERTAMA
# =============================
try:
first_result = wait.until(
EC.element_to_be_clickable((By.XPATH, "(//a[contains(@href,'/maps/place')])[1]"))
)
first_result.click()
time.sleep(5)
except:
print("Tempat tidak ditemukan")
continue
# =============================
# BUKA TAB REVIEW
# =============================
try:
review_button = wait.until(
EC.element_to_be_clickable((By.XPATH, "//button[contains(@aria-label,'review')]"))
)
review_button.click()
time.sleep(5)
except:
print("Review button tidak ditemukan")
continue
# =============================
# SCROLL REVIEW
# =============================
try:
scrollable_div = driver.find_element(By.XPATH, "//div[@role='region']")
except:
print("Container review tidak ditemukan")
continue
last_count = 0
same_count = 0
reviews = []
for i in range(200):
driver.execute_script(
"arguments[0].scrollTop = arguments[0].scrollHeight",
scrollable_div
)
time.sleep(2)
reviews = driver.find_elements(By.XPATH, "//div[@data-review-id]")
current_count = len(reviews)
print("Scroll", i, "Review:", current_count)
if current_count == last_count:
same_count += 1
else:
same_count = 0
if same_count >= 6:
print("✔ Semua review sudah termuat")
break
last_count = current_count
# =============================
# AMBIL DATA REVIEW
# =============================
for review in reviews:
try:
review_id = review.get_attribute("data-review-id")
if not review_id:
continue
# cek duplikasi
cursor.execute(
"SELECT COUNT(*) FROM ulasan WHERE review_id=%s",
(review_id,)
)
exists = cursor.fetchone()[0]
if exists > 0:
continue
# reviewer
try:
reviewer = review.find_element(
By.XPATH, ".//div[contains(@class,'d4r55')]"
).text
except:
reviewer = "Unknown"
# rating
try:
rating_text = review.find_element(
By.XPATH, ".//span[@role='img']"
).get_attribute("aria-label")
rating = float(rating_text.split()[0])
except:
rating = 0
# ulasan
try:
review_text = review.find_element(
By.XPATH, ".//span[contains(@class,'wiI7pd')]"
).text
except:
review_text = ""
# tanggal
try:
review_date = review.find_element(
By.XPATH, ".//span[contains(@class,'rsqaWe')]"
).text
except:
review_date = ""
if review_text == "":
continue
# =============================
# INSERT DATABASE
# =============================
cursor.execute("""
INSERT INTO ulasan
(review_id,destinasi,reviewer,ulasan,rating,tanggal,scraping_date)
VALUES(%s,%s,%s,%s,%s,%s,%s)
""",
(
review_id,
nama_wisata,
reviewer,
review_text,
rating,
review_date,
datetime.now()
))
conn.commit()
saved += 1
except Exception as e:
print("skip:", e)
print("Total tersimpan:", saved)
print("\n====================")
print("SCRAPING SELESAI")
print("====================")
driver.quit()
conn.close()