Trường Hợp Sử Dụng

Vượt CAPTCHA khi scrape bảng tin tuyển dụng bằng CaptchaAI

Indeed, LinkedIn và Glassdoor bật CAPTCHA ngay khi phát hiện request tần suất cao — nhưng scraper vẫn chạy ổn định nếu xử lý đúng ba dạng chính: reCAPTCHA v2, reCAPTCHA v3 và Cloudflare Challenge/Turnstile. CaptchaAI giải cả ba nên job thu thập không phải dừng giữa chừng. Bài này dành cho dev cần một scraper chạy được, không phải bài tổng quan lý thuyết về web scraping.


CAPTCHA xuất hiện ở đâu trên từng nền tảng tuyển dụng

Chọn đúng method khi gọi CaptchaAI theo từng nền tảng:

Nền tảng Loại CAPTCHA Kích hoạt khi Dữ liệu thu được
Indeed reCAPTCHA v2 Request tần suất cao Tin tuyển dụng, mức lương
LinkedIn Cloudflare Challenge Phát hiện bot Tin tuyển dụng, dữ liệu công ty
Glassdoor reCAPTCHA v2 Phát hiện scraping Đánh giá, lương, tin tuyển dụng
ZipRecruiter Cloudflare Turnstile Truy cập tự động Tin tuyển dụng
Monster reCAPTCHA v2 Trang tìm kiếm Tin tuyển dụng
CareerBuilder reCAPTCHA v3 Đăng nhập, tìm kiếm Tin tuyển dụng, tìm hồ sơ

Ví dụ thực tế: đội outsource theo dõi lương ngành IT

Nhiều công ty outsource và agency tuyển dụng ở TP.HCM, Hà Nội theo dõi tin tuyển dụng, mức lương trên Indeed, LinkedIn, Glassdoor để dựng báo cáo benchmark cho khách hàng nước ngoài — khác việc đọc ITviec hay TopDev, vốn hiếm khi chặn traffic thường bằng CAPTCHA. Crawl liên tục nhiều trang ở nước ngoài gần như chắc chắn gặp CAPTCHA sau vài chục request; scraper cần giải tự động thay vì dừng chờ xử lý tay. Quy mô vài nghìn trang/ngày, BASIC ($15/tháng, 5 thread) đủ; crawl song song nhiều domain thường cần ADVANCE ($90/tháng, 50 thread) trở lên.


Scraper xử lý CAPTCHA cho bảng tin tuyển dụng

Đoạn code dưới đây phát hiện CAPTCHA, gửi task tới CaptchaAI, polling kết quả rồi submit lại request với token — dùng chung cho cả reCAPTCHA v2 lẫn 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:[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)}")

Thu thập dữ liệu tiền lương

Cùng một JobBoardScraper, đổi truy vấn theo từng cặp title/location rồi ghi ra CSV để dựng bảng benchmark lương.

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

Khắc phục sự cố thường gặp

  1. CAPTCHA ở mọi lần tìm kiếm — IP bị gắn cờ; đổi nguồn request, giãn delay dài hơn.
  2. Trang kết quả trả về trống — thực chất là trang chặn CAPTCHA; kiểm tra dấu hiệu trước khi parse HTML.
  3. Gặp "Xin hãy xác minh bạn là con người" — bộ phát hiện bot kích hoạt; dùng đa dạng nguồn request + User-Agent thực tế.
  4. Yêu cầu đăng nhập mới thấy lương — nền tảng khoá nội dung; cần phiên đã xác thực.
  5. Kết quả khác trình duyệt thật — lệch vị trí hoặc cookie; khớp Accept-Language và proxy theo khu vực.

Cấu hình request ổn định để giảm tần suất gặp CAPTCHA

  • Đa dạng nguồn request (proxy chất lượng) thay vì dồn vào một nguồn duy nhất.
  • Giãn 3-5 giây giữa các trang, mô phỏng tốc độ duyệt của người thật.
  • Giữ nguyên một User-Agent trong suốt một phiên.
  • Chấp nhận cookie vì bảng tin tuyển dụng thường theo dõi phiên qua cookie.
  • Ngẫu nhiên hoá thứ tự tìm kiếm, tránh mẫu truy cập tuần tự dễ bị nhận diện.
  • Giới hạn khoảng 200 trang/ngày mỗi domain để dưới ngưỡng cảnh báo.

Câu hỏi thường gặp

  • CaptchaAI hỗ trợ loại CAPTCHA nào trên bảng tin tuyển dụng? Có — reCAPTCHA v2, reCAPTCHA v3 và Cloudflare Turnstile/Challenge, đúng ba loại xuất hiện trên Indeed, LinkedIn, Glassdoor, ZipRecruiter, Monster và CareerBuilder trong bảng ở trên.
  • Cần bao nhiêu thread để scrape hàng nghìn tin mỗi ngày? BASIC (5 thread) đủ cho job tuần tự vài nghìn trang/ngày; crawl nhiều domain cùng lúc thì cân nhắc ADVANCE (50 thread). CaptchaAI tính theo thread đang chạy, không theo số lần giải.
  • Nên dùng polling hay callback để lấy token? Polling (gọi res.php mỗi 5 giây như code trên) đơn giản nhất cho script batch. Callback hợp lý hơn khi scraper chạy dài hạn.
  • Bảng tin tuyển dụng có chặn việc thu thập dữ liệu tự động không? Hầu hết có điều khoản không khuyến khích truy cập tự động nhưng mức thực thi khác nhau; CAPTCHA là lớp bảo vệ chính, và đó là phần CaptchaAI xử lý.
  • Loại proxy nào phù hợp khi scrape Indeed, LinkedIn, Glassdoor? Ưu tiên đa dạng nguồn request chất lượng cao — IP datacenter bị LinkedIn, Glassdoor chặn rất nhanh.

Hướng dẫn liên quan


Thu thập dữ liệu thị trường tuyển dụng ổn định — lấy API key CaptchaAI và giải CAPTCHA tự động ngay hôm nay.

Os comentários estão desativados para este artigo.