update fitur

This commit is contained in:
MufridaFaraDiani27 2026-04-04 12:44:55 +07:00
parent 5fa8005eb1
commit dc393edbba
16 changed files with 734 additions and 528 deletions

View File

@ -10,16 +10,16 @@ class AnalisisController extends Controller
public function index(Request $request) public function index(Request $request)
{ {
// Dropdown list destinasi unik // Dropdown list destinasi unik
$destinasiList = HasilAnalisis::select('nama_wisata') $wisataList = HasilAnalisis::select('wisata')
->distinct() ->distinct()
->pluck('nama_wisata'); ->pluck('wisata');
// Query utama tabel // Query utama tabel
$query = HasilAnalisis::query(); $query = HasilAnalisis::query();
// Filter kalau user pilih destinasi tertentu // Filter kalau user pilih destinasi tertentu
if ($request->filled('destinasi')) { if ($request->filled('wisata')) {
$query->where('nama_wisata', $request->destinasi); $query->where('wisata', $request->wisata);
} }
// Data hasil analisis (pakai paginate biar rapi) // Data hasil analisis (pakai paginate biar rapi)
@ -27,7 +27,7 @@ public function index(Request $request)
return view('analisis.index', compact( return view('analisis.index', compact(
'hasil', 'hasil',
'destinasiList' 'wisataList'
)); ));
} }
} }

View File

@ -3,19 +3,113 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class DashboardController extends Controller class DashboardController extends Controller
{ {
public function index() public function index(Request $request)
{ {
$stats = [ $wisata = $request->wisata;
'total' => 520,
'positif_persen' => 68,
'negatif_persen' => 20,
'netral_persen' => 12,
];
$lastUpdate = '27 Februari 2026, 22:45';
return view('dashboard', compact('stats', 'lastUpdate')); // =============================
// TOTAL DATA
// =============================
$total = DB::table('hasil_analisis')
->when($wisata, function ($q) use ($wisata) {
$q->where('wisata', $wisata);
})
->count();
// =============================
// HITUNG SENTIMEN
// =============================
$positif = DB::table('hasil_analisis')
->where('sentimen', 'Positif')
->when($wisata, function ($q) use ($wisata) {
$q->where('wisata', $wisata);
})
->count();
$negatif = DB::table('hasil_analisis')
->where('sentimen', 'Negatif')
->when($wisata, function ($q) use ($wisata) {
$q->where('wisata', $wisata);
})
->count();
$netral = DB::table('hasil_analisis')
->where('sentimen', 'Netral')
->when($wisata, function ($q) use ($wisata) {
$q->where('wisata', $wisata);
})
->count();
// =============================
// PERSENTASE
// =============================
$stats = [
'total' => $total,
'positif_persen' => $total ? round(($positif / $total) * 100) : 0,
'negatif_persen' => $total ? round(($negatif / $total) * 100) : 0,
'netral_persen' => $total ? round(($netral / $total) * 100) : 0,
];
// =============================
// GRAFIK PIE (Distribusi Sentimen)
// =============================
$chartSentimen = DB::table('hasil_analisis')
->select('sentimen', DB::raw('count(*) as total'))
->when($wisata, function ($q) use ($wisata) {
$q->where('wisata', $wisata);
})
->groupBy('sentimen')
->get();
// =============================
// GRAFIK BAR (per destinasi)
// =============================
$chartDestinasi = DB::table('hasil_analisis')
->select(
'wisata',
DB::raw("SUM(CASE WHEN sentimen = 'Positif' THEN 1 ELSE 0 END) as positif"),
DB::raw("SUM(CASE WHEN sentimen = 'Negatif' THEN 1 ELSE 0 END) as negatif"),
DB::raw("SUM(CASE WHEN sentimen = 'Netral' THEN 1 ELSE 0 END) as netral")
)
->groupBy('wisata')
->get();
// =============================
// TOTAL DATA PER DESTINASI
// =============================
$totalPerWisata = DB::table('hasil_analisis')
->select('wisata', DB::raw('count(*) as total'))
->groupBy('wisata')
->get()
->toArray();
// =============================
// WORD CLOUD
// =============================
$texts = DB::table('preprocessing_data')
->pluck('stemming')
->toArray(); // 🔥 WAJIB
$allText = implode(' ', $texts);
// =============================
// LAST UPDATE
// =============================
$lastUpdate = DB::table('hasil_analisis')
->latest('created_at')
->value('created_at');
return view('dashboard', compact(
'stats',
'chartSentimen',
'chartDestinasi',
'totalPerWisata',
'allText',
'lastUpdate'
));
} }
} }

View File

@ -10,16 +10,16 @@ class RekomendasiController extends Controller
public function index(Request $request) public function index(Request $request)
{ {
// dropdown destinasi unik // dropdown destinasi unik
$destinasiList = HasilAnalisis::select('nama_wisata') $destinasiList = HasilAnalisis::select('wisata')
->distinct() ->distinct()
->pluck('nama_wisata'); ->pluck('scraping_pipeline.py');
// query utama // query utama
$query = HasilAnalisis::query(); $query = HasilAnalisis::query();
// filter destinasi jika dipilih // filter destinasi jika dipilih
if ($request->filled('destinasi')) { if ($request->filled('destinasi')) {
$query->where('nama_wisata', $request->destinasi); $query->where('wisata', $request->destinasi);
} }
// ambil ulasan negatif // ambil ulasan negatif

View File

@ -4,58 +4,51 @@
use Illuminate\Http\Request; use Illuminate\Http\Request;
use App\Models\Ulasan; use App\Models\Ulasan;
// use App\Models\HasilSentimen;
use App\Models\PreprocessingData; use App\Models\PreprocessingData;
class UlasanController extends Controller class UlasanController extends Controller
{ {
// public function index() public function index(Request $request)
// { {
// $mentah = Ulasan::where('is_processed', 0)->get(); $wisata = $request->wisata;
// // $hasil = HasilSentimen::latest()->get();
// // return view('ulasan.index', compact('mentah', 'hasil')); $query = Ulasan::query();
// } if ($request->filled('search')) {
$query->where('ulasan', 'LIKE', '%' . $request->search . '%');
public function index()
{
// Data ulasan mentah (belum diproses)
$mentah = Ulasan::all();
// Data hasil preprocessing
$preprocessing = PreprocessingData::latest()->get();
return view('ulasan.index', compact('mentah', 'preprocessing'));
} }
if ($wisata) {
$query->where('wisata', 'LIKE', '%' . $wisata . '%');
}
// Tombol ambil data terbaru // ✅ PAGINATION 50 DATA
public function ambilData() $mentah = $query->latest()->paginate(50)->withQueryString();
{
$pythonPath = '"C:\\Users\\Mufrida Farah\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"';
$scriptPath = base_path('scraper/scraping_pipeline.py');
$command = $pythonPath . " " . $scriptPath . " 2>&1"; // preprocessing
$preprocessing = PreprocessingData::latest()->paginate(50);
shell_exec($command); return view('ulasan.index', compact('mentah', 'preprocessing'));
}
return redirect()->route('ulasan.index') public function ambilData()
->with('success', 'Data berhasil diambil dari Google Maps'); {
} $pythonPath = "C:\\Users\\Mufrida Farah\\AppData\\Local\\Programs\\Python\\Python312\\python.exe";
$scriptPath = base_path('scraper/scraping_pipeline.py');
shell_exec($pythonPath . " " . $scriptPath);
return redirect()->route('ulasan.index')
->with('success','Data berhasil diambil');
}
// Tombol analisis data
public function analisisData() public function analisisData()
{ {
$pythonPath = '"C:\\Users\\Mufrida Farah\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"'; $pythonPath = '"C:\\Users\\Mufrida Farah\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"';
$scriptPath = '"C:\\laragon\\www\\Sentara\\scraper\\preprocessing.py"';
$scriptPath = base_path('scraper/preprocessing.py'); shell_exec($pythonPath . " " . $scriptPath . " 2>&1");
$command = $pythonPath . " " . $scriptPath . " 2>&1"; return redirect()->route('ulasan.index')
->with('success','Analisis berhasil');
shell_exec($command); }
}
return redirect()->route('ulasan.index')
->with('success','Preprocessing berhasil dilakukan');
}
}

View File

@ -12,7 +12,7 @@ class HasilAnalisis extends Model
protected $table = 'hasil_analisis'; protected $table = 'hasil_analisis';
protected $fillable = [ protected $fillable = [
'nama_wisata', 'wisata',
'ulasan_terolah', 'ulasan_terolah',
'sentimen', 'sentimen',
'probabilitas' 'probabilitas'

View File

@ -12,7 +12,7 @@ class HasilSentimen extends Model
protected $table = 'hasil_sentimen'; protected $table = 'hasil_sentimen';
protected $fillable = [ protected $fillable = [
'nama_wisata', 'wisata',
'ulasan', 'ulasan',
'sentimen' 'sentimen'
]; ];

View File

@ -12,7 +12,7 @@ class Ulasan extends Model
protected $table = 'ulasan'; protected $table = 'ulasan';
protected $fillable = [ protected $fillable = [
'nama_wisata', 'wisata',
'reviewer', 'reviewer',
'rating', 'rating',
'ulasan', 'ulasan',

View File

@ -13,7 +13,7 @@ public function up(): void
{ {
Schema::create('ulasan_mentah', function (Blueprint $table) { Schema::create('ulasan_mentah', function (Blueprint $table) {
$table->id(); $table->id();
$table->string('nama_wisata'); $table->string('wisata');
$table->string('reviewer_name')->nullable(); $table->string('reviewer_name')->nullable();
$table->integer('rating')->nullable(); $table->integer('rating')->nullable();
$table->text('ulasan'); $table->text('ulasan');

View File

@ -13,7 +13,7 @@ public function up(): void
{ {
Schema::create('hasil_analisis', function (Blueprint $table) { Schema::create('hasil_analisis', function (Blueprint $table) {
$table->id(); $table->id();
$table->string('nama_wisata'); $table->string('wisata');
$table->text('ulasan_terolah'); $table->text('ulasan_terolah');
$table->string('sentimen'); $table->string('sentimen');
$table->float('probabilitas'); $table->float('probabilitas');

View File

@ -26,3 +26,10 @@ .sidebar-menu .sidebar-link {
.sidebar-menu .sidebar-link:hover { .sidebar-menu .sidebar-link:hover {
background: rgba(255, 255, 255, 0.15); background: rgba(255, 255, 255, 0.15);
} }
.line-clamp-3 {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}

View File

@ -12,16 +12,16 @@
<!-- Dropdown Filter --> <!-- Dropdown Filter -->
<form method="GET" action="{{ route('analisis.index') }}"> <form method="GET" action="{{ route('analisis.index') }}">
<select name="destinasi" <select name="wisata"
onchange="this.form.submit()" onchange="this.form.submit()"
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 value="">Semua Destinasi</option> <option value="">Semua Destinasi</option>
@foreach($destinasiList as $dest) @foreach($wisataList as $dest)
<option value="{{ $dest }}" <option value="{{ $dest }}"
{{ request('destinasi') == $dest ? 'selected' : '' }}> {{ request('wisata') == $dest ? 'selected' : '' }}>
{{ $dest }} {{ $dest }}
</option> </option>
@endforeach @endforeach
@ -41,7 +41,7 @@ class="bg-white border border-gray-300 text-sm rounded-lg px-4 py-2 shadow-sm
<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">wisata</th>
<th class="px-4 py-3">Ulasan Terolah</th> <th class="px-4 py-3">Ulasan Terolah</th>
<th class="px-4 py-3">Sentimen</th> <th class="px-4 py-3">Sentimen</th>
<th class="px-4 py-3">Probabilitas</th> <th class="px-4 py-3">Probabilitas</th>
@ -54,7 +54,7 @@ class="bg-white border border-gray-300 text-sm rounded-lg px-4 py-2 shadow-sm
<!-- Destinasi --> <!-- Destinasi -->
<td class="px-4 py-3 font-medium"> <td class="px-4 py-3 font-medium">
{{ $row->nama_wisata }} {{ $row->wisata }}
</td> </td>
<!-- Ulasan --> <!-- Ulasan -->

View File

@ -2,147 +2,145 @@
@section('content') @section('content')
<div class="max-w-7xl mx-auto"> <div class="max-w-6xl mx-auto space-y-8">
{{-- Header --}} <!-- HEADER -->
<div class="flex justify-between items-start mb-8"> <div class="flex justify-between items-center">
<div> <h2 class="text-3xl font-bold text-gray-800">Data Ulasan</h2>
<h2 class="text-2xl font-semibold text-gray-800">
Dashboard
</h2>
<!-- <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
focus:ring-2 focus:ring-blue-400 focus:outline-none">
<select id="filterWisata" class="border rounded px-3 py-2 text-sm">
<option value="">Semua Destinasi</option> <option value="">Semua Destinasi</option>
<option value="papuma">Pantai Papuma</option> <option value="Pantai Papuma" {{ request('wisata')=='Pantai Papuma'?'selected':'' }}>Pantai Papuma</option>
<option value="watuulo">Pantai Watu Ulo</option> <option value="Pantai Watu Ulo" {{ request('wisata')=='Pantai Watu Ulo'?'selected':'' }}>Pantai Watu Ulo</option>
<option value="teluklove">Teluk Love (Payangan)</option> <option value="Teluk Love (Payangan)" {{ request('wisata')=='Teluk Love (Payangan)'?'selected':'' }}>Teluk Love</option>
<option value="gununggambir">Kebun Teh Gunung Gambir </option> <option value="Kebun Teh Gunung Gambir" {{ request('wisata')=='Kebun Teh Gunung Gambir'?'selected':'' }}>Kebun Teh Gunung Gambir</option>
</select> </select>
</div>
<script>
document.getElementById("filterWisata").addEventListener("change", function () {
window.location.href = "/ulasan?wisata=" + encodeURIComponent(this.value);
});
</script>
<!-- BUTTON -->
<form action="{{ route('ulasan.ambil') }}" method="POST">
@csrf
<button class="bg-blue-600 text-white px-5 py-2 rounded">Ambil Data Terbaru</button>
</form> </form>
</div>
{{-- Statistik Cards --}} <!-- ================= TABLE ================= -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<div class="bg-white rounded-xl shadow p-5">
<p class="text-gray-500 text-sm">Total Ulasan</p>
<h3 class="text-2xl font-bold mt-2 text-gray-900">
{{ $stats['total'] }}
</h3>
</div>
<div class="bg-white rounded-xl shadow p-5">
<p class="text-gray-500 text-sm">Sentimen Positif</p>
<h3 class="text-2xl font-bold mt-2 text-green-600">
{{ $stats['positif_persen'] }}%
</h3>
</div>
<div class="bg-white rounded-xl shadow p-5">
<p class="text-gray-500 text-sm">Sentimen Negatif</p>
<h3 class="text-2xl font-bold mt-2 text-red-600">
{{ $stats['negatif_persen'] }}%
</h3>
</div>
<div class="bg-white rounded-xl shadow p-5">
<p class="text-gray-500 text-sm">Sentimen Netral</p>
<h3 class="text-2xl font-bold mt-2 text-yellow-500">
{{ $stats['netral_persen'] }}%
</h3>
</div>
</div>
{{-- Chart Section --}}
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
<!-- 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">
Distribusi Sentimen <h3 class="font-semibold mb-4">Data Ulasan Mentah</h3>
</h3>
<div class="h-[280px] flex justify-center items-center"> <div class="border rounded overflow-hidden">
<canvas id="pieChart"></canvas>
<!-- HEADER -->
<table class="w-full table-fixed text-sm">
<thead class="bg-gray-100">
<tr>
<th class="px-4 py-3 w-1/4">Wisata</th>
<th class="px-4 py-3 w-1/6">Rating</th>
<th class="px-4 py-3 w-2/4">Ulasan</th>
<th class="px-4 py-3 w-1/6">Tanggal</th>
</tr>
</thead>
</table>
<!-- BODY SCROLL -->
<div class="max-h-[300px] overflow-y-auto">
<table class="w-full table-fixed text-sm">
<tbody>
@forelse($mentah as $item)
<tr class="border-b hover:bg-gray-50">
<td class="px-4 py-2">{{ $item->wisata }}</td>
<td class="px-4 py-2">{{ $item->rating }}</td>
<td class="px-4 py-2 break-words line-clamp-3">
{{ $item->ulasan }}
</td>
<td class="px-4 py-2">{{ $item->tanggal }}</td>
</tr>
@empty
<tr>
<td colspan="4" class="text-center py-4 text-gray-400">
Tidak ada data
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div> </div>
<!-- PAGINATION SIMPLE -->
<div class="flex justify-center mt-6 gap-2 text-sm">
@if($mentah->onFirstPage())
<span class="text-gray-400">Prev</span>
@else
<a href="{{ $mentah->previousPageUrl() }}">Prev</a>
@endif
@for ($i = 1; $i <= $mentah->lastPage(); $i++)
@if ($i == $mentah->currentPage())
<span class="font-bold text-blue-600">{{ $i }}</span>
@elseif ($i <= 5 || $i == $mentah->lastPage())
<a href="{{ $mentah->url($i) }}">{{ $i }}</a>
@elseif ($i == 6)
<span>...</span>
@endif
@endfor
@if($mentah->hasMorePages())
<a href="{{ $mentah->nextPageUrl() }}">Next</a>
@else
<span class="text-gray-400">Next</span>
@endif
</div>
</div> </div>
<!-- Bar Chart --> <!-- BUTTON ANALISIS -->
<form action="{{ route('ulasan.analisis') }}" method="POST">
@csrf
<button class="bg-green-600 text-white px-5 py-2 rounded">Proses Analisis</button>
</form>
<!-- PREPROCESSING -->
<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">Hasil Preprocessing</h3>
Sentimen per Destinasi
</h3> <div class="max-h-[300px] overflow-y-auto">
<div class="h-[280px]"> <table class="w-full text-sm">
<canvas id="barChart"></canvas> <thead class="bg-gray-100 sticky top-0">
<tr>
<th class="px-4 py-2">Wisata</th>
<th class="px-4 py-2">Ulasan Mentah</th>
<th class="px-4 py-2">Ulasan Bersih</th>
</tr>
</thead>
<tbody>
@forelse($preprocessing as $row)
<tr class="border-b">
<td class="px-4 py-2">{{ $row->wisata }}</td>
<td class="px-4 py-2">{{ $row->ulasan_asli }}</td>
<td class="px-4 py-2">{{ $row->stemming }}</td>
</tr>
@empty
<tr>
<td colspan="3" class="text-center py-4 text-gray-400">
Belum ada data
</td>
</tr>
@endforelse
</tbody>
</table>
</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> @endsection
</div>
</div>
@endsection

View File

@ -6,136 +6,250 @@
<!-- HEADER --> <!-- HEADER -->
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<h2 class="text-3xl font-bold text-gray-800"> <h2 class="text-3xl font-bold text-gray-800">Data Ulasan</h2>
Data Ulasan
</h2>
<select <select id="filterWisata" class="border rounded px-3 py-2 text-sm">
class="bg-white border border-gray-300 text-sm rounded-lg px-4 py-2 shadow-sm <option value="">Semua Destinasi</option>
focus:ring-2 focus:ring-blue-400 focus:outline-none"> <option value="Pantai Papuma" {{ request('wisata')=='Pantai Papuma'?'selected':'' }}>Pantai Papuma</option>
<option>Semua Destinasi</option> <option value="Pantai Watu Ulo" {{ request('wisata')=='Pantai Watu Ulo'?'selected':'' }}>Pantai Watu Ulo</option>
<option>Pantai Papuma</option> <option value="Teluk Love (Payangan)" {{ request('wisata')=='Teluk Love (Payangan)'?'selected':'' }}>Teluk Love</option>
<option>Pantai Watu Ulo</option> <option value="Kebun Teh Gunung Gambir" {{ request('wisata')=='Kebun Teh Gunung Gambir'?'selected':'' }}>Kebun Teh Gunung Gambir</option>
<option>Teluk Love (Payangan)</option>
<option>Kebun Teh Gunung Gambir </option>
</select> </select>
</div> </div>
<!-- BUTTON AMBIL DATA --> <script>
<div> document.getElementById("filterWisata").addEventListener("change", function () {
<form action="{{ route('ulasan.ambil') }}" method="POST"> window.location.href = "/ulasan?wisata=" + encodeURIComponent(this.value);
@csrf });
<button </script>
class="bg-blue-600 hover:bg-blue-700 text-white font-semibold
px-6 py-2 rounded-lg shadow"> <!-- BUTTON -->
Ambil Data Terbaru <form action="{{ route('ulasan.ambil') }}" method="POST">
</button> @csrf
</form> <button class="bg-blue-600 text-white px-5 py-2 rounded">
Ambil Data Terbaru
</button>
</form>
<!-- ================= TABLE ================= -->
<div class="bg-white rounded-xl shadow p-6">
<h3 class="font-semibold mb-4">Data Ulasan Mentah</h3>
<div class="border rounded overflow-hidden">
<!-- HEADER -->
<table class="w-full text-sm text-left table-fixed border border-gray-200 rounded-t-lg overflow-hidden">
<thead class="bg-gray-100 text-gray-700">
<tr>
<th class="px-4 py-3 w-[15%]">Wisata</th>
<th class="px-4 py-3 w-[10%] text-center">Rating</th>
<th class="px-4 py-3 w-[60%]">Ulasan</th>
<th class="px-4 py-3 w-[15%] text-center">Tanggal</th>
</tr>
</thead>
</table>
<!-- BODY -->
<div class="max-h-[300px] overflow-y-auto border-x border-b border-gray-200 rounded-b-lg">
<table class="w-full text-sm table-fixed">
<tbody class="divide-y divide-gray-200">
@foreach($mentah as $item)
<tr class="hover:bg-gray-50 transition">
<!-- WISATA -->
<td class="px-4 py-3 w-[15%] font-medium text-gray-700">
{{ $item->wisata }}
</td>
<!-- RATING -->
<td class="px-4 py-3 w-[10%] text-center">
{{ $item->rating }}
</td>
<!-- ULASAN -->
<td class="px-4 py-3 w-[60%] text-black-700 leading-relaxed">
<div class="line-clamp-3">
{{ $item->ulasan }}
</div>
</td>
<!-- TANGGAL -->
<td class="px-4 py-3 w-[15%] text-center text-black-700 leading-relaxed">
{{ $item->tanggal }}
</td>
</tr>
@endforeach
</tbody>
</table>
</div> </div>
<!-- TABLE DATA MENTAH --> </div>
<div class="bg-white rounded-xl shadow p-6">
<h3 class="text-lg font-semibold text-gray-700 mb-4">
Data Ulasan Mentah
</h3>
<div class="overflow-x-auto max-h-[400px] overflow-y-auto"> <!-- INFO -->
<table class="w-full text-sm text-left"> <div class="text-sm text-gray-500 mt-4 text-center">
<thead> Showing {{ $mentah->firstItem() }} to {{ $mentah->lastItem() }}
<tr class="bg-gray-100 text-gray-600"> of {{ $mentah->total() }} results
<th class="px-4 py-3">Destinasi</th> </div>
<th class="px-4 py-3">Reviewer</th>
<th class="px-4 py-3">Rating</th> <!-- PAGINATION -->
<th class="px-4 py-3">Ulasan</th> <div class="flex justify-center mt-6 items-center gap-2 text-sm">
<th class="px-4 py-3">Tanggal</th>
{{-- FIRST --}}
@if ($mentah->currentPage() > 1)
<a href="{{ $mentah->url(1) }}">«</a>
@endif
{{-- PREV --}}
@if ($mentah->onFirstPage())
<span class="text-gray-400"></span>
@else
<a href="{{ $mentah->previousPageUrl() }}"></a>
@endif
@php
$start = max($mentah->currentPage() - 2, 1);
$end = min($mentah->currentPage() + 2, $mentah->lastPage());
@endphp
{{-- NUMBER DINAMIS --}}
@for ($i = $start; $i <= $end; $i++)
@if ($i == $mentah->currentPage())
<span class="px-3 py-1 bg-blue-600 text-white rounded-full">{{ $i }}</span>
@else
<a href="{{ $mentah->url($i) }}" class="px-3 py-1 hover:underline">
{{ $i }}
</a>
@endif
@endfor
{{-- NEXT --}}
@if ($mentah->hasMorePages())
<a href="{{ $mentah->nextPageUrl() }}"></a>
@else
<span class="text-gray-400"></span>
@endif
{{-- LAST --}}
@if ($mentah->currentPage() < $mentah->lastPage())
<a href="{{ $mentah->url($mentah->lastPage()) }}">»</a>
@endif
</div>
</div>
<!-- BUTTON ANALISIS -->
<form action="{{ route('ulasan.analisis') }}" method="POST">
@csrf
<button class="bg-green-600 text-white px-5 py-2 rounded">
Proses Analisis
</button>
</form>
<!-- PREPROCESSING -->
<div class="bg-white rounded-xl shadow p-6">
<h3 class="font-semibold mb-4">Hasil Preprocessing</h3>
<div class="max-h-[300px] overflow-y-auto">
<table class="w-full text-sm">
<thead class="bg-gray-100 sticky top-0">
<tr>
<th class="px-4 py-2">Wisata</th>
<th class="px-4 py-2">Ulasan Mentah</th>
<th class="px-4 py-2">Ulasan Bersih</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@forelse($mentah as $data) @forelse($preprocessing as $row)
<tr> <tr class="border-b">
<td class="px-4 py-3">{{ $data->destinasi }}</td> <td class="px-4 py-2">{{ $row->wisata }}</td>
<td class="px-4 py-3">{{ $data->reviewer }}</td> <td class="px-4 py-2">{{ $row->ulasan_asli }}</td>
<td class="px-4 py-3">{{ $data->rating }}</td> <td class="px-4 py-2">{{ $row->stemming }}</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="3" class="text-center py-4 text-gray-400">
@empty Belum ada data
<tr> </td>
<td colspan="6" class="text-center py-6 text-gray-400"> </tr>
Belum ada data mentah. <div class="flex justify-center mt-4 text-sm">
</td>
</tr> <!-- INFO -->
@endforelse <div class="text-sm text-gray-500 mt-4 text-center">
</tbody> Showing {{ $preprocessing->firstItem() }} to {{ $preprocessing->lastItem() }}
of {{ $preprocessing->total() }} results
</div>
</table> <!-- PAGINATION -->
</div> <div class="flex justify-center mt-6 items-center gap-2 text-sm">
</div>
<!-- BUTTON PREPROCESSING --> {{-- FIRST --}}
<div> @if ($preprocessing->currentPage() > 1)
<form action="{{ route('ulasan.analisis') }}" method="POST"> <a href="{{ $preprocessing->url(1) }}">«</a>
@csrf @endif
<button
class="bg-green-600 hover:bg-green-700 text-white font-semibold
px-6 py-2 rounded-lg shadow">
Proses Analisis
</button>
</form>
</div>
<!-- HASIL PREPROCESSING --> {{-- PREV --}}
<div class="bg-white rounded-xl shadow p-6"> @if ($preprocessing->onFirstPage())
<h3 class="text-lg font-semibold text-gray-700 mb-4"> <span class="text-gray-400"></span>
Hasil Preprocessing Data @else
</h3> <a href="{{ $preprocessing->previousPageUrl() }}"></a>
@endif
<div class="overflow-x-auto"> @php
<table class="w-full text-sm text-left"> $start = max($preprocessing->currentPage() - 2, 1);
<thead class="sticky top-0 bg-gray-100"> $end = min($preprocessing->currentPage() + 2, $preprocessing->lastPage());
<tr class="bg-gray-100 text-gray-600"> @endphp
<th class="px-4 py-3">Destinasi</th>
<th class="px-4 py-3">Ulasan Mentah</th>
<th class="px-4 py-3">Ulasan Bersih</th>
</tr>
</thead>
<tbody> {{-- NUMBER DINAMIS --}}
@forelse($preprocessing as $row) @for ($i = $start; $i <= $end; $i++)
<tr class="border-b"> @if ($i == $preprocessing->currentPage())
<span class="px-3 py-1 bg-blue-600 text-white rounded-full">{{ $i }}</span>
@else
<a href="{{ $preprocessing->url($i) }}" class="px-3 py-1 hover:underline">
{{ $i }}
</a>
@endif
@endfor
<td class="px-4 py-3"> {{-- NEXT --}}
{{ $row->destinasi }} @if ($preprocessing->hasMorePages())
</td> <a href="{{ $preprocessing->nextPageUrl() }}"></a>
@else
<span class="text-gray-400"></span>
@endif
<td class="px-4 py-3"> {{-- LAST --}}
{{ $row->ulasan_asli }} @if ($preprocessing->currentPage() < $preprocessing->lastPage())
</td> <a href="{{ $preprocessing->url($preprocessing->lastPage()) }}">»</a>
@endif
<td class="px-4 py-3"> </div>
{{ $row->stemming }}
</td>
</tr> </div>
<script>
@empty document.getElementById("filterWisata").addEventListener("change", function () {
<tr> let wisata = this.value;
<td colspan="3" class="text-center py-6 text-gray-400">
Belum ada hasil preprocessing.
</td>
</tr>
@endforelse
</tbody>
fetch(`/ulasan?wisata=${wisata}`)
.then(res => res.text())
.then(html => {
document.querySelector("#tableContainer").innerHTML =
new DOMParser().parseFromString(html, "text/html")
.querySelector("#tableContainer").innerHTML;
});
});
</script>
@endforelse
</tbody>
</table> </table>
</div> </div>
</div> </div>
</div> </div>
@endsection @endsection

View File

@ -1,70 +0,0 @@
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()

143
scraper/preprocessing.py Normal file
View File

@ -0,0 +1,143 @@
import mysql.connector
import re
import math
from collections import Counter
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()
# =============================
# DATA TRAINING
# =============================
data_train = [
("bagus bersih indah nyaman sejuk", "positif"),
("indah nyaman segar bagus", "positif"),
("kotor jelek rusak mahal sempit", "negatif"),
("jalan rusak sempit mahal", "negatif")
]
# =============================
# TRAIN NAIVE BAYES
# =============================
def train_nb(data):
word_counts = {"positif": Counter(), "negatif": Counter()}
class_counts = {"positif": 0, "negatif": 0}
total_words = {"positif": 0, "negatif": 0}
for text, label in data:
words = text.split()
class_counts[label] += 1
word_counts[label].update(words)
total_words[label] += len(words)
return word_counts, class_counts, total_words
word_counts, class_counts, total_words = train_nb(data_train)
# =============================
# PREDICT
# =============================
def predict_nb(text):
words = text.split()
vocab = set(word_counts["positif"]).union(set(word_counts["negatif"]))
log_pos = math.log(class_counts["positif"] / sum(class_counts.values()))
log_neg = math.log(class_counts["negatif"] / sum(class_counts.values()))
for word in words:
prob_pos = (word_counts["positif"][word] + 1) / (total_words["positif"] + len(vocab))
prob_neg = (word_counts["negatif"][word] + 1) / (total_words["negatif"] + len(vocab))
log_pos += math.log(prob_pos)
log_neg += math.log(prob_neg)
if log_pos > log_neg:
return "Positif", log_pos
else:
return "Negatif", log_neg
# =============================
# HAPUS DATA LAMA (PENTING!)
# =============================
cursor.execute("DELETE FROM preprocessing_data")
cursor.execute("DELETE FROM hasil_analisis")
# =============================
# AMBIL DATA
# =============================
cursor.execute("SELECT wisata, ulasan FROM ulasan")
data = cursor.fetchall()
print("Total ulasan:", len(data))
# =============================
# PROSES
# =============================
for row in data:
wisata = row[0]
text = row[1]
if not text:
continue
# CLEANING
text_clean = re.sub(r'[^a-zA-Z\s]', '', text)
text_clean = text_clean.lower()
# TOKENIZING
tokens = text_clean.split()
text_token = ' '.join(tokens)
# STEMMING
text_stem = stemmer.stem(text_clean)
# PREDIKSI
sentimen, probabilitas = predict_nb(text_stem)
# INSERT PREPROCESSING
cursor.execute("""
INSERT INTO preprocessing_data
(wisata, ulasan_asli, cleaning, tokenizing, stemming)
VALUES (%s,%s,%s,%s,%s)
""",
(
wisata,
text,
text_clean,
text_token,
text_stem
))
# INSERT HASIL ANALISIS
cursor.execute("""
INSERT INTO hasil_analisis
(wisata, ulasan_terolah, hasil_preprocessing, sentimen, probabilitas)
VALUES (%s,%s,%s,%s,%s)
""", (wisata, text, text_stem, sentimen, probabilitas))
# =============================
# COMMIT SEKALI SAJA
# =============================
conn.commit()
print(" Preprocessing & Analisis Sentimen selesai")
conn.close()

View File

@ -1,214 +1,143 @@
from selenium import webdriver from selenium import webdriver
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.options import Options 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 mysql.connector
import time import time
from datetime import datetime from datetime import datetime
# =============================
# DATABASE # DATABASE
# =============================
conn = mysql.connector.connect( conn = mysql.connector.connect(
host="localhost", host="localhost",
database="sentara", database="sentara",
user="root", user="root",
password="" password=""
) )
cursor = conn.cursor() cursor = conn.cursor()
# =============================
# CHROME # CHROME
# =============================
options = Options() options = Options()
options.add_argument("--start-maximized") options.add_argument("--start-maximized")
driver = webdriver.Chrome(options=options) driver = webdriver.Chrome(options=options)
wait = WebDriverWait(driver, 15)
# =============================
# DESTINASI
# =============================
destinations = { destinations = {
"Pantai Papuma": "Pantai Papuma Jember", "Pantai Papuma":"https://www.google.com/maps/place/Pantai+Papuma+Jember",
"Pantai Watu Ulo": "Pantai Watu Ulo Jember", "Pantai Watu Ulo":"https://www.google.com/maps/place/Pantai+Watu+Ulo+Jember",
"Teluk Love": "Teluk Love Jember", "Teluk Love":"https://www.google.com/maps/place/Teluk+Love+Jember",
"Kebun Teh Gunung Gambir": "Kebun Teh Gunung Gambir Jember" "Kebun Teh Gunung Gambir":"https://www.google.com/maps/place/Kebun+Teh+Gunung+Gambir"
} }
# ============================= for wisata, url in destinations.items():
# LOOP DESTINASI
# =============================
for nama_wisata, keyword in destinations.items():
saved = 0 print("\nScraping:", wisata)
print("\n====================")
print("Scraping:", nama_wisata)
print("====================")
driver.get("https://www.google.com/maps") driver.get(url)
time.sleep(5) time.sleep(10)
# ============================= # =========================
# CARI TEMPAT # BUKA ULASAN (FIX)
# ============================= # =========================
try: try:
search = driver.find_element(By.ID, "searchboxinput") buttons = driver.find_elements(By.XPATH, "//button")
except:
search = driver.find_element(By.XPATH, "//input[@aria-label='Search Google Maps']")
search.clear() ketemu = False
search.send_keys(keyword)
driver.find_element(By.ID, "searchbox-searchbutton").click() for btn in buttons:
try:
label = btn.get_attribute("aria-label")
time.sleep(6) if label and ("ulasan" in label.lower() or "review" in label.lower()):
driver.execute_script("arguments[0].click();", btn)
time.sleep(5)
print("Berhasil buka ulasan")
ketemu = True
break
except:
pass
# ============================= if not ketemu:
# KLIK HASIL PERTAMA print("Tombol ulasan tidak ditemukan")
# ============================= continue
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: except Exception as e:
print("Tempat tidak ditemukan") print("Error buka ulasan:", e)
continue continue
# ============================= # =========================
# BUKA TAB REVIEW # SCROLL
# ============================= # =========================
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: try:
scrollable_div = driver.find_element(By.XPATH, "//div[@role='region']") scrollable_div = driver.find_element(By.XPATH, "//div[@role='region']")
except: except:
print("Container review tidak ditemukan") print("Container tidak ditemukan")
continue continue
last_count = 0 last_height = 0
same_count = 0
reviews = []
for i in range(200):
for i in range(50):
driver.execute_script( driver.execute_script(
"arguments[0].scrollTop = arguments[0].scrollHeight", "arguments[0].scrollTop = arguments[0].scrollHeight",
scrollable_div scrollable_div
) )
time.sleep(2) time.sleep(2)
reviews = driver.find_elements(By.XPATH, "//div[@data-review-id]") new_height = driver.execute_script(
"return arguments[0].scrollHeight",
scrollable_div
)
current_count = len(reviews) print("Scroll ke-", i)
print("Scroll", i, "Review:", current_count) if new_height == last_height:
print("Sudah mentok")
if current_count == last_count:
same_count += 1
else:
same_count = 0
if same_count >= 6:
print("✔ Semua review sudah termuat")
break break
last_count = current_count last_height = new_height
# =============================
# AMBIL DATA REVIEW
# =============================
for review in reviews:
# =========================
# EXPAND
# =========================
buttons = driver.find_elements(By.XPATH, "//button[contains(text(),'Selengkapnya')]")
for b in buttons:
try: try:
driver.execute_script("arguments[0].click();", b)
except:
pass
review_id = review.get_attribute("data-review-id") time.sleep(2)
if not review_id: # =========================
# AMBIL DATA
# =========================
reviews = driver.find_elements(By.XPATH, "//div[@data-review-id]")
print("Total review ditemukan:", len(reviews))
saved = 0
for review in reviews:
try:
reviewer = review.find_element(By.XPATH, ".//div[contains(@class,'d4r55')]").text
rating = review.find_element(By.XPATH, ".//span[@role='img']").get_attribute("aria-label")
ulasan = review.find_element(By.XPATH, ".//span[contains(@class,'wiI7pd')]").text
tanggal = review.find_element(By.XPATH, ".//span[contains(@class,'rsqaWe')]").text
if ulasan.strip() == "":
continue 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(""" cursor.execute("""
INSERT INTO ulasan INSERT INTO ulasan
(review_id,destinasi,reviewer,ulasan,rating,tanggal,scraping_date) (destinasi, reviewer, ulasan, rating, tanggal, scraping_date)
VALUES(%s,%s,%s,%s,%s,%s,%s) VALUES (%s,%s,%s,%s,%s,%s)
""", """, (
( wisata,
review_id,
nama_wisata,
reviewer, reviewer,
review_text, ulasan,
rating, rating,
review_date, tanggal,
datetime.now() datetime.now()
)) ))
@ -218,11 +147,9 @@ for nama_wisata, keyword in destinations.items():
except Exception as e: except Exception as e:
print("skip:", e) print("skip:", e)
print("Total tersimpan:", saved) print("Berhasil simpan:", saved)
print("\n====================")
print("SCRAPING SELESAI")
print("====================")
driver.quit() driver.quit()
conn.close() conn.close()
print("\nSCRAPING SELESAI")