Use Cases

Scraping Job Board dengan Penanganan CAPTCHA Menggunakan CaptchaAI

Kunci scraping job board dalam skala besar bukan menghindari CAPTCHA, melainkan menanganinya begitu muncul: deteksi tantangan di respons halaman, kirim ke API CaptchaAI untuk diselesaikan, lalu kirim ulang halaman dengan token yang valid. Dengan pola ini, satu scraper bisa terus berjalan di Indeed, LinkedIn, atau Glassdoor tanpa berhenti di setiap verifikasi.

Kebutuhan ini nyata di ekosistem data Indonesia — mulai dari pekerja lepas scraping di Fastwork dan Upwork, agensi pemantau gaji, hingga tim data startup yang menyusun laporan tren rekrutmen. Semuanya butuh pengumpulan data lowongan yang stabil, dan CAPTCHA adalah hambatan teknis terbesarnya. Panduan ini menunjukkan cara membangun scraper Python yang menyelesaikan hambatan itu secara otomatis.


Alur penanganan CAPTCHA saat scraping

Pola integrasinya selalu sama, apa pun job board-nya. Empat langkah ini yang perlu Anda pegang:

  1. Deteksi — periksa apakah respons halaman berisi penanda CAPTCHA (data-sitekey, g-recaptcha, atau cf-turnstile) alih-alih data lowongan.
  2. Kirim — ambil sitekey dari halaman dan kirim task ke in.php dengan method yang sesuai jenis CAPTCHA.
  3. Polling — periksa res.php secara berkala sampai token siap; CaptchaAI menyelesaikan reCAPTCHA v2 dan Turnstile dalam hitungan detik untuk tipe yang didukung.
  4. Pakai token — sisipkan token ke field yang benar (g-recaptcha-response atau cf-turnstile-response) dan kirim ulang request.

Memisahkan deteksi dari penyelesaian membuat kode lebih mudah dirawat: scraper Anda hanya memanggil layanan solver saat benar-benar bertemu tantangan, bukan di setiap request.


Jenis CAPTCHA di job board populer

Setiap platform memakai proteksi yang berbeda, dan strategi solve-nya mengikuti jenis tantangan yang dipasang. Tabel berikut merangkum apa yang biasa Anda temui:

Platform Jenis CAPTCHA Pemicu Data Tersedia
Indeed reCAPTCHA v2 Volume request tinggi Daftar pekerjaan, gaji
LinkedIn Cloudflare Challenge Deteksi bot Pekerjaan, data perusahaan
Glassdoor reCAPTCHA v2 Deteksi scraping Ulasan, gaji, pekerjaan
ZipRecruiter Cloudflare Turnstile Akses otomatis Daftar pekerjaan
Monster reCAPTCHA v2 Halaman pencarian Daftar pekerjaan
CareerBuilder reCAPTCHA v3 Login, pencarian Daftar pekerjaan, pencarian resume

CaptchaAI menangani semua tipe di tabel ini — reCAPTCHA v2, reCAPTCHA v3, Cloudflare Turnstile, maupun Cloudflare Challenge — lewat satu endpoint yang sama.


Membangun scraper dengan deteksi CAPTCHA otomatis

Kelas JobBoardScraper di bawah menggabungkan keempat langkah tadi. Fungsi _has_captcha menandai halaman tantangan, dan _solve_and_retry memilih method yang tepat berdasarkan apakah halaman memuat penanda Turnstile atau reCAPTCHA sebelum mengirim ulang token:

import requests
import time
import re
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("Solve timeout")


class JobBoardScraper:
    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-Language": "en-US,en;q=0.9",
        })

    def search_jobs(self, base_url, query, location, pages=5):
        """Search job listings across multiple pages."""
        all_jobs = []

        for page in range(pages):
            url = f"{base_url}/jobs?q={query}&l={location}&start={page * 10}"
            resp = self.session.get(url, timeout=30)

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

            if resp.status_code == 200:
                jobs = self._parse_listings(resp.text)
                all_jobs.extend(jobs)
                print(f"Page {page + 1}: {len(jobs)} jobs found")
            else:
                print(f"Page {page + 1}: Request failed ({resp.status_code})")

            time.sleep(3)  # Rate limit

        return all_jobs

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

    def _solve_and_retry(self, html, url):
        # Try reCAPTCHA first
        match = re.search(r'data-sitekey="([^"]+)"', html)
        if match:
            sitekey = match.group(1)

            # Detect Turnstile vs reCAPTCHA
            if 'cf-turnstile' in html:
                token = solve_captcha("turnstile", sitekey, url)
                field = "cf-turnstile-response"
            else:
                token = solve_captcha("userrecaptcha", sitekey, url)
                field = "g-recaptcha-response"

            return self.session.post(url, data={field: token})

        return self.session.get(url)

    def _parse_listings(self, html):
        soup = BeautifulSoup(html, "html.parser")
        jobs = []

        for card in soup.select(".job_seen_beacon, .jobsearch-ResultsList > li"):
            title_el = card.select_one("h2 a, .jobTitle a")
            company_el = card.select_one(".companyName, [data-testid='company-name']")
            location_el = card.select_one(".companyLocation, [data-testid='text-location']")
            salary_el = card.select_one(".salary-snippet, .estimated-salary")

            if title_el:
                jobs.append({
                    "title": title_el.get_text(strip=True),
                    "company": company_el.get_text(strip=True) if company_el else "",
                    "location": location_el.get_text(strip=True) if location_el else "",
                    "salary": salary_el.get_text(strip=True) if salary_el else "",
                    "url": title_el.get("href", ""),
                })

        return jobs


# Usage
scraper = JobBoardScraper(
    proxy="http://user:pass@residential.proxy.com:5000"
)
jobs = scraper.search_jobs(
    base_url="https://jobs.example.com",
    query="python developer",
    location="New York",
    pages=10,
)
print(f"Total jobs collected: {len(jobs)}")

Mengumpulkan data gaji untuk analisis pasar

Setelah scraper dasar berjalan, kasus pakai paling umum adalah menyusun tabel gaji per jabatan dan lokasi. Fungsi berikut menjalankan pencarian untuk kombinasi judul dan lokasi, lalu menulis hasilnya ke CSV — format yang gampang dibawa ke spreadsheet atau notebook analisis:

import csv


def collect_salary_data(titles, locations, output_file):
    """Collect salary data across job titles and locations."""
    scraper = JobBoardScraper(
        proxy="http://user:pass@residential.proxy.com:5000"
    )

    results = []
    for title in titles:
        for location in locations:
            try:
                jobs = scraper.search_jobs(
                    "https://jobs.example.com",
                    title, location, pages=3,
                )
                salaries = [j["salary"] for j in jobs if j["salary"]]
                results.append({
                    "title": title,
                    "location": location,
                    "listings": len(jobs),
                    "with_salary": len(salaries),
                    "salary_samples": "; ".join(salaries[:5]),
                })
                time.sleep(5)
            except Exception as e:
                results.append({
                    "title": title,
                    "location": location,
                    "error": str(e),
                })

    with open(output_file, "w", newline="") as f:
        writer = csv.DictWriter(
            f, fieldnames=["title", "location", "listings",
                           "with_salary", "salary_samples", "error"],
        )
        writer.writeheader()
        writer.writerows(results)

    return results


# Collect salary data for market analysis
collect_salary_data(
    titles=["Data Engineer", "ML Engineer", "DevOps Engineer"],
    locations=["San Francisco", "New York", "Austin", "Remote"],
    output_file="salary_data.csv",
)

Menjaga scraper tetap stabil

CAPTCHA hanyalah satu lapis pertahanan. Agar scraper tidak terus-menerus memicu tantangan, atur ritme akses supaya menyerupai pola manusia:

Teknik Mengapa Membantu
Rotasi egress jaringan yang diotorisasi Mendistribusikan request ke banyak IP nyata
Delay 3–5 detik antar halaman Meniru kecepatan browsing manusia
User-Agent konsisten per session Menghindari ketidakcocokan sinyal browser
Terima cookie Job board melacak session via cookie
Acak urutan pencarian Hindari pola halaman berurutan
Batasi hingga 200 halaman/hari per domain Tetap di bawah threshold deteksi

Untuk deployment, region cloud yang dekat dengan target memperkecil latensi — tim di Indonesia biasanya memakai AWS ap-southeast-1 (Singapura) atau ap-southeast-3 (Jakarta). Satu catatan kepatuhan: sesuai UU Pelindungan Data Pribadi (UU 27/2022), scraping sebaiknya dibatasi pada data lowongan publik dan menghindari data pribadi yang berada di balik login.


Pemecahan masalah umum

Masalah Penyebab Solusi
CAPTCHA di setiap pencarian IP di-flag atau rate limit terlampaui Ganti IP, tambahkan delay lebih panjang
Halaman hasil kosong CAPTCHA block dikembalikan Deteksi CAPTCHA sebelum di-parse
"Harap verifikasi bahwa kamu manusia" Deteksi bot terpicu Gunakan egress jaringan yang diotorisasi + UA realistis
Login diperlukan untuk data gaji Konten di-gate oleh platform Implementasikan authenticated session
Hasil berbeda dari browser Perbedaan lokasi/cookie Cocokkan Accept-Language dan proxy geografi

Biaya untuk scraping volume besar

Untuk operasi scraping berkelanjutan, model harga berbasis thread lebih mudah diprediksi ketimbang tarif per solve — poin yang penting bagi pekerja lepas dan agensi yang sensitif terhadap biaya. CaptchaAI menagih per thread bersamaan, dengan solve tak terbatas per thread selama sebulan. Paket BASIC ($15/bulan, 5 thread) cukup untuk scraper tunggal, sedangkan ADVANCE ($90/bulan, 50 thread) atau PREMIUM ($170/bulan, 100 thread) sesuai untuk beberapa job board yang di-scrape paralel. Berapa pun jumlah CAPTCHA yang muncul, biaya bulanan tetap.


Pertanyaan umum

Bagaimana cara mendeteksi CAPTCHA sebelum mem-parse halaman?

Periksa dulu apakah HTML respons memuat penanda seperti data-sitekey, g-recaptcha, atau cf-turnstile. Jika ada, halaman itu adalah tantangan, bukan data lowongan — selesaikan CAPTCHA lebih dulu sebelum menyerahkan HTML ke parser.

Apakah CaptchaAI bisa menangani Cloudflare Challenge di LinkedIn?

Ya. CaptchaAI mendukung Cloudflare Turnstile dan Cloudflare Challenge, serta reCAPTCHA v2 dan v3 yang dipakai job board lain. Satu integrasi API menangani seluruh tipe tersebut.

Berapa biaya untuk scraping job board volume tinggi?

Biaya mengikuti jumlah thread bersamaan, bukan jumlah solve. Mulai dari BASIC ($15/bulan, 5 thread); naikkan ke ADVANCE ($90/bulan, 50 thread) saat menjalankan banyak scraper sekaligus. Solve per thread tidak dibatasi.

Apakah scraping data lowongan diperbolehkan di Indonesia?

Sebagian besar job board memiliki ketentuan yang membatasi akses otomatis, dan penegakannya beragam. Fokuskan pada data lowongan yang bersifat publik, hindari data pribadi di balik login, dan perhatikan UU Pelindungan Data Pribadi (UU 27/2022). Panduan ini bukan nasihat hukum.


Panduan terkait

  • Rotasi egress jaringan yang diotorisasi untuk penyelesaian CAPTCHA
  • Sticky session vs rotating session untuk scraping
Komentar dinonaktifkan untuk artikel ini.