Cas d'Usage

Gestion des CAPTCHA pour la collecte de données sur les salaires et les rémunérations

Les bases de données sur les salaires, les sites d'emploi et les portails gouvernementaux du travail protègent les données de rémunération avec Cloudflare Turnstile et reCAPTCHA. Les CAPTCHA se déclenchent lors de l'interrogation des échelles salariales par rôle, lieu ou secteur d'activité, en particulier lors de la collecte de données en masse sur de nombreux titres de poste. Voici comment les gérer.

Modèles CAPTCHA sur les portails de salaires

Type de source CAPTCHA Déclencheur
Sites de comparaison de salaires Cloudflare Turnstile Requêtes de recherche répétées
Filtres salariaux sur les sites d'emploi reCAPTCHA v2 Plusieurs recherches de salaire
Statistiques gouvernementales du travail Image CAPTCHA Demandes de téléchargement de données
Pages de salaires d'entreprise Cloudflare Challenge Pages vues en masse
Plateformes d'enquêtes RH reCAPTCHA v3 Soumissions de formulaires

Collecteur de données sur les salaires

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_of(node):
          return node.get_text(strip=True) if node else ""

        for row in soup.select(".salary-row, .compensation-entry, tr[data-salary]"):
            try:
                records.append(SalaryRecord(
              title=text_of(row.select_one(".job-title, .title")),
              location=text_of(row.select_one(".location")),
                    min_salary=self._parse_amount(
                text_of(row.select_one(".min-salary, .low"))
                    ),
                    max_salary=self._parse_amount(
                text_of(row.select_one(".max-salary, .high"))
                    ),
                    median_salary=self._parse_amount(
                text_of(row.select_one(".median, .mid"))
                    ),
              sample_size=int(text_of(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})")

Agrégation multi-sources (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)`);

Stratégie de collecte de données

Approche Volume par jour Fréquence des CAPTCHA Idéal pour
Séquentiel avec délais 100 à 500 requêtes Faible Petites enquêtes
Rotation des procurations 500 à 2 000 requêtes Modéré Analyse régionale
Parallèle multi-sessions 2 000 à 10 000 requêtes Élevé Ensembles de données complets

Dépannage

Problème Parce que Corriger
Tourniquet à chaque recherche Session expirée Conserver le cookie cf_clearance
Les données salariales affichent « Connexion requise » Le portail nécessite une authentification Authentifiez-vous avant de rechercher
Résultats vides après la résolution du CAPTCHA Paramètres POST manquants Inclure tous les champs de formulaire masqués
Données incohérentes entre les exécutions Le portail affiche différentes gammes Utiliser des paramètres de requête cohérents

FAQ

Combien de demandes de salaire puis-je effectuer par jour ?

Cela dépend des limites de débit du portail, pas de CaptchaAI. CaptchaAI résout Turnstile avec un taux de réussite de 100 %. Espacez les demandes à un intervalle de 2 à 5 secondes et faites pivoter les proxys pour une collecte de gros volumes.

Dois-je utiliser des proxys pour la collecte de données salariales ?

Oui, en particulier pour la collecte groupée sur des milliers de titres de poste. Les proxys résidentiels réduisent considérablement la fréquence des CAPTCHA par rapport aux adresses IP des centres de données.

Puis-je collecter des données salariales en temps réel ?

La plupart des portails salariaux mettent à jour les données mensuellement ou trimestriellement, la collecte en temps réel n'est donc pas nécessaire. Planifiez des collectes hebdomadaires ou mensuelles pour des ensembles de données complets.

Articles connexes

Prochaines étapes

Collectez les données de rémunération de manière fiable :récupérez votre clé API CaptchaAIet gérez automatiquement les CAPTCHA du portail salarial.

Les commentaires sont désactivés pour cet article.