Kasus Penggunaan

Penanganan CAPTCHA untuk Pengumpulan Data Gaji dan Kompensasi

Basis data gaji, papan workeran, dan portal tenaga kerja pemerintah melindungi data kompensasi dengan Cloudflare Turnstile dan reCAPTCHA. CAPTCHA terpicu saat menanyakan kisaran gaji berdasarkan peran, lokasi, atau industri – terutama selama pengumpulan data massal di banyak jabatan. Berikut cara menanganinya.

Pola CAPTCHA di Portal Gaji

Jenis sumber CAPTCHA Pemicu
Situs perbandingan gaji Cloudflare Turnstile Request pencarian berulang
Filter gaji papan workeran reCAPTCHA v2 Beberapa pencarian gaji
Statistik ketenagakerjaan pemerintah Gambar CAPTCHA Request pengunduhan data
Halaman gaji enterprise Cloudflare Challenge Tampilan halaman massal
Platform survei SDM reCAPTCHA v3 Pengiriman formulir

Pengumpul Data Gaji

import requests
import time
import re
from dataclasses import dataclass

@dataclass
class SalaryRecord:
    title: str
    location: str
    min_salary: float
    max_salary: float
    median_salary: float
    sample_size: int
    source: str

class SalaryCollector:
    def __init__(self, api_key):
        self.api_key = api_key
        self.session = requests.Session()
        self.session.headers.update({
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
        })

    def collect_salary_data(self, portal_url, job_title, location):
        """Search for salary data, solving CAPTCHAs as needed."""
        response = self.session.get(portal_url, params={
            "title": job_title,
            "location": location
        })

        if self._is_turnstile_challenge(response):
            response = self._solve_turnstile_and_retry(response, portal_url)

        return self._parse_salary_data(response.text, portal_url)

    def collect_bulk(self, portal_url, job_titles, locations):
        """Collect salary data for multiple job title + location combos."""
        results = []

        for title in job_titles:
            for location in locations:
                try:
                    data = self.collect_salary_data(
                        portal_url, title, location
                    )
                    results.extend(data)
                    # Respectful delay between requests
                    time.sleep(2)
                except Exception as e:
                    print(f"Failed for {title} in {location}: {e}")

        return results

    def _is_turnstile_challenge(self, response):
        return (
            response.status_code == 403 or
            "cf-turnstile" in response.text or
            "challenges.cloudflare.com" in response.text
        )

    def _solve_turnstile_and_retry(self, response, url):
        match = re.search(r'data-sitekey="(0x[^"]+)"', response.text)
        if not match:
            raise ValueError("Turnstile sitekey not found")

        resp = requests.post("https://ocr.captchaai.com/in.php", data={
            "key": self.api_key,
            "method": "turnstile",
            "sitekey": match.group(1),
            "pageurl": url,
            "json": 1
        })
        task_id = resp.json()["request"]

        for _ in range(60):
            time.sleep(3)
            result = requests.get("https://ocr.captchaai.com/res.php", params={
                "key": self.api_key,
                "action": "get",
                "id": task_id,
                "json": 1
            })
            data = result.json()
            if data["status"] == 1:
                return self.session.post(url, data={
                    "cf-turnstile-response": data["request"]
                })

        raise TimeoutError("Turnstile solve timed out")

    def _parse_salary_data(self, html, source):
        from bs4 import BeautifulSoup
        soup = BeautifulSoup(html, "html.parser")
        records = []

        def text_or_empty(node):
            return node.text.strip() if node and node.text else ""

        for row in soup.select(".salary-row, .compensation-entry, tr[data-salary]"):
            try:
                records.append(SalaryRecord(
                    title=text_or_empty(row.select_one(".job-title, .title")),
                    location=text_or_empty(row.select_one(".location")),
                    min_salary=self._parse_amount(
                        text_or_empty(row.select_one(".min-salary, .low"))
                    ),
                    max_salary=self._parse_amount(
                        text_or_empty(row.select_one(".max-salary, .high"))
                    ),
                    median_salary=self._parse_amount(
                        text_or_empty(row.select_one(".median, .mid"))
                    ),
                    sample_size=int(
                        text_or_empty(row.select_one(".count, .sample")).replace(",", "") or 0
                    ),
                    source=source
                ))
            except (AttributeError, ValueError):
                continue

        return records

    def _parse_amount(self, text):
        if not text:
            return 0.0
        cleaned = re.sub(r'[^\d.]', '', text)
        return float(cleaned) if cleaned else 0.0


# Usage
collector = SalaryCollector("YOUR_API_KEY")
data = collector.collect_bulk(
    "https://salary.example.com/search",
    job_titles=["Software Engineer", "Data Analyst", "Product Manager"],
    locations=["San Francisco", "New York", "Austin"]
)

for record in data:
    print(f"{record.title} in {record.location}: "
          f"${record.min_salary:,.0f}–${record.max_salary:,.0f} "
          f"(median: ${record.median_salary:,.0f})")

Agregasi Multi-Sumber (JavaScript)

class SalaryAggregator {
  constructor(apiKey) {
    this.apiKey = apiKey;
    this.sources = [];
  }

  addSource(name, searchUrl) {
    this.sources.push({ name, searchUrl });
  }

  async collectForRole(jobTitle, location) {
    const results = [];

    for (const source of this.sources) {
      try {
        const data = await this.querySource(source, jobTitle, location);
        results.push({ source: source.name, ...data });
      } catch (error) {
        results.push({ source: source.name, error: error.message });
      }
    }

    return this.aggregateResults(results, jobTitle, location);
  }

  async querySource(source, jobTitle, location) {
    const url = `${source.searchUrl}?title=${encodeURIComponent(jobTitle)}&location=${encodeURIComponent(location)}`;
    const response = await fetch(url);
    const html = await response.text();

    if (html.includes('cf-turnstile') || response.status === 403) {
      return this.solveAndRetry(source.searchUrl, html, jobTitle, location);
    }

    return this.parseSalaryData(html);
  }

  async solveAndRetry(baseUrl, html, jobTitle, location) {
    const match = html.match(/data-sitekey="(0x[^"]+)"/);
    if (!match) throw new Error('Turnstile sitekey not found');

    const submitResp = await fetch('https://ocr.captchaai.com/in.php', {
      method: 'POST',
      body: new URLSearchParams({
        key: this.apiKey,
        method: 'turnstile',
        sitekey: match[1],
        pageurl: baseUrl,
        json: '1'
      })
    });
    const { request: taskId } = await submitResp.json();

    for (let i = 0; i < 60; i++) {
      await new Promise(r => setTimeout(r, 3000));
      const result = await fetch(
        `https://ocr.captchaai.com/res.php?key=${this.apiKey}&action=get&id=${taskId}&json=1`
      );
      const data = await result.json();
      if (data.status === 1) {
        const response = await fetch(baseUrl, {
          method: 'POST',
          body: new URLSearchParams({
            'cf-turnstile-response': data.request,
            title: jobTitle,
            location: location
          })
        });
        return this.parseSalaryData(await response.text());
      }
    }
    throw new Error('Turnstile solve timed out');
  }

  aggregateResults(results, jobTitle, location) {
    const valid = results.filter(r => !r.error && r.median);
    if (valid.length === 0) return null;

    const medians = valid.map(r => r.median);
    return {
      jobTitle,
      location,
      avgMedian: medians.reduce((a, b) => a + b, 0) / medians.length,
      sources: valid.length,
      range: { min: Math.min(...medians), max: Math.max(...medians) }
    };
  }
}

// Usage
const aggregator = new SalaryAggregator('YOUR_API_KEY');
aggregator.addSource('SalaryDB', 'https://salarydb.example.com/search');
aggregator.addSource('PayScale', 'https://payscale.example.com/lookup');

const result = await aggregator.collectForRole('Software Engineer', 'San Francisco');
console.log(`Median salary: $${result.avgMedian.toLocaleString()} (${result.sources} sources)`);

Strategi Pengumpulan Data

Pendekatan Volume per hari frecookiensi CAPTCHA Terbaik untuk
Berurutan dengan penundaan 100–500 pertanyaan Rendah Survei kecil
Rotasi proxy 500–2.000 pertanyaan Sedang Analisis wilayah
Paralel multi-sesi 2.000–10.000 cookieri Tinggi Kumpulan data yang komprehensif

Pemecahan Masalah

Masalah Penyebab Solusi
Turnstile di setiap pencarian Sesi berakhir Pertahankan cookie qa_validation_cookie
Data gaji menunjukkan "Diperlukan login" Portal memerlukan otentikasi Otentikasi sebelum mencari
Hasil kosong setelah solve CAPTCHA Parameter POST hilang Sertakan semua bidang formulir tersembunyi
Data tidak konsisten antar proses Portal menunjukkan rentang yang berbeda Gunakan parameter cookieri yang konsisten

Pertanyaan Umum

Berapa banyak pertanyaan gaji yang dapat saya buat per hari?

Itu tergantung pada rate limit portal, bukan CaptchaAI. CaptchaAI memecahkan Turnstile dengan tingkat keberhasilan 100%. Request ruang berjarak 2–5 detik dan memutar proxy untuk pengumpulan volume tinggi.

Haruskah saya menggunakan proxy untuk pengumpulan data gaji?

Ya, terutama untuk pengumpulan massal di ribuan jabatan. egress jaringan yang diotorisasi mengurangi frecookiensi CAPTCHA secara signifikan dibandingkan dengan IP pusat data.

Bisakah saya mengumpulkan data gaji secara real-time?

Sebagian besar portal gaji memperbarui data bulanan atau triwulanan, sehingga pengumpulan data secara real-time tidak diperlukan. Jadwalkan pengumpulan mingguan atau bulanan untuk kumpulan data yang komprehensif.

Artikel Terkait

Langkah Selanjutnya

Kumpulkan data kompensasi dengan andal –dapatkan kunci API CaptchaAI Andadan menangani CAPTCHA portal gaji secara otomatis.

Komentar dinonaktifkan untuk artikel ini.