Use Cases

招聘网站抓取遇到验证码怎么处理?CaptchaAI 方案

招聘网站抓取被拦,是触发了机器人检测——请求频率或翻页行为让 Indeed、领英(LinkedIn)判定这是自动化流量。国内猎头、薪酬调研团队做跨境对标时常要采集这类数据。下面先回答三个高频问题,再给出可复用的抓取器实现。


常见问题

抓取 Indeed、LinkedIn 这类海外招聘网站合规吗?

条款通常不鼓励自动化访问,执行力度各家不同。国内团队跨境采集建议只采集职位、薪资等公开信息,参照网络安全法与 PIPL 处理。

LinkedIn 的 Cloudflare 验证要怎么处理?

命中后页面会带 cf-turnstile 标志,下文 _solve_and_retry 会自动识别并用 turnstile 方法提交 sitekey,拿到 token 写入 cf-turnstile-response 重新提交。

抓取 CareerBuilder 这类用 reCAPTCHA v3 的网站要注意什么?

reCAPTCHA v3 不弹验证框,而是按行为打分,分数低于站点阈值时会静默拦截。solve_captchamethod 换成 userrecaptcha 同样能拿到 token,但请求节奏和 User-Agent 一致性比 v2 更关键。


主流海外招聘网站会遇到什么验证码

网站 验证码类型 触发条件 可抓取数据
Indeed reCAPTCHA v2 请求频率过高 职位列表、薪资
领英 Cloudflare 验证 机器人检测 职位、公司信息
Glassdoor reCAPTCHA v2 抓取行为触发 点评、薪资、职位
ZipRecruiter Cloudflare Turnstile 自动化访问 职位列表
Monster reCAPTCHA v2 搜索页 职位列表
CareerBuilder reCAPTCHA v3 登录、搜索 职位列表、简历检索

抓取招聘网站的标准配置建议

  1. 轮换自有服务器基础设施——请求分散在真实 IP 上
  2. 页面间隔 3–5 秒——模拟真实浏览节奏
  3. 每会话固定 User-Agent——避免特征不一致
  4. 接受 cookie——招聘网站靠 cookie 追踪会话
  5. 打乱搜索顺序——避免翻页规律模式
  6. 每域名每天限 200 页——保持在检测阈值以下

带验证码处理的招聘网站抓取器

solve_captcha 提交 sitekey、pageurl 到 in.php,轮询 res.php 拿结果;JobBoardScraper 自动区分 reCAPTCHA 和 Turnstile,写入对应字段后重新提交:

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)}")

批量采集薪资数据做市场对标

做薪酬报告可外包一层批处理:按职位 × 城市循环调用 search_jobs,样本写入 CSV:

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",
)

常见故障排查

问题 原因 处理方式
每次搜索都触发验证码 IP 已标记或频率超限 换 IP,拉长间隔
结果为空 返回的是拦截页 解析前先判断是否命中验证码
提示“请证明你不是机器人” 触发机器人检测 换 IP + 真实 UA
薪资数据需登录 内容受限 走已登录会话流程
结果和浏览器不一样 地理位置或 cookie 不一致 对齐 Accept-Language 与代理地理位置

选择合适的 CaptchaAI 套餐

CaptchaAI 按并发线程计费,线程内可无限次识别。小规模测试用 STANDARD($30/月,15 线程)够用,并发更大时升级 ADVANCE($90/月,50 线程)。


相关指南


大规模采集招聘市场数据——获取你的 CaptchaAI 密钥,把验证码处理接入自动化流程。

该文章已禁用评论。