update fitur
This commit is contained in:
parent
5fa8005eb1
commit
dc393edbba
|
|
@ -10,16 +10,16 @@ class AnalisisController extends Controller
|
|||
public function index(Request $request)
|
||||
{
|
||||
// Dropdown list destinasi unik
|
||||
$destinasiList = HasilAnalisis::select('nama_wisata')
|
||||
$wisataList = HasilAnalisis::select('wisata')
|
||||
->distinct()
|
||||
->pluck('nama_wisata');
|
||||
->pluck('wisata');
|
||||
|
||||
// Query utama tabel
|
||||
$query = HasilAnalisis::query();
|
||||
|
||||
// Filter kalau user pilih destinasi tertentu
|
||||
if ($request->filled('destinasi')) {
|
||||
$query->where('nama_wisata', $request->destinasi);
|
||||
if ($request->filled('wisata')) {
|
||||
$query->where('wisata', $request->wisata);
|
||||
}
|
||||
|
||||
// Data hasil analisis (pakai paginate biar rapi)
|
||||
|
|
@ -27,7 +27,7 @@ public function index(Request $request)
|
|||
|
||||
return view('analisis.index', compact(
|
||||
'hasil',
|
||||
'destinasiList'
|
||||
'wisataList'
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,19 +3,113 @@
|
|||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
public function index()
|
||||
public function index(Request $request)
|
||||
{
|
||||
$stats = [
|
||||
'total' => 520,
|
||||
'positif_persen' => 68,
|
||||
'negatif_persen' => 20,
|
||||
'netral_persen' => 12,
|
||||
];
|
||||
$lastUpdate = '27 Februari 2026, 22:45';
|
||||
$wisata = $request->wisata;
|
||||
|
||||
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'
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,16 +10,16 @@ class RekomendasiController extends Controller
|
|||
public function index(Request $request)
|
||||
{
|
||||
// dropdown destinasi unik
|
||||
$destinasiList = HasilAnalisis::select('nama_wisata')
|
||||
$destinasiList = HasilAnalisis::select('wisata')
|
||||
->distinct()
|
||||
->pluck('nama_wisata');
|
||||
->pluck('scraping_pipeline.py');
|
||||
|
||||
// query utama
|
||||
$query = HasilAnalisis::query();
|
||||
|
||||
// filter destinasi jika dipilih
|
||||
if ($request->filled('destinasi')) {
|
||||
$query->where('nama_wisata', $request->destinasi);
|
||||
$query->where('wisata', $request->destinasi);
|
||||
}
|
||||
|
||||
// ambil ulasan negatif
|
||||
|
|
|
|||
|
|
@ -4,58 +4,51 @@
|
|||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Ulasan;
|
||||
// use App\Models\HasilSentimen;
|
||||
use App\Models\PreprocessingData;
|
||||
|
||||
|
||||
class UlasanController extends Controller
|
||||
{
|
||||
// public function index()
|
||||
// {
|
||||
// $mentah = Ulasan::where('is_processed', 0)->get();
|
||||
// // $hasil = HasilSentimen::latest()->get();
|
||||
public function index(Request $request)
|
||||
{
|
||||
$wisata = $request->wisata;
|
||||
|
||||
// // return view('ulasan.index', compact('mentah', 'hasil'));
|
||||
// }
|
||||
|
||||
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'));
|
||||
$query = Ulasan::query();
|
||||
if ($request->filled('search')) {
|
||||
$query->where('ulasan', 'LIKE', '%' . $request->search . '%');
|
||||
}
|
||||
|
||||
if ($wisata) {
|
||||
$query->where('wisata', 'LIKE', '%' . $wisata . '%');
|
||||
}
|
||||
|
||||
// Tombol ambil data terbaru
|
||||
public function ambilData()
|
||||
{
|
||||
$pythonPath = '"C:\\Users\\Mufrida Farah\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"';
|
||||
$scriptPath = base_path('scraper/scraping_pipeline.py');
|
||||
// ✅ PAGINATION 50 DATA
|
||||
$mentah = $query->latest()->paginate(50)->withQueryString();
|
||||
|
||||
$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')
|
||||
->with('success', 'Data berhasil diambil dari Google Maps');
|
||||
}
|
||||
public function ambilData()
|
||||
{
|
||||
$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()
|
||||
{
|
||||
$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";
|
||||
|
||||
shell_exec($command);
|
||||
|
||||
return redirect()->route('ulasan.index')
|
||||
->with('success','Preprocessing berhasil dilakukan');
|
||||
}
|
||||
}
|
||||
return redirect()->route('ulasan.index')
|
||||
->with('success','Analisis berhasil');
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@ class HasilAnalisis extends Model
|
|||
protected $table = 'hasil_analisis';
|
||||
|
||||
protected $fillable = [
|
||||
'nama_wisata',
|
||||
'wisata',
|
||||
'ulasan_terolah',
|
||||
'sentimen',
|
||||
'probabilitas'
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ class HasilSentimen extends Model
|
|||
protected $table = 'hasil_sentimen';
|
||||
|
||||
protected $fillable = [
|
||||
'nama_wisata',
|
||||
'wisata',
|
||||
'ulasan',
|
||||
'sentimen'
|
||||
];
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ class Ulasan extends Model
|
|||
protected $table = 'ulasan';
|
||||
|
||||
protected $fillable = [
|
||||
'nama_wisata',
|
||||
'wisata',
|
||||
'reviewer',
|
||||
'rating',
|
||||
'ulasan',
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ public function up(): void
|
|||
{
|
||||
Schema::create('ulasan_mentah', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('nama_wisata');
|
||||
$table->string('wisata');
|
||||
$table->string('reviewer_name')->nullable();
|
||||
$table->integer('rating')->nullable();
|
||||
$table->text('ulasan');
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ public function up(): void
|
|||
{
|
||||
Schema::create('hasil_analisis', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('nama_wisata');
|
||||
$table->string('wisata');
|
||||
$table->text('ulasan_terolah');
|
||||
$table->string('sentimen');
|
||||
$table->float('probabilitas');
|
||||
|
|
|
|||
|
|
@ -26,3 +26,10 @@ .sidebar-menu .sidebar-link {
|
|||
.sidebar-menu .sidebar-link:hover {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.line-clamp-3 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
|
@ -12,16 +12,16 @@
|
|||
|
||||
<!-- Dropdown Filter -->
|
||||
<form method="GET" action="{{ route('analisis.index') }}">
|
||||
<select name="destinasi"
|
||||
<select name="wisata"
|
||||
onchange="this.form.submit()"
|
||||
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 value="">Semua Destinasi</option>
|
||||
|
||||
@foreach($destinasiList as $dest)
|
||||
@foreach($wisataList as $dest)
|
||||
<option value="{{ $dest }}"
|
||||
{{ request('destinasi') == $dest ? 'selected' : '' }}>
|
||||
{{ request('wisata') == $dest ? 'selected' : '' }}>
|
||||
{{ $dest }}
|
||||
</option>
|
||||
@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">
|
||||
<thead>
|
||||
<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">Sentimen</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 -->
|
||||
<td class="px-4 py-3 font-medium">
|
||||
{{ $row->nama_wisata }}
|
||||
{{ $row->wisata }}
|
||||
</td>
|
||||
|
||||
<!-- Ulasan -->
|
||||
|
|
|
|||
|
|
@ -2,147 +2,145 @@
|
|||
|
||||
@section('content')
|
||||
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<div class="max-w-6xl mx-auto space-y-8">
|
||||
|
||||
{{-- Header --}}
|
||||
<div class="flex justify-between items-start mb-8">
|
||||
<div>
|
||||
<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">
|
||||
<!-- HEADER -->
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="text-3xl font-bold text-gray-800">Data Ulasan</h2>
|
||||
|
||||
<select id="filterWisata" class="border rounded px-3 py-2 text-sm">
|
||||
<option value="">Semua Destinasi</option>
|
||||
<option value="papuma">Pantai Papuma</option>
|
||||
<option value="watuulo">Pantai Watu Ulo</option>
|
||||
<option value="teluklove">Teluk Love (Payangan)</option>
|
||||
<option value="gununggambir">Kebun Teh Gunung Gambir </option>
|
||||
|
||||
<option value="Pantai Papuma" {{ request('wisata')=='Pantai Papuma'?'selected':'' }}>Pantai Papuma</option>
|
||||
<option value="Pantai Watu Ulo" {{ request('wisata')=='Pantai Watu Ulo'?'selected':'' }}>Pantai Watu Ulo</option>
|
||||
<option value="Teluk Love (Payangan)" {{ request('wisata')=='Teluk Love (Payangan)'?'selected':'' }}>Teluk Love</option>
|
||||
<option value="Kebun Teh Gunung Gambir" {{ request('wisata')=='Kebun Teh Gunung Gambir'?'selected':'' }}>Kebun Teh Gunung Gambir</option>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
{{-- Statistik Cards --}}
|
||||
<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 -->
|
||||
<!-- ================= TABLE ================= -->
|
||||
<div class="bg-white rounded-xl shadow p-6">
|
||||
<h3 class="font-semibold mb-4 text-gray-700">
|
||||
Distribusi Sentimen
|
||||
</h3>
|
||||
<div class="h-[280px] flex justify-center items-center">
|
||||
<canvas id="pieChart"></canvas>
|
||||
|
||||
<h3 class="font-semibold mb-4">Data Ulasan Mentah</h3>
|
||||
|
||||
<div class="border rounded overflow-hidden">
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- 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">
|
||||
<h3 class="font-semibold mb-4 text-gray-700">
|
||||
Sentimen per Destinasi
|
||||
</h3>
|
||||
<div class="h-[280px]">
|
||||
<canvas id="barChart"></canvas>
|
||||
<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>
|
||||
</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> {{-- 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>
|
||||
|
||||
|
||||
|
||||
@endsection
|
||||
@endsection
|
||||
|
|
@ -6,136 +6,250 @@
|
|||
|
||||
<!-- HEADER -->
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="text-3xl font-bold text-gray-800">
|
||||
Data Ulasan
|
||||
</h2>
|
||||
<h2 class="text-3xl font-bold text-gray-800">Data Ulasan</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>Pantai Watu Ulo</option>
|
||||
<option>Teluk Love (Payangan)</option>
|
||||
<option>Kebun Teh Gunung Gambir </option>
|
||||
<select id="filterWisata" class="border rounded px-3 py-2 text-sm">
|
||||
<option value="">Semua Destinasi</option>
|
||||
<option value="Pantai Papuma" {{ request('wisata')=='Pantai Papuma'?'selected':'' }}>Pantai Papuma</option>
|
||||
<option value="Pantai Watu Ulo" {{ request('wisata')=='Pantai Watu Ulo'?'selected':'' }}>Pantai Watu Ulo</option>
|
||||
<option value="Teluk Love (Payangan)" {{ request('wisata')=='Teluk Love (Payangan)'?'selected':'' }}>Teluk Love</option>
|
||||
<option value="Kebun Teh Gunung Gambir" {{ request('wisata')=='Kebun Teh Gunung Gambir'?'selected':'' }}>Kebun Teh Gunung Gambir</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- BUTTON AMBIL DATA -->
|
||||
<div>
|
||||
<form action="{{ route('ulasan.ambil') }}" method="POST">
|
||||
@csrf
|
||||
<button
|
||||
class="bg-blue-600 hover:bg-blue-700 text-white font-semibold
|
||||
px-6 py-2 rounded-lg shadow">
|
||||
Ambil Data Terbaru
|
||||
</button>
|
||||
</form>
|
||||
<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>
|
||||
|
||||
<!-- ================= 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>
|
||||
|
||||
<!-- TABLE DATA MENTAH -->
|
||||
<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>
|
||||
|
||||
<div class="overflow-x-auto max-h-[400px] overflow-y-auto">
|
||||
<table class="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">Rating</th>
|
||||
<th class="px-4 py-3">Ulasan</th>
|
||||
<th class="px-4 py-3">Tanggal</th>
|
||||
|
||||
<!-- INFO -->
|
||||
<div class="text-sm text-gray-500 mt-4 text-center">
|
||||
Showing {{ $mentah->firstItem() }} to {{ $mentah->lastItem() }}
|
||||
of {{ $mentah->total() }} results
|
||||
</div>
|
||||
|
||||
<!-- PAGINATION -->
|
||||
<div class="flex justify-center mt-6 items-center gap-2 text-sm">
|
||||
|
||||
{{-- 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>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
@forelse($mentah as $data)
|
||||
<tr>
|
||||
<td class="px-4 py-3">{{ $data->destinasi }}</td>
|
||||
<td class="px-4 py-3">{{ $data->reviewer }}</td>
|
||||
<td class="px-4 py-3">{{ $data->rating }}</td>
|
||||
<td class="px-4 py-3">{{ $data->ulasan }}</td>
|
||||
<td class="px-4 py-3">{{ $data->tanggal }}</td>
|
||||
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="6" class="text-center py-6 text-gray-400">
|
||||
Belum ada data mentah.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</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>
|
||||
<div class="flex justify-center mt-4 text-sm">
|
||||
|
||||
<!-- INFO -->
|
||||
<div class="text-sm text-gray-500 mt-4 text-center">
|
||||
Showing {{ $preprocessing->firstItem() }} to {{ $preprocessing->lastItem() }}
|
||||
of {{ $preprocessing->total() }} results
|
||||
</div>
|
||||
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<!-- PAGINATION -->
|
||||
<div class="flex justify-center mt-6 items-center gap-2 text-sm">
|
||||
|
||||
<!-- BUTTON PREPROCESSING -->
|
||||
<div>
|
||||
<form action="{{ route('ulasan.analisis') }}" method="POST">
|
||||
@csrf
|
||||
<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>
|
||||
{{-- FIRST --}}
|
||||
@if ($preprocessing->currentPage() > 1)
|
||||
<a href="{{ $preprocessing->url(1) }}">«</a>
|
||||
@endif
|
||||
|
||||
<!-- HASIL PREPROCESSING -->
|
||||
<div class="bg-white rounded-xl shadow p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-700 mb-4">
|
||||
Hasil Preprocessing Data
|
||||
</h3>
|
||||
{{-- PREV --}}
|
||||
@if ($preprocessing->onFirstPage())
|
||||
<span class="text-gray-400">‹</span>
|
||||
@else
|
||||
<a href="{{ $preprocessing->previousPageUrl() }}">‹</a>
|
||||
@endif
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm text-left">
|
||||
<thead class="sticky top-0 bg-gray-100">
|
||||
<tr class="bg-gray-100 text-gray-600">
|
||||
<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>
|
||||
@php
|
||||
$start = max($preprocessing->currentPage() - 2, 1);
|
||||
$end = min($preprocessing->currentPage() + 2, $preprocessing->lastPage());
|
||||
@endphp
|
||||
|
||||
<tbody>
|
||||
@forelse($preprocessing as $row)
|
||||
<tr class="border-b">
|
||||
{{-- NUMBER DINAMIS --}}
|
||||
@for ($i = $start; $i <= $end; $i++)
|
||||
@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">
|
||||
{{ $row->destinasi }}
|
||||
</td>
|
||||
{{-- NEXT --}}
|
||||
@if ($preprocessing->hasMorePages())
|
||||
<a href="{{ $preprocessing->nextPageUrl() }}">›</a>
|
||||
@else
|
||||
<span class="text-gray-400">›</span>
|
||||
@endif
|
||||
|
||||
<td class="px-4 py-3">
|
||||
{{ $row->ulasan_asli }}
|
||||
</td>
|
||||
{{-- LAST --}}
|
||||
@if ($preprocessing->currentPage() < $preprocessing->lastPage())
|
||||
<a href="{{ $preprocessing->url($preprocessing->lastPage()) }}">»</a>
|
||||
@endif
|
||||
|
||||
<td class="px-4 py-3">
|
||||
{{ $row->stemming }}
|
||||
</td>
|
||||
</div>
|
||||
|
||||
</tr>
|
||||
|
||||
@empty
|
||||
<tr>
|
||||
|
||||
<td colspan="3" class="text-center py-6 text-gray-400">
|
||||
Belum ada hasil preprocessing.
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</div>
|
||||
<script>
|
||||
document.getElementById("filterWisata").addEventListener("change", function () {
|
||||
let wisata = this.value;
|
||||
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
@endsection
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -1,214 +1,143 @@
|
|||
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"
|
||||
"Pantai Papuma":"https://www.google.com/maps/place/Pantai+Papuma+Jember",
|
||||
"Pantai Watu Ulo":"https://www.google.com/maps/place/Pantai+Watu+Ulo+Jember",
|
||||
"Teluk Love":"https://www.google.com/maps/place/Teluk+Love+Jember",
|
||||
"Kebun Teh Gunung Gambir":"https://www.google.com/maps/place/Kebun+Teh+Gunung+Gambir"
|
||||
}
|
||||
|
||||
# =============================
|
||||
# LOOP DESTINASI
|
||||
# =============================
|
||||
for nama_wisata, keyword in destinations.items():
|
||||
for wisata, url in destinations.items():
|
||||
|
||||
saved = 0
|
||||
print("\n====================")
|
||||
print("Scraping:", nama_wisata)
|
||||
print("====================")
|
||||
print("\nScraping:", wisata)
|
||||
|
||||
driver.get("https://www.google.com/maps")
|
||||
time.sleep(5)
|
||||
driver.get(url)
|
||||
time.sleep(10)
|
||||
|
||||
# =============================
|
||||
# CARI TEMPAT
|
||||
# =============================
|
||||
# =========================
|
||||
# BUKA ULASAN (FIX)
|
||||
# =========================
|
||||
try:
|
||||
search = driver.find_element(By.ID, "searchboxinput")
|
||||
except:
|
||||
search = driver.find_element(By.XPATH, "//input[@aria-label='Search Google Maps']")
|
||||
buttons = driver.find_elements(By.XPATH, "//button")
|
||||
|
||||
search.clear()
|
||||
search.send_keys(keyword)
|
||||
ketemu = False
|
||||
|
||||
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
|
||||
|
||||
# =============================
|
||||
# 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)
|
||||
if not ketemu:
|
||||
print("Tombol ulasan tidak ditemukan")
|
||||
continue
|
||||
|
||||
except:
|
||||
print("Tempat tidak ditemukan")
|
||||
except Exception as e:
|
||||
print("Error buka ulasan:", e)
|
||||
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
|
||||
# =============================
|
||||
# =========================
|
||||
# SCROLL
|
||||
# =========================
|
||||
try:
|
||||
scrollable_div = driver.find_element(By.XPATH, "//div[@role='region']")
|
||||
except:
|
||||
print("Container review tidak ditemukan")
|
||||
print("Container tidak ditemukan")
|
||||
continue
|
||||
|
||||
last_count = 0
|
||||
same_count = 0
|
||||
reviews = []
|
||||
|
||||
for i in range(200):
|
||||
last_height = 0
|
||||
|
||||
for i in range(50):
|
||||
driver.execute_script(
|
||||
"arguments[0].scrollTop = arguments[0].scrollHeight",
|
||||
scrollable_div
|
||||
)
|
||||
|
||||
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 current_count == last_count:
|
||||
same_count += 1
|
||||
else:
|
||||
same_count = 0
|
||||
|
||||
if same_count >= 6:
|
||||
print("✔ Semua review sudah termuat")
|
||||
if new_height == last_height:
|
||||
print("Sudah mentok")
|
||||
break
|
||||
|
||||
last_count = current_count
|
||||
|
||||
# =============================
|
||||
# AMBIL DATA REVIEW
|
||||
# =============================
|
||||
for review in reviews:
|
||||
last_height = new_height
|
||||
|
||||
# =========================
|
||||
# EXPAND
|
||||
# =========================
|
||||
buttons = driver.find_elements(By.XPATH, "//button[contains(text(),'Selengkapnya')]")
|
||||
for b in buttons:
|
||||
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
|
||||
|
||||
# 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,
|
||||
(destinasi, reviewer, ulasan, rating, tanggal, scraping_date)
|
||||
VALUES (%s,%s,%s,%s,%s,%s)
|
||||
""", (
|
||||
wisata,
|
||||
reviewer,
|
||||
review_text,
|
||||
ulasan,
|
||||
rating,
|
||||
review_date,
|
||||
tanggal,
|
||||
datetime.now()
|
||||
))
|
||||
|
||||
|
|
@ -218,11 +147,9 @@ for nama_wisata, keyword in destinations.items():
|
|||
except Exception as e:
|
||||
print("skip:", e)
|
||||
|
||||
print("Total tersimpan:", saved)
|
||||
|
||||
print("\n====================")
|
||||
print("SCRAPING SELESAI")
|
||||
print("====================")
|
||||
print("Berhasil simpan:", saved)
|
||||
|
||||
driver.quit()
|
||||
conn.close()
|
||||
conn.close()
|
||||
|
||||
print("\nSCRAPING SELESAI")
|
||||
Loading…
Reference in New Issue