Der Engpass bei der Erfassung von Gehalts- und Vergütungsdaten ist selten das Parsen der Tabellen – es ist die CAPTCHA-Abfrage, die nach wenigen Suchen erscheint. Sobald Sie Gehaltsspannen nach Rolle, Standort oder Branche im großen Stil abfragen, blenden Gehaltsvergleichsportale, Jobbörsen und staatliche Arbeitsmarktstatistiken Cloudflare Turnstile oder reCAPTCHA ein. CaptchaAI löst diese Abfrage in derselben Sitzung und übergibt das Token an Ihren Collector, sodass die Erfassung ohne manuelles Eingreifen weiterläuft. Dieser Leitfaden zeigt die typischen CAPTCHA-Muster, lauffähigen Collector-Code in Python und JavaScript, eine Durchsatzstrategie und einen Blick auf die DSGVO-Seite.
Wo CAPTCHAs auf Gehaltsportalen auftreten
Nicht jedes Portal schützt sich gleich. Die folgende Übersicht zeigt, welcher CAPTCHA-Typ Ihnen bei welcher Quelle typischerweise begegnet und was ihn auslöst:
| Quelltyp | CAPTCHA | Auslöser |
|---|---|---|
| Gehaltsvergleichsseiten | Cloudflare Turnstile | Wiederholte Suchanfragen |
| Gehaltsfilter der Jobbörse | reCAPTCHA v2 | Mehrere Gehaltssuchen |
| Staatliche Arbeitsstatistik | Bild-CAPTCHA | Anfragen zum Herunterladen von Daten |
| Gehaltsseiten für Unternehmen | Cloudflare Challenge | Massenseitenaufrufe |
| HR-Umfrageplattformen | reCAPTCHA v3 | Formulareinreichungen |
CaptchaAI deckt genau diese Familie ab – Turnstile und Cloudflare Challenge, reCAPTCHA v2 und v3 sowie Bild- und Grid-CAPTCHAs. hCaptcha und FunCaptcha gehören nicht dazu; auf diese Typen setzen Gehaltsportale in der Praxis aber ohnehin selten.
Gehaltsdaten-Collector in Python
Der folgende Collector fragt ein Portal ab, erkennt eine Turnstile-Abfrage und löst sie über CaptchaAI, bevor er die Ergebnisseite parst. Die Methode collect_bulk iteriert über Kombinationen aus Berufsbezeichnung und Standort und hält zwischen den Anfragen eine bewusste Verzögerung ein.
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})")
Der Ablauf ist bei jedem Turnstile-Portal identisch: den sitekey aus dem HTML lesen, die Aufgabe an in.php übermitteln, das Ergebnis über res.php abfragen (Polling) und das zurückgegebene cf-turnstile-response-Token im selben Session-Kontext absenden. Erst danach liefert das Portal die eigentlichen Gehaltsdaten. Preise stehen im Code bewusst in US-Dollar, weil viele Quellen international sind – für die Ausgabe können Sie die Formatierung an Ihren Markt anpassen.
Aggregation über mehrere Quellen (JavaScript)
Verlässliche Vergütungsbenchmarks entstehen selten aus einer einzigen Quelle. Der folgende Aggregator fragt mehrere Portale nacheinander ab, löst pro Quelle die Turnstile-Abfrage und bildet aus den Medianwerten einen Durchschnitt samt Spannbreite.
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)`);
Das Muster ist dasselbe wie in Python: pro Quelle Turnstile lösen, das Token einfügen, die Antwort parsen. Weichen die Mediane mehrerer Quellen stark voneinander ab, ist die Spannbreite oft aussagekräftiger als der Mittelwert – gerade bei kleinen Stichproben.
Durchsatz und Erfassungsstrategie
Wie viele Abfragen pro Tag realistisch sind, hängt weniger von CaptchaAI als von den Rate-Limits der Quelle ab. Als Orientierung:
| Ansatz | Volumen pro Tag | CAPTCHA-Häufigkeit | Am besten für |
|---|---|---|---|
| Sequentiell mit Verzögerungen | 100–500 Abfragen | Niedrig | Kleine Umfragen |
| Proxy-Rotation | 500–2.000 Abfragen | Mäßig | Regionale Analyse |
| Mehrere Sitzungen parallel | 2.000–10.000 Abfragen | Hoch | Umfangreiche Datensätze |
Für regionale Auswertungen mit rotierenden Residential-Proxys eignet sich ein einfaches Worker-Setup bei Anbietern wie Hetzner oder netcup gut. Entscheidend ist ein ruhiges Anfrageprofil: konstante Sessions, moderate Intervalle und saubere Proxys senken die CAPTCHA-Häufigkeit spürbar und halten damit auch die Kosten niedrig.
DSGVO und rechtssichere Erfassung
Gehalts- und Vergütungsdaten berühren in der DACH-Region schnell den Datenschutz. Beziehen sich die erfassten Werte auf identifizierbare Personen – etwa individuelle Gehälter statt aggregierter Spannen –, gelten sie nach DSGVO als personenbezogene Daten, und auch die IP-Adressen aus Ihrem Erfassungs-Setup fallen darunter. Prüfen Sie vor einem größeren Lauf die Nutzungsbedingungen der Quelle, Ihre Rechtsgrundlage und ob aggregierte Marktdaten für Ihre Fragestellung ausreichen. CaptchaAI verarbeitet ausschließlich die CAPTCHA-Abfrage; die Verantwortung für die erhobenen Inhalte und deren Rechtsgrundlage bleibt bei Ihnen.
Fehlerbehebung
| Problem | Ursache | Lösung |
|---|---|---|
| Zu viele CAPTCHAs in kurzer Zeit | Abrufrate oder Parallelität ist für die Quelle zu aggressiv | Drosseln Sie die Intervalle, halten Sie Sessions stabil und prüfen Sie die Qualität Ihrer Proxys |
| Daten fehlen trotz gelöster CAPTCHA | Der Parser liest eine alte oder unvollständige Ansicht aus | Extrahieren Sie Daten erst nach erfolgreicher Token-Anwendung in derselben Sitzung |
| Kosten steigen stärker als erwartet | Zu viele Wiederholungen oder unnötige Seitenaufrufe lösen zusätzliche Challenges aus | Lösen Sie nur kritische Schritte und protokollieren Sie Wiederholungen pro Quelle |
Häufige Fragen
Welche CAPTCHA-Typen löst CaptchaAI auf Gehaltsportalen?
Cloudflare Turnstile und Cloudflare Challenge, reCAPTCHA v2 und v3 sowie Bild- und Grid-CAPTCHAs – also genau die Typen, die auf Gehalts- und Jobportalen üblich sind. hCaptcha und FunCaptcha unterstützt CaptchaAI nicht; trifft eine Quelle ausschließlich darauf ab, brauchen Sie eine alternative Datenquelle.
Ist das automatisierte Erfassen von Gehaltsdaten DSGVO-konform?
Das hängt von den Daten und der Quelle ab. Aggregierte Gehaltsspannen sind unkritischer als personenbezogene Einzelwerte. Prüfen Sie die Nutzungsbedingungen des Portals und Ihre Rechtsgrundlage; CaptchaAI löst nur die CAPTCHA-Abfrage und trifft keine Aussage über die Zulässigkeit Ihrer Erhebung.
Was kostet die CAPTCHA-Verarbeitung bei hohem Abfragevolumen?
CaptchaAI rechnet pro gleichzeitigem Thread ab, nicht pro Lösung – jeder Tarif enthält unbegrenzte Lösungen pro Thread. Für einzelne Erfassungsläufe reicht BASIC (15 $/Monat, 5 Threads); für parallele Multi-Session-Erfassung über viele Berufsbilder skalieren ADVANCE (90 $/Monat, 50 Threads) oder höher. Die Kosten steigen also mit der Parallelität, nicht mit der Zahl der Suchen.
Warum erscheint Turnstile nach einigen Suchanfragen erneut?
Portale werten Anfragerate, Session-Alter und IP-Reputation aus. Bricht die Session ab oder feuern zu viele Anfragen von derselben IP, wird die Abfrage erneut ausgelöst. Halten Sie Cookies über die Session hinweg stabil, verteilen Sie Anfragen über Residential-Proxys und lassen Sie zwischen den Suchen einige Sekunden Abstand.