Skip to content
Konsultasi

Scraper Jawaban Chat AI Nyangkut di Gema Prompt

Adityo Guni Waluyo

Capture scraper chat AI malah menangkap prompt sendiri. Solusinya: filter gema, klik Copy lalu baca clipboard, plus sidecar URL untuk recover jawaban.

Ringkasan

Scraper sempat menangkap gema prompt sendiri karena anchor cocok di bubble prompt, lalu diperbaiki dengan filter marker dan syarat panjang minimal. Karena DOM chat tervirtualisasi, ekstraksi dipindah ke clipboard: tombol Copy diklik Playwright lalu teksnya dibaca lewat clipboard API. File sidecar URL menyediakan mode recover, menguatkan prinsipnya: ambil data dari clipboard, bukan render, dan siapkan jalur pemulihan sejak awal.

Polling saya bilang jawabannya sudah selesai. Snapshot accessibility diambil, anchor "1. Judul (H1)" dicari, teks dipotong sampai sentinel. Tapi yang keluar bukan artikel, melainkan prompt saya sendiri yang dikembalikan utuh oleh halaman chat, lengkap dengan daftar instruksi di dalamnya. Saya cek layar: jawabannya ada, bubble-nya utuh. Yang tertangkap scraper malah bubble prompt saya.

Capture nyangkut di gema prompt

Dugaan pertama saya regex anchor-nya salah. Kenyataannya lebih jail: anchor itu memang ada di halaman, tapi di dalam bubble prompt, karena template prompt saya menyertakan contoh struktur artikel. Fungsi capture lama di qwen-article.py mengambil kemunculan terakhir anchor itu, dan saat model menawarkan dua versi sekaligus sambil bertanya balik, kemunculan "terakhir" bisa jatuh ke tempat yang salah. Kandidat capture yang terlalu pendek juga lolos jadi sampah.

Perbaikan pertamanya sederhana. Iterasi anchor dari belakang, tolak kandidat yang memuat marker khas prompt saya sendiri seperti "KONTEKS TOPIK" atau "bahan riset internal", lalu syarat panjang minimal 500 karakter. Kalau semua kandidat ditolak, capture dianggap gagal dan polling lanjut. Filter begini memang temuan lokal, tapi dia menutup satu celah besar: scraper tidak bisa membedakan jawaban dari gema prompt kalau hanya melihat struktur teks.

Clipboard, jalur data yang tidak berubah-ubah

Masalah kedua lebih mendasar. Jawaban panjang di chat Qwen dirender dengan virtualisasi: snapshot accessibility dan innerText hanya memuat potongan yang terlihat. Saya pernah menulis sisi virtualisasi DOM-nya; kali ini solusinya pindah jalur sepenuhnya. Tombol Copy di jendela jawaban saya klik lewat Playwright, lalu teks dibaca lewat navigator.clipboard.readText().

API ini kontraknya jelas. readText() mengembalikan Promise berisi salinan teks sistem clipboard [1]. Kalau clipboard kosong atau bukan teks, hasilnya string kosong, bukan error [1]. Kalau akses ditolak, muncul NotAllowedError [1]. Syarat keamanannya juga tegas: hanya jalan di secure context, dan spesifikasi mensyaratkan pengguna baru saja berinteraksi dengan halaman [2]. Dalam praktiknya browser mengizinkan baca bila izin clipboard-read sudah diberikan atau lewat prompt per operasi [2], dan justru klik tombol Copy itulah yang memenuhi syarat interaksi tersebut, sehingga pola klik dulu baca kemudian sah sekaligus andal.

Playwright membantu di sisi kepastian klik. Sebelum aksi, dia menunggu elemen lolos rangkaian pemeriksaan: terlihat, stabil, tidak tertutup elemen lain, dan enabled [4]. Stabil di sini konkret: bounding box elemen sama selama dua frame animasi berturut-turut [4]. Locator-nya juga strict; operasi melempar exception kalau ada lebih dari satu elemen yang cocok [3], jadi salah klik tombol copy milik bubble lain tertangkap sebagai error, bukan data diam-diam salah.

Hasil akhirnya di skrip: urutan polling dibalik jadi clipboard dulu, snapshot jadi cadangan. Teks yang didapat markdown mentah tanpa nomor baris hasil virtualisasi, dan langsung lolos validasi panjang minimum 300 karakter.

Sidecar URL dan mode recover

Kegagalan terakhir yang menyisakan luka: jawaban panjang berhasil dibuat Qwen, tapi skrip mati sebelum sempat menarik hasilnya. Kirim ulang prompt berarti bayar kuota dua kali untuk jawaban yang sebenarnya sudah ada.

Solusinya murah. Begitu prompt terkirim dan URL chat diketahui, skrip menulis file sidecar berekstensi .url sebelum mulai polling. Saat generate dipanggil lagi dan sidecar itu ada, skrip tidak mengirim apa pun; dia buka chat lama dan menarik jawabannya lewat jalur clipboard yang sama, lewat subcommand recover dengan timeout bawaan 240 detik. Penutupan tab juga dipindah ke blok finally, jadi tab tidak nyangkut saat proses gagal di tengah.

Pola ini yang sekarang saya pegang untuk semua ekstraksi dari UI chat: DOM itu lapisan presentasi, clipboard itu jalur data. Yang butuh data stabil sebaiknya ambil dari data, bukan dari render, dan sediakan jalur pemulihan sejak awal, bukan setelah jawaban panjang menguap.

---

Sumber:

[1] https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/readText [2] https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API [3] https://playwright.dev/docs/locators [4] https://playwright.dev/docs/actionability

Artikel terkait