Berapa lama waktu yang habis kalau tim Anda mengecek harga dan fitur pesaing satu per satu setiap minggu? Agensi price-monitoring, tim data e-commerce, dan freelancer otomasi di Indonesia biasanya kehilangan pola penting karena hanya sempat mengecek sesekali — padahal pesaing sering mengubah harga tanpa pemberitahuan. Dasbor analisis pesaing menyelesaikan masalah ini: scraper mengambil harga, daftar fitur, dan jumlah produk pesaing secara terjadwal, menyimpan setiap perubahan ke SQLite, lalu menghasilkan laporan perbandingan yang bisa dibaca dalam hitungan detik. Masalahnya, halaman harga pesaing hampir selalu dilindungi reCAPTCHA v2 begitu traffic scraping terdeteksi — di sinilah CaptchaAI masuk, menyelesaikan tantangan itu lewat API sebelum scraper melanjutkan pengambilan data. Panduan ini membangun lima komponennya satu per satu: model data, solver CAPTCHA, scraper, pembuat laporan, dan skrip utama yang menjalankan semuanya sesuai jadwal.
Arsitektur Dasbor Analisis Pesaing
Alurnya lurus ke depan: setiap request ke situs pesaing melewati solver CAPTCHA dulu sebelum data mentah diteruskan ke extractor, tersimpan sebagai riwayat di SQLite, lalu dirangkum jadi laporan yang bisa langsung dibaca tim non-teknis.
Competitor Sites ──> CAPTCHA Solver ──> Data Extractors
│
SQLite Store
│
Dashboard Report
Empat lapisan ini sengaja dipisah — kalau selector HTML satu pesaing berubah, Anda cukup memperbaiki extractor tanpa menyentuh solver atau skema database.
Model Data untuk Riwayat Harga Pesaing
Setiap metrik pesaing — harga, fitur, atau jumlah produk — disimpan sebagai baris generik di tabel metrics, bukan kolom terpisah per pesaing. Desain ini membuat penambahan pesaing baru atau metrik baru tidak perlu perubahan skema sama sekali; cukup tambahkan baris dengan metric dan value yang sesuai. Kolom numeric_value memisahkan angka yang bisa dihitung (harga, jumlah produk) dari teks mentah (daftar fitur), jadi laporan bisa mengurutkan dan membandingkan tanpa parsing ulang setiap kali dijalankan.
# models.py
import sqlite3
from datetime import datetime
from dataclasses import dataclass
from typing import Optional
@dataclass
class CompetitorData:
competitor: str
metric: str
value: str
numeric_value: Optional[float] = None
url: str = ""
scraped_at: str = ""
def __post_init__(self):
if not self.scraped_at:
self.scraped_at = datetime.now().isoformat()
class CompetitorDB:
def __init__(self, path="competitor_data.db"):
self.conn = sqlite3.connect(path)
self._init()
def _init(self):
self.conn.execute("""
CREATE TABLE IF NOT EXISTS metrics (
id INTEGER PRIMARY KEY AUTOINCREMENT,
competitor TEXT,
metric TEXT,
value TEXT,
numeric_value REAL,
url TEXT,
scraped_at TEXT
)
""")
self.conn.commit()
def save(self, data: CompetitorData):
self.conn.execute(
"""INSERT INTO metrics
(competitor, metric, value, numeric_value, url, scraped_at)
VALUES (?, ?, ?, ?, ?, ?)""",
(data.competitor, data.metric, data.value,
data.numeric_value, data.url, data.scraped_at),
)
self.conn.commit()
def get_history(self, competitor, metric, limit=30):
cursor = self.conn.execute(
"""SELECT value, numeric_value, scraped_at
FROM metrics
WHERE competitor = ? AND metric = ?
ORDER BY scraped_at DESC LIMIT ?""",
(competitor, metric, limit),
)
return cursor.fetchall()
def latest_comparison(self, metric):
cursor = self.conn.execute(
"""SELECT competitor, value, numeric_value, MAX(scraped_at) as latest
FROM metrics WHERE metric = ?
GROUP BY competitor ORDER BY numeric_value""",
(metric,),
)
return cursor.fetchall()
latest_comparison mengambil entri terbaru per pesaing untuk satu metrik — inilah yang dipakai laporan untuk membandingkan harga hari ini antar-pesaing — sementara get_history menyediakan deret waktu yang dipakai untuk grafik tren.
Solver CAPTCHA untuk Halaman Pesaing
Halaman harga dan katalog pesaing hampir selalu memasang reCAPTCHA v2 begitu pola traffic-nya terlihat seperti scraping. Kelas CaptchaSolver mengecek keberadaan atribut data-sitekey di HTML, mengirim task ke in.php, lalu polling res.php setiap 5 detik sampai status bernilai 1 — pola empat langkah standar CaptchaAI: kirim, simpan task ID, polling, pakai token.
# solver.py
import requests
import time
import re
import os
class CaptchaSolver:
def __init__(self):
self.api_key = os.environ["CAPTCHAAI_API_KEY"]
def solve_if_needed(self, session, url, html):
if "data-sitekey" not in html:
return html
match = re.search(r'data-sitekey="([^"]+)"', html)
if not match:
return html
sitekey = match.group(1)
resp = requests.post("https://ocr.captchaai.com/in.php", data={
"key": self.api_key,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": url,
"json": 1,
}, timeout=30)
task_id = resp.json()["request"]
time.sleep(15)
for _ in range(24):
resp = requests.get("https://ocr.captchaai.com/res.php", params={
"key": self.api_key, "action": "get",
"id": task_id, "json": 1,
}, timeout=15)
data = resp.json()
if data.get("status") == 1:
post_resp = session.post(url, data={
"g-recaptcha-response": data["request"],
}, timeout=30)
return post_resp.text
if data["request"] != "CAPCHA_NOT_READY":
raise RuntimeError(data["request"])
time.sleep(5)
raise TimeoutError("CAPTCHA solve timeout")
Untuk tim data freelance yang memantau belasan pesaing sekaligus, paket ADVANCE ($90/bulan, 50 thread) biasanya cukup — satu thread menangani satu CAPTCHA yang sedang diproses, dan begitu solve-nya selesai, thread itu langsung bebas mengambil task berikutnya tanpa biaya tambahan per solve. Kalau baru mulai dengan satu-dua pesaing, BASIC ($15/bulan, 5 thread) sudah jalan; tinggal naik kelas begitu jumlah pesaing yang dipantau bertambah.
Scraper untuk Harga, Fitur, dan Jumlah Produk
CompetitorScraper membungkus tiga jenis pengambilan data: harga (scrape_pricing), daftar fitur (scrape_features), dan jumlah produk (scrape_product_count). Ketiganya memanggil _fetch yang sama, jadi solver CAPTCHA otomatis berjalan begitu halaman butuh verifikasi — scraper tidak perlu tahu lebih dulu apakah situs target memasang CAPTCHA atau tidak.
# scraper.py
import requests
import re
from bs4 import BeautifulSoup
from solver import CaptchaSolver
from models import CompetitorData
class CompetitorScraper:
def __init__(self):
self.solver = CaptchaSolver()
self.session = requests.Session()
self.session.headers["User-Agent"] = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 Chrome/125.0.0.0 Safari/537.36"
)
def scrape_pricing(self, competitor_name, url, plan_selector, price_selector):
html = self._fetch(url)
soup = BeautifulSoup(html, "html.parser")
plans = soup.select(plan_selector)
data = []
for plan in plans:
name_el = plan.select_one("h3, h2, .plan-name")
price_el = plan.select_one(price_selector)
if not name_el or not price_el:
continue
price_text = price_el.get_text(strip=True)
match = re.search(r'[\d,.]+', price_text)
numeric = float(match.group().replace(",", "")) if match else None
data.append(CompetitorData(
competitor=competitor_name,
metric=f"price_{name_el.get_text(strip=True).lower().replace(' ', '_')}",
value=price_text,
numeric_value=numeric,
url=url,
))
return data
def scrape_features(self, competitor_name, url, feature_list_selector):
html = self._fetch(url)
soup = BeautifulSoup(html, "html.parser")
features = soup.select(f"{feature_list_selector} li")
return [
CompetitorData(
competitor=competitor_name,
metric="feature",
value=f.get_text(strip=True),
url=url,
)
for f in features if f.get_text(strip=True)
]
def scrape_product_count(self, competitor_name, url, count_selector):
html = self._fetch(url)
soup = BeautifulSoup(html, "html.parser")
el = soup.select_one(count_selector)
if el:
text = el.get_text(strip=True)
match = re.search(r'[\d,]+', text)
if match:
count = int(match.group().replace(",", ""))
return CompetitorData(
competitor=competitor_name,
metric="product_count",
value=text,
numeric_value=count,
url=url,
)
return None
def _fetch(self, url):
resp = self.session.get(url, timeout=20)
return self.solver.solve_if_needed(self.session, url, resp.text)
Kalau scraper dijalankan terjadwal (cron, Lambda, atau Cloud Scheduler) dari region seperti AWS ap-southeast-1 (Singapura) atau GCP asia-southeast2 (Jakarta), latensi ke server CaptchaAI maupun ke situs pesaing tetap rendah. Jeda time.sleep(5) antar-pesaing di main.py sudah cukup untuk menghindari rate-limit sederhana di sisi target.
Pembuat Laporan Perbandingan
report.py mengubah data mentah di SQLite jadi dua bentuk keluaran: generate_report merangkum posisi terbaru semua pesaing per metrik dalam format teks rapi, sementara generate_trend menampilkan riwayat satu metrik untuk satu pesaing dari waktu ke waktu — berguna kalau Anda ingin tahu apakah pesaing menurunkan harga bertahap atau sekali lompatan besar.
# report.py
from models import CompetitorDB
def generate_report(db: CompetitorDB, metrics):
lines = ["=" * 60, "Competitor Analysis Report", "=" * 60, ""]
for metric in metrics:
results = db.latest_comparison(metric)
if not results:
continue
lines.append(f"--- {metric.replace('_', ' ').title()} ---")
for comp, value, numeric, ts in results:
marker = ""
if numeric is not None:
marker = f" (${numeric:,.2f})" if "price" in metric else f" ({numeric:,.0f})"
lines.append(f" {comp}: {value}{marker}")
lines.append("")
return "\n".join(lines)
def generate_trend(db: CompetitorDB, competitor, metric, periods=10):
history = db.get_history(competitor, metric, limit=periods)
if not history:
return f"No data for {competitor} — {metric}"
lines = [f"Trend: {competitor} — {metric}", "-" * 40]
for value, numeric, ts in reversed(history):
date = ts[:10]
lines.append(f" {date}: {value}")
return "\n".join(lines)
Skrip Utama dan Penjadwalan
main.py menyatukan semua komponen: daftar pesaing di COMPETITORS (ganti dengan domain, selector plan, dan selector harga pesaing Anda sendiri), lalu loop yang men-scrape tiap pesaing, menyimpan hasilnya, dan menghasilkan laporan di akhir. Kegagalan pada satu pesaing — selector berubah, halaman timeout — ditangkap dengan try/except per pesaing, jadi satu masalah tidak menghentikan seluruh batch.
# main.py
import time
from models import CompetitorDB
from scraper import CompetitorScraper
from report import generate_report
COMPETITORS = [
{
"name": "Competitor A",
"pricing_url": "https://competitor-a.example.com/pricing",
"plan_selector": ".pricing-plan",
"price_selector": ".price",
},
{
"name": "Competitor B",
"pricing_url": "https://competitor-b.example.com/pricing",
"plan_selector": ".plan-card",
"price_selector": ".plan-price",
},
]
def main():
db = CompetitorDB()
scraper = CompetitorScraper()
for comp in COMPETITORS:
print(f"Scraping {comp['name']}...")
try:
pricing = scraper.scrape_pricing(
comp["name"], comp["pricing_url"],
comp["plan_selector"], comp["price_selector"],
)
for p in pricing:
db.save(p)
print(f" {p.metric}: {p.value}")
except Exception as e:
print(f" Error: {e}")
time.sleep(5)
# Generate report
metrics = ["price_basic", "price_pro", "price_enterprise", "product_count"]
report = generate_report(db, metrics)
print(report)
with open("competitor_report.txt", "w") as f:
f.write(report)
if __name__ == "__main__":
main()
Jadwalkan main.py lewat cron harian (misalnya 0 6 * * *) supaya data historis terkumpul otomatis tanpa perlu dijalankan manual — laporan tren baru terasa berguna setelah beberapa hari data terkumpul, jadi jangan kaget kalau hari pertama grafiknya masih kosong.
Masalah Umum dan Solusinya
| Masalah | Penyebab | Solusi |
|---|---|---|
| Harga tidak diekstraksi | Selektor tidak cocok | Periksa HTML halaman dan perbarui selektor per pesaing |
| Data historis kosong | Baru pertama kali dijalankan | Data terakumulasi; jalankan setiap hari untuk visibilitas tren |
| CAPTCHA muncul terus di halaman harga | Deteksi bot pada traffic yang berulang | Tambahkan jeda antar-request dan pertahankan sesi (cookies) yang sama antar-permintaan |
| Laporan menampilkan data basi | Entri yang sama dimasukkan kembali | Gunakan latest_comparison yang mengelompokkan berdasarkan tanggal MAX |
| Scraping lambat atau timeout dari koneksi tidak stabil | Latensi tinggi ke situs pesaing, umum di jaringan mobile | Naikkan nilai timeout di _fetch dan tambahkan retry dengan jeda bertambah (exponential backoff) |
Pertanyaan Umum
Apakah scraping data harga pesaing seperti ini legal?
Selama Anda hanya mengambil data yang memang publik — halaman harga, fitur, katalog — dan tidak menyentuh data pribadi atau area yang mengharuskan login, praktik ini umumnya aman. UU ITE dan UU Pelindungan Data Pribadi (UU 27/2022) tetap mewajibkan Anda memproses data secara bertanggung jawab; hindari data pribadi dan patuhi ketentuan penggunaan situs yang relevan.
Berapa banyak thread CaptchaAI yang saya butuhkan untuk memantau banyak pesaing?
Tergantung jumlah pesaing dan frekuensi scraping. Memantau 5–10 pesaing sekali sehari biasanya cukup dengan STANDARD ($30/bulan, 15 thread); begitu jumlahnya naik atau Anda scraping tiap jam, ADVANCE ($90/bulan, 50 thread) memberi headroom tanpa biaya tambahan per solve.
Alat apa yang paling praktis untuk membuat grafik dari data historis ini?
Ekspor tabel metrics dari SQLite ke CSV, lalu plot dengan matplotlib untuk laporan internal, atau import ke Google Spreadsheet kalau tim non-teknis perlu mengakses datanya tanpa install apa pun.
Bagaimana cara mendapat notifikasi otomatis saat pesaing mengubah harga?
Bandingkan hasil scraping hari ini dengan nilai yang tersimpan kemarin lewat get_history, lalu kirim notifikasi ke Slack atau Telegram kalau selisihnya melewati ambang batas yang Anda tentukan sendiri, misalnya 5%.
Apakah pola yang sama berlaku untuk Cloudflare Turnstile atau GeeTest di halaman pesaing?
Ya — endpoint in.php/res.php dan pola pollingnya sama untuk semua tipe CAPTCHA yang didukung CaptchaAI, baik reCAPTCHA v2, Turnstile, maupun GeeTest v3. Yang berbeda hanya parameter method dan payload yang dikirim; struktur solver di panduan ini bisa dipakai ulang tanpa perombakan besar.
Panduan Terkait
Pantau pesaing Anda tanpa buka tab manual tiap hari — mulai dengan CaptchaAI.