Use Cases

Парсинг досок объявлений с обработкой CAPTCHA через CaptchaAI

Через несколько десятков запросов Indeed, LinkedIn и Glassdoor вместо списка вакансий показывают reCAPTCHA или Cloudflare Turnstile — это стандартная защита джоб-бордов от массового сбора данных, а не разовый сбой. Ниже — рабочие настройки, которые снижают долю заблокированных запросов, карта того, какая CAPTCHA стоит на каждой площадке, и парсер на Python, который решает её через API CaptchaAI.


Как настроить парсинг джоб-бордов, чтобы он не падал на CAPTCHA

Практика Зачем нужна
Резидентные прокси с ротацией Распределяют запросы по разным реальным IP вместо одного адреса
Пауза 3–5 секунд между страницами Снижает нагрузку на сервер площадки и риск упереться в rate-limit
Один и тот же User-Agent на всю сессию Меньше ложных срабатываний защиты внутри одной сессии
Принимать cookies сессии Джоб-борды связывают запросы через cookies — без них каждая страница выглядит новым визитом
Случайный порядок запросов к страницам поиска Меньше похоже на автоматический перебор по шаблону
Лимит около 200 страниц в день на домен Снижает риск временной блокировки IP

Где на джоб-бордах встречается CAPTCHA

Платформа Тип CAPTCHA Когда появляется Какие данные доступны
Indeed reCAPTCHA v2 Высокая частота запросов Вакансии, зарплаты
LinkedIn Cloudflare Challenge Обнаружение бота Вакансии, данные о компаниях
Glassdoor reCAPTCHA v2 Обнаружение парсинга Отзывы, зарплаты, вакансии
ZipRecruiter Cloudflare Turnstile Автоматизированный доступ Списки вакансий
Monster reCAPTCHA v2 Страницы поиска Списки вакансий
CareerBuilder reCAPTCHA v3 Вход, поиск Вакансии, поиск по резюме

Типичные проблемы при парсинге джоб-бордов

Проблема Причина Что делать
CAPTCHA на каждом запросе IP в чёрном списке или превышена частота запросов Смените IP, увеличьте паузы между запросами
Пустая страница результатов Вместо списка вакансий вернулся экран CAPTCHA Проверяйте наличие CAPTCHA до парсинга HTML
«Подтвердите, что вы не робот» Сработала защита от ботов Резидентный прокси + реалистичный User-Agent
Данные о зарплате требуют входа Контент закрыт авторизацией Реализуйте сессию с аутентификацией
Результаты отличаются от браузера Разное местоположение или cookies Синхронизируйте Accept-Language и гео прокси

Парсер вакансий с автоматическим решением CAPTCHA

Ниже — рабочий парсер на Python: перед разбором страницы он проверяет, не подсунул ли сайт reCAPTCHA или Cloudflare Turnstile вместо результатов поиска, получает токен через CaptchaAI и повторяет запрос уже с решённой капчей.

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:[email protected]: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)}")

Сбор данных о зарплатах по нескольким городам и вакансиям

Такой подход особенно востребован у HR-аналитиков и рекрутинговых агентств из СНГ, которые готовят бенчмарки зарплат по удалённым западным вакансиям для своих клиентов.

Например, сравнивают ставки Data Engineer в Сан-Франциско, Нью-Йорке, Остине и по вакансиям с меткой Remote, чтобы обосновать вилку оплаты при найме в России, Беларуси или Казахстане. Если в выгрузку попадают не только сами объявления, но и отклики или резюме кандидатов, стоит заранее убедиться, что обработка таких данных соответствует 152-ФЗ «О персональных данных» (для аудитории в РФ) или требованиям GDPR — это отдельный вопрос комплаенса, а не часть самого парсинга.

import csv


def collect_salary_data(titles, locations, output_file):
    """Collect salary data across job titles and locations."""
    scraper = JobBoardScraper(
        proxy="http://user:[email protected]: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",
)

Часто задаваемые вопросы

Как часто стоит обновлять данные о вакансиях, чтобы бенчмарк оставался актуальным?

Для зарплатной аналитики хватает повторного обхода раз в 3–7 дней — на большинстве джоб-бордов объявления не обновляются чаще. Более частый обход увеличивает долю запросов, упирающихся в CAPTCHA, но не добавляет полезных данных.

Что делать, если LinkedIn или CareerBuilder возвращают reCAPTCHA v3 с низким score вместо явной проверки?

Низкий score обычно означает, что сессия выглядит нетипично: новый IP без истории, отсутствие cookies, нестандартные заголовки. CaptchaAI решает и вызовы reCAPTCHA v3, но снизить их частоту помогает более «тёплая» сессия — постоянные cookies и один User-Agent на весь проход.

Сколько потоков CaptchaAI нужно, чтобы одновременно обходить несколько джоб-бордов?

Для 3–5 источников с умеренной частотой обычно достаточно тарифа ADVANCE ($90/мес, 50 потоков). Для более крупных пайплайнов с параллельным сбором по нескольким городам подойдёт PREMIUM ($170/мес, 100 потоков).

Какой прокси лучше подходит для парсинга джоб-бордов?

Резидентные прокси с ротацией — оптимальный баланс стоимости и стабильности. IP дата-центров LinkedIn и Glassdoor блокируют быстрее остальных площадок.

Законно ли собирать вакансии с публичных досок объявлений?

Сами объявления обычно являются публичными данными, но в пользовательском соглашении большинства площадок есть пункт против автоматического доступа, и соблюдение варьируется от площадки к площадке. Если в выгрузку попадают персональные данные — например, из откликов кандидатов, — их обработка отдельно регулируется 152-ФЗ или GDPR, и это стоит проверить до старта сбора.


Похожие материалы


Собирайте данные о вакансиях и зарплатах в промышленном масштабе — получите API-ключ CaptchaAI и уберите блокировки CAPTCHA из пайплайна.

Комментарии для этой статьи отключены.