Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Scraper Studio

alt text

Web scraper dengan Next.js (frontend) + Python Flask (backend). Fokus utamanya ngambil struktur Google Form publik — pertanyaan, tipe, opsi, dan entry.xxxx ID-nya — plus scraper generik buat halaman web biasa.

┌─────────────────┐   /api/*    ┌──────────────────┐   HTTP    ┌────────────┐
│  Next.js :3000  │ ──rewrite─> │  Flask :5000     │ ────────> │  Target    │
│  React UI       │ <────────── │  fetch + parse   │ <──────── │  page      │
└─────────────────┘    JSON     └──────────────────┘           └────────────┘

Frontend nggak pernah manggil Flask langsung dari browser. Semua lewat rewrite di next.config.mjs, jadi requestnya same-origin — nggak ada CORS preflight, dan alamat backend nggak ikut ke-bundle ke client.


Yang bisa diambil

Google Form

Form publik ngirim seluruh definisinya ke browser lewat variabel JavaScript FB_PUBLIC_LOAD_DATA_ — rendernya emang di sisi client. Parser di sini baca variabel itu, bukan HTML hasil render, jadi yang kebaca struktur aslinya.

Data Keterangan
Judul & deskripsi form
entry_id Format entry.123456789, dipakai buat prefill link
Tipe field 14 tipe: short answer, paragraph, radio, dropdown, checkbox, linear scale, grid, date, time, file upload, dll
Wajib / tidak
Opsi pilihan Termasuk nandain opsi "Other"
Grid Dipecah jadi satu entry_id per baris
Linear scale Beserta label ujung min/max

Export ke CSV atau JSON langsung dari UI.

Halaman generik

Metadata (title, description, OpenGraph), heading, link, gambar, tabel, dan JSON-LD. Bisa juga pakai CSS selector sendiri:

{
  "url": "https://example.com",
  "options": {
    "rules": {
      "harga":  { "selector": ".price" },
      "produk": { "selector": "h2.title", "all": true },
      "gambar": { "selector": "img.main", "attr": "src" }
    }
  }
}

Yang TIDAK bisa (dan kenapa)

Jawaban/response Google Form nggak bisa diambil. Bukan karena belum dibikin — datanya emang nggak pernah dikirim ke halaman publik. Response cuma bisa diakses pemilik form lewat Google Sheets atau Forms API pakai OAuth. Tool apa pun yang ngaku bisa nyedot response dari form orang lain itu bohong, atau minta kredensial kamu.

Yang diambil di sini murni struktur form yang emang publik — sama persis kayak yang dikirim ke browser siapa pun yang buka link formnya.


Instalasi

Yang dibutuhin

Versi Cek
Node.js 18+ (dites di 22.16) node --version
Python 3.9+ (dites di 3.13.2) py --version

Kenapa py, bukan python? Di Windows, python sering nunjuk ke shim Microsoft Store atau venv tool lain yang nggak bawa pip. Launcher py selalu ngarah ke interpreter asli. Cek punyamu dengan py --list.

Cara cepat

cd C:\laragon\www\scraper-studio
.\scripts\setup.ps1
.\scripts\dev.ps1

setup.ps1 bikin venv dan install dua-duanya. dev.ps1 nyalain kedua server di jendela terpisah. Habis itu buka http://localhost:3000.

Cara manual

Kalau mau ngerti tiap langkahnya.

Backend:

cd backend
py -m venv .venv
.\.venv\Scripts\python.exe -m pip install -r requirements.txt
Copy-Item .env.example .env
.\.venv\Scripts\python.exe app.py

Jalan di http://127.0.0.1:5000.

Frontend (terminal terpisah):

cd frontend
npm install
npm run dev

Jalan di http://localhost:3000.

Cek instalasi berhasil

curl http://127.0.0.1:5000/api/health

Harusnya balik {"ok":true,"service":"scraper-studio",...}.

Test backend:

cd backend
.\.venv\Scripts\python.exe -m pytest tests\ -q

Struktur project

scraper-studio/
├── backend/                  # Flask
│   ├── app.py                # route + error handler
│   ├── config.py             # config dari environment
│   ├── openapi.py            # spec OpenAPI 3.0 -> /api/docs
│   ├── requirements.txt
│   ├── .env.example
│   ├── core/
│   │   ├── fetcher.py        # HTTP + proteksi SSRF
│   │   ├── robots.py         # cek robots.txt (cache 1 jam)
│   │   ├── ratelimit.py      # rate limit per IP
│   │   └── errors.py         # error -> status code HTTP
│   ├── scrapers/
│   │   ├── __init__.py       # registry + auto-detect
│   │   ├── google_form.py    # parser FB_PUBLIC_LOAD_DATA_
│   │   └── generic.py        # BeautifulSoup + CSS rules
│   └── tests/                # 33 test
├── frontend/                 # Next.js App Router + TypeScript + Tailwind v4
│   ├── next.config.mjs       # rewrite /api/* -> Flask
│   ├── postcss.config.mjs    # plugin Tailwind
│   ├── app/
│   │   ├── page.tsx
│   │   ├── layout.tsx
│   │   └── globals.css       # token neobrutalism, light/dark ikut OS
│   ├── components/
│   │   ├── ui.tsx            # Card, Button, Badge, Stat, Code
│   │   ├── ScrapeForm.tsx
│   │   ├── ResultPanel.tsx
│   │   └── FieldTable.tsx
│   └── lib/
│       ├── api.ts            # client + export CSV
│       └── types.ts
└── scripts/
    ├── setup.ps1
    └── dev.ps1

Tampilan

Frontend-nya pakai Tailwind v4 dengan gaya neobrutalism: border tebal 3px, shadow keras tanpa blur, warna flat yang nyala, dan tombol yang "keteken" — bergeser masuk ke bayangannya sendiri pas diklik.

Token-nya ada di @theme dalam app/globals.css. Tailwind v4 nerbitin tiap entry @theme jadi CSS variable di :root, dan utility-nya nunjuk ke var() — jadi dark mode cukup nulis ulang variabelnya di dalam @media (prefers-color-scheme: dark), nggak perlu nyebar class dark: ke seluruh markup. Di mode gelap, border sama shadow-nya balik dari hitam ke putih-tulang; warna aksennya dibiarin karena udah cukup kontras.

Cuma dua pola yang diangkat jadi class (.brut dan .brut-press di @layer components) karena kepakai di mana-mana. Sisanya utility biasa, dan variasi warna dibungkus jadi komponen React di components/ui.tsx supaya tone-nya kejaga sama TypeScript, bukan sama @apply.

Mau ganti font display? Satu baris --font-display di @theme.


API

Dokumentasi interaktif

Swagger UI ada di http://localhost:3000/api/docs — lewat rewrite yang sama kayak endpoint lain, jadi nggak perlu tau alamat Flask-nya. Kalau backend-nya lagi jalan sendirian, bisa juga langsung di http://127.0.0.1:5000/api/docs.

Spec mentahnya di /api/openapi.json (OpenAPI 3.0.3) — tinggal di-import ke Postman/Insomnia, atau dipakai buat generate client:

curl http://localhost:3000/api/openapi.json -o openapi.json

Spec-nya ditulis sebagai dict di backend/openapi.py, bukan YAML di docstring, biar seluruh kontraknya kebaca dalam satu tempat. Enum tipe field sama daftar error code-nya di-derive dari kodenya langsung (FIELD_TYPES dan subclass ScrapeError), jadi nambah tipe baru otomatis kebawa ke dokumentasi. Yang nggak bisa auto — misal nambah route baru — dijaga sama tests/test_openapi.py: kalau ada endpoint yang belum didokumentasiin, testnya merah.

POST /api/scrape

{
  "url": "https://docs.google.com/forms/d/e/FORM_ID/viewform",
  "scraper": "google_form",
  "options": {}
}

scraper opsional — kalau dikosongin, backend nebak sendiri dari URL-nya (docs.google.com/forms jadi google_form, selainnya generic).

Respons sukses:

{
  "ok": true,
  "scraper": "google_form",
  "fetch": { "status_code": 200, "final_url": "...", "bytes": 48210 },
  "data": {
    "title": "Form Pendaftaran",
    "field_count": 12,
    "fields": [
      {
        "title": "Nama lengkap",
        "type": "short_answer",
        "entry_id": "entry.123456789",
        "required": true,
        "options": []
      }
    ]
  }
}

Respons gagal — bentuknya konsisten, jadi client cukup ngecek ok:

{
  "ok": false,
  "error": {
    "code": "blocked_url",
    "message": "Refusing to fetch a private or loopback address.",
    "detail": "192.168.1.1 resolves to 192.168.1.1"
  }
}
Code HTTP Artinya
invalid_url 400 Bukan http/https, atau host nggak ke-resolve
blocked_url 403 Nunjuk ke alamat privat/loopback
robots_disallowed 403 Ditolak robots.txt host tujuan
parse_failed 422 Halaman keambil tapi strukturnya nggak dikenali
rate_limited 429 Kena limit
fetch_failed 502 Timeout, kegedean, atau error dari server tujuan

GET /api/health · GET /api/scrapers

Status service, dan daftar scraper yang tersedia.

GET /api/docs · GET /api/openapi.json

Swagger UI dan spec-nya. Aset Swagger UI di-bundle di dalam package-nya, jadi halamannya tetap render tanpa internet.


Konfigurasi

Semua lewat backend/.env (contoh lengkap di .env.example):

Variabel Default Fungsi
FLASK_PORT 5000 Port backend
FETCH_TIMEOUT 20 Timeout per request (detik)
MAX_FETCH_BYTES 5000000 Batas ukuran respons
RESPECT_ROBOTS true Patuhi robots.txt
ALLOW_PRIVATE_HOSTS false Matiin proteksi SSRF — dev doang
RATE_LIMIT / RATE_WINDOW 30 / 60 Limit per IP

Catatan keamanan

Backend nerima URL dari client terus nge-fetch dari sisi server. Itu pola klasik SSRF: tanpa penjagaan, orang bisa nyuruh server kamu nembak 169.254.169.254 (endpoint metadata cloud) atau ngintip service internal di localhost:3306.

Makanya tiap URL di-resolve dulu ke IP-nya sebelum koneksi dibuka, dan semua alamat privat, loopback, link-local, sama reserved langsung ditolak. Ini yang dites di tests/test_fetcher.py.

ALLOW_PRIVATE_HOSTS=true mematikan penjagaan itu. Cuma buat nembak server test lokal — jangan pernah dinyalain di instance yang bisa diakses publik.


Etika & legalitas

Tool ini defaultnya patuh robots.txt dan ada rate limit. Jangan dimatiin kecuali kamu emang punya situsnya.

Buat Google Forms, docs.google.com/robots.txt nulis Allow: /forms secara eksplisit — jadi ngambil struktur form publik itu diizinkan, dan ada Crawl-delay: 1 yang sebaiknya dihormati.

Yang perlu dijaga sendiri:

  • Ambil struktur publik, bukan data pribadi orang.
  • Jangan pakai entry_id buat spam submit ke form yang bukan punyamu. Prefill link itu fitur resmi Google buat bantu orang isi form, bukan buat nge-flood.
  • Cek ToS situs targetnya. robots.txt bukan izin hukum.
  • Kalau nyimpen hasil scraping yang ada data orang, itu masuk ranah aturan perlindungan data.

Troubleshooting

Could not reach the backend. di UI. Flask-nya belum nyala. Cek curl http://127.0.0.1:5000/api/health.

No module named pip waktu bikin venv. python kamu nunjuk ke venv tool lain. Pakai py -m venv .venv, jangan python -m venv .venv. Lihat py --list buat daftar interpreter asli.

This page has no FB_PUBLIC_LOAD_DATA_ block. Formnya nggak publik (minta login), atau yang kamu tempel link /edit bukan /viewform. Tes dulu: buka link itu di jendela penyamaran — kalau kamu diminta login, scraper juga bakal ketolak.

Struktur form kebaca aneh padahal sebelumnya normal. Format FB_PUBLIC_LOAD_DATA_ itu internal Google dan nggak ada jaminan stabil. Kalau Google ngubah layoutnya, indeks di scrapers/google_form.py perlu disesuaikan. Parsernya udah defensif (_at() nggak bakal IndexError), jadi gejalanya bakal field kosong, bukan crash.

Turbopack ngeluh soal workspace root. Udah dikunci di next.config.mjs lewat turbopack.root. Kalau muncul lagi, berarti foldernya dipindah.


Lisensi

MIT

About

Scaraper Studio Solision if you want get data if not open api to get thats data

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages