Use Cases

Pemantauan Inventaris Ritel dengan Penanganan CAPTCHA

Pemantau harga yang berhenti di halaman verifikasi bukan masalah scraping, melainkan masalah penanganan CAPTCHA. Selesaikan tantangan itu di dalam loop pemantauan (reCAPTCHA v2, reCAPTCHA v3, atau Cloudflare Turnstile lewat API CaptchaAI), lanjutkan permintaan dengan token hasilnya, dan siklus cek stok Anda kembali jalan tanpa ditunggui.

Urutannya mengikuti kerja nyata: tentukan ritme cek dan kebutuhan thread dulu, baru pasang kodenya, lalu tutup dengan error yang sering muncul di produksi.


Tentukan ritme cek sebelum menulis kodenya

Kesalahan paling mahal dalam pemantauan ritel adalah mengecek terlalu sering. Setiap permintaan tambahan menaikkan peluang halaman produk berubah jadi halaman tantangan — artinya biaya penyelesaian naik dan data justru terlambat. Tetapkan interval per kategori sebelum satu baris kode ditulis.

Jenis produk Interval cek Jalur jaringan
Elektronik Setiap 30 menit Egress berputar yang diotorisasi
Bahan pangan segar Setiap 4 jam Egress berputar yang diotorisasi
Fesyen Setiap 2 jam IP residensial
Rilis terbatas Setiap 1 menit Egress seluler yang diotorisasi
Perlengkapan rumah Setiap 6 jam IP residensial
Barang komoditas Setiap 12 jam IP pusat data (biasanya cukup)

Angka pada kolom kedua adalah titik awal, bukan aturan mati. Perbesar intervalnya begitu CAPTCHA muncul lebih sering dari satu kali per sepuluh permintaan pada satu domain.

Menghitung thread untuk katalog Anda

CaptchaAI menagih per thread bersamaan, bukan per penyelesaian, sehingga biaya bulanan ditentukan oleh berapa banyak CAPTCHA yang diselesaikan pada saat yang sama, bukan oleh total SKU. Ini relevan untuk agensi price monitoring di Indonesia yang beranggaran ketat: katalog 2.000 SKU yang dicek tiap 30 menit hampir tidak pernah menyentuh langit-langit thread.

Hitungannya sederhana: permintaan per menit di puncak siklus, dikali proporsi halaman yang memunculkan tantangan, dikali waktu penyelesaian rata-rata dalam menit. Untuk katalog menengah, BASIC ($15/bulan, 5 thread) sudah cukup; tim multi-klien umumnya berhenti di STANDARD ($30/bulan, 15 thread) atau ADVANCE ($90/bulan, 50 thread). Semua paket memberi penyelesaian tanpa batas per thread, jadi menambah SKU tidak otomatis menambah tagihan.


Tipe CAPTCHA yang akan Anda temui di halaman produk

Platform Tipe CAPTCHA Pemicu Data yang dipantau
Amazon reCAPTCHA v2 Akses bervolume tinggi Harga, stok, ulasan
Walmart reCAPTCHA v3 + Cloudflare Deteksi bot Persediaan, harga
Best Buy reCAPTCHA v2 Pemeriksaan tambah ke keranjang Stok, harga
Target Cloudflare Turnstile Akses otomatis Ketersediaan
Toko Shopify Cloudflare Turnstile Rate limiting Data produk
eBay reCAPTCHA v2 Pencarian dan halaman listing Listing, harga

Semua tipe di kolom kedua didukung penuh oleh CaptchaAI, jadi satu fungsi penyelesaian cukup untuk seluruh daftar itu. Catatan pentingnya: hCaptcha dan FunCaptcha (Arkose Labs) belum didukung, dan GeeTest v4 masih berstatus segera hadir. Kalau target Anda memakai salah satunya, rencanakan jalur lain sejak awal.


Pemantau produk: satu kelas untuk harga dan stok

Pola kerjanya sama dengan integrasi CaptchaAI lain: kirim ke in.php, simpan task ID, polling res.php, lalu pakai token pada permintaan berikutnya. Bedanya, di sini penyelesaian dipanggil dari dalam loop pemantauan dan hanya ketika halaman memang menampilkan tantangan.

import requests
import time
import re
import json
from datetime import datetime
from bs4 import BeautifulSoup

CAPTCHAAI_KEY = "YOUR_API_KEY"
CAPTCHAAI_URL = "https://ocr.captchaai.com"


def solve_captcha(method, sitekey, pageurl, **kwargs):
    data = {
        "key": CAPTCHAAI_KEY, "method": method,
        "googlekey": sitekey, "pageurl": pageurl, "json": 1,
    }
    data.update(kwargs)
    resp = requests.post(f"{CAPTCHAAI_URL}/in.php", data=data)
    task_id = resp.json()["request"]
    for _ in range(60):
        time.sleep(5)
        result = requests.get(f"{CAPTCHAAI_URL}/res.php", params={
            "key": CAPTCHAAI_KEY, "action": "get",
            "id": task_id, "json": 1,
        })
        r = result.json()
        if r["request"] != "CAPCHA_NOT_READY":
            return r["request"]
    raise TimeoutError("Timeout")


class RetailMonitor:
    def __init__(self, proxy=None):
        self.session = requests.Session()
        if proxy:
            self.session.proxies = {"http": proxy, "https": proxy}
        self.session.headers.update({
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
            "AppleWebKit/537.36 Chrome/126.0.0.0 Safari/537.36",
            "Accept": "text/html,application/xhtml+xml,*/*;q=0.8",
            "Accept-Language": "en-US,en;q=0.9",
        })

    def check_product(self, url):
        """Check single product's price and availability."""
        resp = self.session.get(url, timeout=30)

        # Handle CAPTCHA
        if self._has_captcha(resp.text):
            resp = self._solve_and_retry(resp.text, url)

        soup = BeautifulSoup(resp.text, "html.parser")
        return {
            "url": url,
            "title": self._text(soup, "h1, .product-title, #productTitle"),
            "price": self._text(soup, ".price, .a-price .a-offscreen, .prod-price"),
            "availability": self._text(soup, "#availability, .stock-status, .fulfillment"),
            "in_stock": self._check_stock(soup),
            "timestamp": datetime.now().isoformat(),
        }

    def monitor_products(self, product_urls, interval_sec=1800):
        """Continuously monitor products for changes."""
        history = {}

        while True:
            for url in product_urls:
                try:
                    current = self.check_product(url)

                    # Check for changes
                    prev = history.get(url)
                    if prev:
                        changes = self._detect_changes(prev, current)
                        if changes:
                            self._alert(current["title"], changes)

                    history[url] = current
                    time.sleep(3)

                except Exception as e:
                    print(f"Error checking {url}: {e}")

            print(f"Cycle complete: {len(product_urls)} products checked")
            time.sleep(interval_sec)

    def track_prices(self, product_urls, output_file="prices.json"):
        """Single price check across all products."""
        results = []
        for url in product_urls:
            try:
                data = self.check_product(url)
                results.append(data)
                time.sleep(3)
            except Exception as e:
                results.append({"url": url, "error": str(e)})

        with open(output_file, "w") as f:
            json.dump(results, f, indent=2)
        print(f"Tracked {len(results)} products → {output_file}")
        return results

    def _has_captcha(self, html):
        return any(tag in html.lower() for tag in [
            'data-sitekey', 'g-recaptcha', 'cf-turnstile', 'captcha',
        ])

    def _solve_and_retry(self, html, url):
        match = re.search(r'data-sitekey="([^"]+)"', html)
        if not match:
            return self.session.get(url)

        sitekey = match.group(1)
        if 'cf-turnstile' in html:
            token = solve_captcha("turnstile", sitekey, url)
            return self.session.post(url, data={"cf-turnstile-response": token})
        else:
            token = solve_captcha("userrecaptcha", sitekey, url)
            return self.session.post(url, data={"g-recaptcha-response": token})

    def _text(self, soup, selector):
        el = soup.select_one(selector)
        return el.get_text(strip=True) if el else ""

    def _check_stock(self, soup):
        stock_el = soup.select_one("#availability, .stock-status")
        if stock_el:
            text = stock_el.get_text(strip=True).lower()
            return "in stock" in text or "available" in text
        return None

    def _detect_changes(self, prev, current):
        changes = []
        if prev["price"] != current["price"]:
            changes.append(f"Price: {prev['price']} → {current['price']}")
        if prev["in_stock"] != current["in_stock"]:
            status = "In Stock" if current["in_stock"] else "Out of Stock"
            changes.append(f"Stock: → {status}")
        return changes

    def _alert(self, title, changes):
        print(f"ALERT [{title}]: {', '.join(changes)}")


# Usage
monitor = RetailMonitor(
    proxy="http://user:pass@residential.proxy.com:5000"
)

products = [
    "https://store.example.com/product/abc123",
    "https://store.example.com/product/def456",
    "https://store.example.com/product/ghi789",
]

# One-time price check
results = monitor.track_prices(products)

# Or continuous monitoring (every 30 min)
# monitor.monitor_products(products, interval_sec=1800)

Perhatikan _has_captcha. Pemeriksaan penanda HTML inilah yang menjaga biaya tetap rendah, karena penyelesaian hanya jalan pada halaman yang membutuhkannya. Metode _solve_and_retry membaca data-sitekey, memilih metode turnstile atau userrecaptcha sesuai penanda, lalu mengirim ulang permintaan dengan token pada field yang benar: cf-turnstile-response untuk Turnstile, g-recaptcha-response untuk reCAPTCHA.

Jeda tiga detik antarproduk di track_prices bukan hiasan. Pada instance yang di-deploy di ap-southeast-1 (Singapura) atau ap-southeast-3 (Jakarta), jeda inilah pembeda antara siklus yang selesai bersih dan siklus yang separuh halamannya berubah jadi tantangan.


Memindai satu kategori penuh

Pemantauan per URL cocok untuk daftar pantau yang stabil. Ketika Anda perlu tahu isi seluruh kategori, termasuk produk baru yang belum masuk daftar, pindai halaman demi halaman sampai hasilnya habis.

def scan_category(base_url, category, max_pages=20):
    """Scan an entire product category for stock status."""
    monitor = RetailMonitor(
        proxy="http://user:pass@residential.proxy.com:5000"
    )

    all_products = []
    for page in range(1, max_pages + 1):
        url = f"{base_url}/{category}?page={page}"
        resp = monitor.session.get(url, timeout=30)

        if monitor._has_captcha(resp.text):
            resp = monitor._solve_and_retry(resp.text, url)

        soup = BeautifulSoup(resp.text, "html.parser")
        items = soup.select(".product-card, .s-result-item")

        if not items:
            break

        for item in items:
            all_products.append({
                "name": monitor._text(item, ".product-name, .a-text-normal"),
                "price": monitor._text(item, ".price, .a-price"),
                "stock": monitor._text(item, ".stock, .a-color-success"),
                "url": item.select_one("a")["href"] if item.select_one("a") else "",
            })

        time.sleep(3)

    return all_products

Loop berhenti sendiri saat sebuah halaman tidak lagi mengembalikan kartu produk, jadi max_pages hanya berfungsi sebagai rem darurat.


Membandingkan harga antartoko

Pertanyaan klien price monitoring biasanya bukan "berapa harga produk ini", melainkan "di toko mana produk ini paling murah hari ini". Jalankan pencarian yang sama di beberapa toko, lalu urutkan hasilnya.

def compare_product_across_stores(product_name, stores):
    """Compare prices across retailers for the same product."""
    results = []

    for store in stores:
        monitor = RetailMonitor(proxy=store.get("proxy"))
        search_url = f"{store['base_url']}/search?q={product_name}"

        try:
            resp = monitor.session.get(search_url, timeout=30)
            if monitor._has_captcha(resp.text):
                resp = monitor._solve_and_retry(resp.text, search_url)

            soup = BeautifulSoup(resp.text, "html.parser")
            first_result = soup.select_one(".product-card, .s-result-item")

            if first_result:
                results.append({
                    "store": store["name"],
                    "price": monitor._text(first_result, ".price"),
                    "in_stock": "in stock" in first_result.get_text().lower(),
                })
        except Exception as e:
            results.append({"store": store["name"], "error": str(e)})

        time.sleep(5)

    results.sort(key=lambda x: x.get("price", "zzzz"))
    return results

Catatan kepatuhan untuk pasar Indonesia: UU Pelindungan Data Pribadi (UU 27/2022) menempatkan data pribadi di luar jangkauan pengumpulan otomatis. Batasi pemantauan pada data produk publik — harga, stok, deskripsi — dan hindari data pembeli, ulasan beridentitas, atau apa pun di balik login milik orang lain.


Error yang paling sering muncul di produksi

Gejala Penyebab umum Perbaikan
CAPTCHA muncul di hampir setiap halaman produk IP sudah ditandai atau laju permintaan terlalu tinggi Perbesar jeda antarpermintaan, putar jalur egress
Harga yang terbaca salah Harga dinamis dirender oleh JavaScript Pindah ke Selenium atau Puppeteer untuk halaman itu
Halaman "robot check" berulang terus Deteksi bot bergaya Amazon Gunakan header yang realistis dan IP residensial
Stok selalu terbaca "tidak tersedia" Ketersediaan dibatasi per wilayah Samakan wilayah egress dengan wilayah target
Field produk tiba-tiba kosong Struktur halaman berubah Perbarui CSS selector, siapkan selector cadangan
Task CAPTCHA kena batas waktu Semua thread terpakai bersamaan Kurangi paralelisme atau naikkan paket thread

Pertanyaan umum

Berapa interval cek yang aman untuk satu halaman produk?

Untuk sebagian besar pengecer, 30–60 menit per produk aman dan jarang memicu tantangan. Interval 1–5 menit bisa dijalankan, tetapi konsekuensinya jelas: lebih banyak CAPTCHA, lebih banyak jalur egress, dan biaya yang ikut naik. Simpan interval agresif hanya untuk SKU bernilai tinggi.

Berapa thread CaptchaAI yang dibutuhkan untuk memantau 1.000 produk?

Lebih sedikit dari perkiraan kebanyakan orang, karena penagihannya berbasis thread bersamaan. Dengan 1.000 produk dan jeda 3 detik, satu siklus penuh memakan waktu sekitar 50 menit dan jarang menuntut lebih dari beberapa thread aktif. Mulai dari BASIC ($15/bulan, 5 thread), lalu naikkan begitu antrean task terlihat menumpuk.

Apakah CaptchaAI bisa menangani hCaptcha di halaman ritel?

Belum. hCaptcha dan FunCaptcha (Arkose Labs) tidak didukung, dan GeeTest v4 baru berstatus segera hadir. Untuk kasus ritel, yang didukung adalah reCAPTCHA v2 dan v3 termasuk varian Enterprise, Cloudflare Turnstile, Cloudflare Challenge, GeeTest v3, serta CAPTCHA gambar dan grid.

Sesi berputar atau sesi lengket untuk pemantauan stok?

Berputar, untuk hampir semua kasus. Setiap halaman produk adalah permintaan yang berdiri sendiri, jadi tidak ada state yang perlu dipertahankan. Sesi lengket baru masuk akal kalau detail stok muncul setelah beberapa langkah navigasi, misalnya memilih varian ukuran atau lokasi toko.

Bagaimana cara menyimpan hasilnya supaya bisa dianalisis kemudian?

Tulis tiap siklus sebagai berkas bertanggal, jangan menimpa berkas yang sama. track_prices sudah mengeluarkan JSON per siklus; tambahkan stempel waktu pada namanya, lalu muat ke database atau spreadsheet. Riwayat harga inilah produk sebenarnya yang dibeli klien.


Panduan terkait


Siklus pemantauan Anda tidak perlu berhenti di halaman verifikasi. Ambil API key CaptchaAI dan sambungkan penyelesaian CAPTCHA ke loop cek stok.

Komentar dinonaktifkan untuk artikel ini.