From 5fa8005eb1b1765d70e3dbf313ce86d7edc9f23b Mon Sep 17 00:00:00 2001 From: MufridaFaraDiani27 Date: Fri, 13 Mar 2026 22:40:08 +0700 Subject: [PATCH] update fitur --- app/Http/Controllers/DashboardController.php | 3 +- app/Http/Controllers/UlasanController.php | 33 ++- app/Models/Ulasan.php | 4 +- ...2_11_071732_create_ulasan_mentah_table.php | 2 +- resources/js/app.js | 4 +- resources/views/auth/login.blade.php | 2 +- resources/views/auth/register.blade.php | 2 +- resources/views/dashboard.blade.php | 84 ++++++- resources/views/data-ulasan.blade.php | 153 ------------ resources/views/rekomendasi/index.blade.php | 74 +++--- resources/views/ulasan/index.blade.php | 99 ++++---- routes/web.php | 2 +- scraper/prepocessing.py | 70 ++++++ scraper/scraping_pipeline.py | 228 ++++++++++++++++++ 14 files changed, 507 insertions(+), 253 deletions(-) delete mode 100644 resources/views/data-ulasan.blade.php create mode 100644 scraper/prepocessing.py create mode 100644 scraper/scraping_pipeline.py diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php index 4f1931a..54a71d4 100644 --- a/app/Http/Controllers/DashboardController.php +++ b/app/Http/Controllers/DashboardController.php @@ -14,7 +14,8 @@ public function index() 'negatif_persen' => 20, 'netral_persen' => 12, ]; + $lastUpdate = '27 Februari 2026, 22:45'; - return view('dashboard', compact('stats')); + return view('dashboard', compact('stats', 'lastUpdate')); } } diff --git a/app/Http/Controllers/UlasanController.php b/app/Http/Controllers/UlasanController.php index e42f83f..d79f7ee 100644 --- a/app/Http/Controllers/UlasanController.php +++ b/app/Http/Controllers/UlasanController.php @@ -31,18 +31,31 @@ public function index() // Tombol ambil data terbaru - public function ambilData() - { - // nanti scraping otomatis disini (sementara dummy) - return redirect()->route('ulasan.index'); - } + public function ambilData() +{ + $pythonPath = '"C:\\Users\\Mufrida Farah\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"'; + $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 public function analisisData() - { - // jalankan python naive bayes - shell_exec("python sentimen.py"); +{ + $pythonPath = '"C:\\Users\\Mufrida Farah\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"'; - 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'); +} } diff --git a/app/Models/Ulasan.php b/app/Models/Ulasan.php index 75c696f..da7e75f 100644 --- a/app/Models/Ulasan.php +++ b/app/Models/Ulasan.php @@ -9,11 +9,11 @@ class Ulasan extends Model { use HasFactory; - protected $table = 'ulasan_mentah'; + protected $table = 'ulasan'; protected $fillable = [ 'nama_wisata', - 'reviewer_name', + 'reviewer', 'rating', 'ulasan', 'tanggal', diff --git a/database/migrations/2026_02_11_071732_create_ulasan_mentah_table.php b/database/migrations/2026_02_11_071732_create_ulasan_mentah_table.php index 6f70989..8a0f1f5 100644 --- a/database/migrations/2026_02_11_071732_create_ulasan_mentah_table.php +++ b/database/migrations/2026_02_11_071732_create_ulasan_mentah_table.php @@ -18,7 +18,7 @@ public function up(): void $table->integer('rating')->nullable(); $table->text('ulasan'); $table->string('tanggal')->nullable(); - $table->datetime('scraping_date')->nullable(); + $table->datetime('gununggambir_date')->nullable(); $table->boolean('is_processed')->default(0); $table->timestamps(); }); diff --git a/resources/js/app.js b/resources/js/app.js index 8f52e03..05a0b18 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -43,8 +43,8 @@ document.addEventListener("DOMContentLoaded", function () { labels: [ "Papuma", "Watu Ulo", - "Sukorambi", - "Air Terjun Tancak Tulis", + "teluklove", + "Kebun Teh Gunung Gambir ", ], datasets: [ { diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index b021e79..d3405ce 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -81,7 +81,7 @@ class="slide hidden absolute inset-0 w-full h-full object-cover"> - diff --git a/resources/views/auth/register.blade.php b/resources/views/auth/register.blade.php index 93410ee..340141c 100644 --- a/resources/views/auth/register.blade.php +++ b/resources/views/auth/register.blade.php @@ -100,7 +100,7 @@ class="slide hidden absolute inset-0 w-full h-full object-cover"> - diff --git a/resources/views/dashboard.blade.php b/resources/views/dashboard.blade.php index afc196c..62d5ae5 100644 --- a/resources/views/dashboard.blade.php +++ b/resources/views/dashboard.blade.php @@ -5,20 +5,35 @@
{{-- Header --}} -
+
+

Dashboard

- +
+ + +
+ --> + + + + + + + + +
{{-- Statistik Cards --}} @@ -55,14 +70,13 @@ class="bg-white border border-gray-300 text-sm rounded-lg px-4 py-2 shadow-sm
{{-- Chart Section --}} -
+

Distribusi Sentimen

-
@@ -73,12 +87,60 @@ class="bg-white border border-gray-300 text-sm rounded-lg px-4 py-2 shadow-sm

Sentimen per Destinasi

-
+
{{-- TUTUP GRID CHART --}} + + +{{-- Section Bawah --}} +
+ + +
+

+ Total Data per Destinasi +

+ +
+
+ Pantai Papuma + 120 ulasan +
+
+ Pantai Watu Ulo + 85 ulasan +
+
+ Teluk Love (Payangan) + 95 ulasan +
+
+ Kebun Teh Gunung Gambir + 60 ulasan +
+
+
+ + + + +
+ +
+ + +
diff --git a/resources/views/data-ulasan.blade.php b/resources/views/data-ulasan.blade.php deleted file mode 100644 index 7119989..0000000 --- a/resources/views/data-ulasan.blade.php +++ /dev/null @@ -1,153 +0,0 @@ -@extends('layouts.sentara') - -@section('content') - -
- - {{-- HEADER --}} -
-

- Data Ulasan Mentah -

- - -
- - {{-- BUTTON SCRAPING --}} -
- - Ambil Data Terbaru - -
- - {{-- DATA MENTAH TABLE --}} -
- -

- Hasil Data Scraping -

- -
- - - - - - - - - - - - @forelse($ulasan as $item) - - - - - - - - - - @empty - - - - @endforelse - - -
DestinasiReviewerUlasanTanggal
- {{ $item->nama_wisata }} - - {{ $item->reviewer_name }} - - {{ Str::limit($item->ulasan, 80) }} - - {{ $item->tanggal }} -
- Data belum tersedia. Silakan scraping dulu. -
-
- -
- - - {{-- BUTTON ANALISIS --}} - - - - {{-- HASIL ANALISIS --}} - @if(isset($hasilAnalisis)) - - - - @endif - -
- -@endsection diff --git a/resources/views/rekomendasi/index.blade.php b/resources/views/rekomendasi/index.blade.php index f8e8aa7..0e93d72 100644 --- a/resources/views/rekomendasi/index.blade.php +++ b/resources/views/rekomendasi/index.blade.php @@ -30,40 +30,60 @@ class="bg-white border border-gray-300 text-sm rounded-lg px-4 py-2 shadow-sm -
+
- -
-

- Isu Utama: -

-

- {{ $isuUtama }} -

+ +
+

+ Isu Utama +

+ +
+ + Kebersihan + + + Parkir +
+
- -
-

- Kata Kunci Dominan: -

-

- {{ $kataDominan }} -

-
+ +
+

+ Kata Kunci Dominan +

- -
-

- Saran: -

-

- {{ $saran }} -

-
+
    +
  • + kotor + 15x +
  • +
  • + parkir + 10x +
  • +
  • + mahal + 7x +
  • +
+
+ +
+

+ Saran Perbaikan +

+ +

+ Perlu peningkatan kebersihan area wisata serta evaluasi sistem + pengelolaan parkir untuk meningkatkan kenyamanan pengunjung. +

+
+ @endsection diff --git a/resources/views/ulasan/index.blade.php b/resources/views/ulasan/index.blade.php index 4e35226..6739824 100644 --- a/resources/views/ulasan/index.blade.php +++ b/resources/views/ulasan/index.blade.php @@ -16,8 +16,8 @@ class="bg-white border border-gray-300 text-sm rounded-lg px-4 py-2 shadow-sm - - + +
@@ -39,33 +39,37 @@ class="bg-blue-600 hover:bg-blue-700 text-white font-semibold Data Ulasan Mentah -
+
+ + - @forelse($mentah as $row) - - - - - - - @empty - - - - @endforelse - +@forelse($mentah as $data) + + + + + + + + +@empty + + + +@endforelse +
Destinasi ReviewerRating Ulasan Tanggal
{{ $row->nama_wisata }}{{ $row->reviewer_name }}{{ $row->ulasan }}{{ $row->tanggal }}
- Belum ada data mentah. -
{{ $data->destinasi }}{{ $data->reviewer }}{{ $data->rating }}{{ $data->ulasan }}{{ $data->tanggal }}
+ Belum ada data mentah. +
@@ -78,7 +82,7 @@ class="bg-blue-600 hover:bg-blue-700 text-white font-semibold
@@ -91,33 +95,42 @@ class="bg-green-600 hover:bg-green-700 text-white font-semibold
- - - - - - - + + + + + + + - - @forelse($preprocessing as $row) - - + + @forelse($preprocessing as $row) + - - + - - - - @empty - - - - @endforelse - + + + + + + + @empty + + + + + + @endforelse +
Ulasan AsliCleaning + Case FoldingUlasan Bersih Final (Stemming)
DestinasiUlasan MentahUlasan Bersih
{{ $row->ulasan_asli }}
{{ $row->cleaning }} + {{ $row->destinasi }} + {{ $row->stemming }}
- Belum ada hasil preprocessing. -
+ {{ $row->ulasan_asli }} + + {{ $row->stemming }} +
+ Belum ada hasil preprocessing. +
diff --git a/routes/web.php b/routes/web.php index 361c61f..7eef91f 100644 --- a/routes/web.php +++ b/routes/web.php @@ -31,7 +31,7 @@ Route::get('/ulasan', [UlasanController::class, 'index'])->name('ulasan.index'); Route::post('/ulasan/ambil', [UlasanController::class, 'ambilData'])->name('ulasan.ambil'); 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'); Route::get('/rekomendasi', [RekomendasiController::class, 'index']) ->name('rekomendasi.index');; diff --git a/scraper/prepocessing.py b/scraper/prepocessing.py new file mode 100644 index 0000000..40c5876 --- /dev/null +++ b/scraper/prepocessing.py @@ -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() \ No newline at end of file diff --git a/scraper/scraping_pipeline.py b/scraper/scraping_pipeline.py new file mode 100644 index 0000000..220b2b7 --- /dev/null +++ b/scraper/scraping_pipeline.py @@ -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() \ No newline at end of file