Cách thu thập dữ liệu Indeed bằng Python (và tránh lỗi 403) năm 2026

Cập nhật lần cuối vào April 22, 2026
Cách thu thập dữ liệu Indeed bằng Python (và tránh lỗi 403) năm 2026

Ở đâu đó vào khoảng lần thứ năm mươi tôi copy-paste một chức danh công việc từ Indeed vào bảng tính, tôi bắt đầu nghi ngờ lựa chọn nghề nghiệp của mình. Nếu bạn từng cố lấy dữ liệu có cấu trúc từ Indeed bằng chương trình, chắc bạn sẽ hiểu ngay “cú chốt” này: lỗi 403 không phải bug — nó là một tính năng trong hệ thống phòng thủ của Indeed.

Indeed là trang tuyển dụng lớn nhất thế giới, với khoảng 350 triệu lượt truy cập duy nhất mỗi tháng, 130 triệu tin tuyển dụng tại bất kỳ thời điểm nào, và hoạt động ở hơn 60 quốc gia. Điều đó khiến nó trở thành một trong những nguồn dữ liệu thị trường việc làm phong phú nhất trên hành tinh — và cũng là một trong những nơi khó thu thập dữ liệu nhất. Công cụ mã nguồn mở JobFunnel (hàng nghìn sao trên GitHub) thậm chí đã bị người bảo trì lưu trữ lại vào tháng 12 năm 2025 sau nhiều năm thua trong cuộc đua chống bot. Nguyên văn lời người bảo trì: "All users can scrape some jobs, but are quickly hit by captcha, and the scraping fails, yielding no jobs." Một cộng tác viên khác báo cáo rằng họ gặp CAPTCHA ngay ở yêu cầu đầu tiên. Vậy nên — đây không phải mục tiêu thu thập dữ liệu dễ nhằn. Trong hướng dẫn này, tôi sẽ đi qua mọi cách thực tế để thu thập Indeed bằng Python, chỉ cho bạn cách thật sự vượt qua “ải” 403, và — với những ai muốn bỏ qua luôn khâu gỡ lỗi — minh họa một giải pháp không cần code bằng Thunderbit.

Thu thập dữ liệu Indeed bằng Python nghĩa là gì?

Cốt lõi của web scraping là tự động trích xuất dữ liệu có cấu trúc từ trang web. Khi nói đến việc thu thập Indeed bằng Python, tức là bạn viết một script truy cập trang kết quả tìm kiếm và các trang chi tiết công việc của Indeed, đọc HTML nền (hoặc dữ liệu nhúng), rồi lấy ra các trường như chức danh, công ty, địa điểm, mức lương và mô tả công việc để đưa vào định dạng dùng được — CSV, cơ sở dữ liệu, Google Sheet.

Những thư viện Python thường dùng gồm Requests (gửi HTTP), BeautifulSoup (phân tích HTML), và Selenium hoặc Playwright (tự động hóa trình duyệt). Nhưng Indeed không phải một trang tĩnh đơn giản. Nó là dạng lai: HTML do server render kèm một blob JSON trạng thái nhúng, phía trước là Cloudflare Bot Management. Nghĩa là scraper của bạn phải xử lý nội dung render bằng JavaScript, tên lớp CSS thay đổi liên tục, và các lớp bảo vệ chống bot rất gắt — tất cả trước cả khi bạn parse được một chức danh công việc.

Năm 2026 cũng không có API Indeed miễn phí, chính thức, chỉ đọc. Publisher Jobs API cũ đã bị ngừng hỗ trợ khoảng năm 2020, và thứ còn lại chỉ dành cho phía nhà tuyển dụng (Job Sync, Sponsored Jobs). Vì vậy, hoặc là thu thập dữ liệu, hoặc là trả tiền cho bên cung cấp dữ liệu thứ ba — đó gần như là hai lựa chọn thực tế duy nhất.

Vì sao nên thu thập dữ liệu việc làm từ Indeed?

Lý do kinh doanh rất rõ ràng: duyệt thủ công hàng nghìn tin tuyển dụng là điều không thực tế, và dữ liệu bên trong các tin đó thực sự rất có giá trị.

indeed_stats_dca2a43cec.png

Trường hợp sử dụngAi hưởng lợiVí dụ
Tạo khách hàng tiềm năngĐội sales & tuyển dụngLập danh sách công ty đang tuyển cùng thông tin liên hệ
Nghiên cứu thị trường việc làmNhà phân tích, đội ngũ HRXác định kỹ năng đang tăng, mức lương chuẩn theo khu vực
Phân tích cạnh tranhNhà tuyển dụng, công ty tuyển dụngTheo dõi xu hướng tuyển dụng và mức lương của đối thủ
Tự động hóa tìm việc cá nhânNgười tìm việcGom các tin phù hợp với tiêu chí trên nhiều khu vực
Dữ liệu huấn luyện cho mô hình MLNhà khoa học dữ liệuXây mô hình dự đoán lương từ dữ liệu lịch sử

Nghiên cứu của Indeed Hiring Lab xác nhận rằng dữ liệu đăng tin bám sát BLS JOLTS và có thể đóng vai trò như chỉ báo gần thời gian thực về tình hình lao động ở Mỹ. Các quỹ đầu cơ dùng tốc độ đăng tin làm tín hiệu dữ liệu thay thế. Đội HR dùng dải lương thu thập được để đối chiếu mức lương. Và các nhà tuyển dụng xây danh sách khách hàng tiềm năng từ những công ty đang thực sự tuyển người.

Một lưu ý thực tế: dữ liệu lương trên Indeed đã khá hơn nhưng vẫn chưa đầy đủ. Tính đến giữa năm 2025, khoảng 59% tin ở Mỹ có thông tin lương, nhưng chỉ khoảng 22% nêu con số chính xác — phần còn lại là khoảng lương. Bất kỳ phân tích lương nào dựa trên dữ liệu Indeed cũng cần tính đến độ thiếu này.

Chọn phương pháp thu thập Indeed bằng Python

Không có một cách “đúng” duy nhất để scrape Indeed. Cách tốt nhất phụ thuộc vào trình độ, lượng dữ liệu bạn cần và mức bảo trì bạn sẵn sàng chấp nhận. Tôi đã thử cả bốn cách chính, và đây là so sánh:

Tiêu chíBS4 + RequestsSeleniumJSON ẩn (window.mosaic)Không cần code (Thunderbit)
Độ khóNgười mới bắt đầuTrung cấpTrung cấp - nâng caoKhông có (2 cú nhấp)
Tốc độNhanhChậm (render trình duyệt)NhanhNhanh (thu thập trên cloud)
Nội dung render bằng JSKhôngCó (dữ liệu nhúng)
Khả năng chống botThấpTrung bình (có thể bị phát hiện)Trung bình - caoCao (xử lý tự động)
Bảo trì khi HTML đổiCao (selector dễ gãy)CaoTrung bình (cấu trúc JSON ổn định hơn)Không cần (AI tự thích ứng)
Phù hợp nhất choMẫu thử nhanhTrang động, nội dung cần đăng nhậpDữ liệu có cấu trúc số lượng lớnNgười không biết code, cần kết quả nhanh

Hướng dẫn này sẽ đi qua từng phương pháp. Nếu bạn là lập trình viên Python, hãy đọc các phần BS4, JSON ẩn và Selenium. Nếu bạn không biết code (hoặc chỉ chán ngấy việc gỡ lỗi 403), hãy bỏ qua sang phần Thunderbit.

Trước khi bắt đầu

  • Độ khó: Người mới đến trung cấp (phần Python); Không cần (phần Thunderbit)
  • Thời gian cần thiết: Khoảng 20–60 phút để thiết lập Python và chạy lần thu thập đầu tiên; khoảng 2 phút với Thunderbit
  • Bạn cần: Python 3.9+, trình soạn thảo code, trình duyệt Chrome, và (cho cách không cần code) Thunderbit Chrome Extension

Thiết lập môi trường Python để thu thập Indeed

Trước khi viết bất kỳ code thu thập nào, hãy chuẩn bị môi trường.

Cài đặt các thư viện cần thiết

Tạo môi trường ảo và cài những gói cần dùng:

python -m venv indeed_env
source indeed_env/bin/activate  # Trên Windows: indeed_env\Scripts\activate

# Cho cách HTTP + phân tích
pip install requests beautifulsoup4 lxml httpx

# Cho cách JSON ẩn (khuyến nghị)
pip install curl_cffi parsel tenacity

# Cho cách tự động hóa trình duyệt
pip install selenium

Một vài lưu ý:

  • curl_cffi là lựa chọn mặc định năm 2026 để thu thập các trang được Cloudflare bảo vệ. Nó giả lập fingerprint TLS của trình duyệt thật, điều mà requestshttpx thuần không làm được. Phần chống bot sẽ nói kỹ hơn tại sao điều này quan trọng.
  • Selenium 4.6+ đi kèm Selenium Manager, nên bạn không còn phải tải ChromeDriver thủ công nữa — nó tự quản lý binary của trình duyệt.
  • Dùng lxml làm backend parser cho BeautifulSoup. Nó nhanh hơn khoảng 1,5 lần so với html.parser của thư viện chuẩn.

Tạo cấu trúc dự án

Giữ đơn giản:

indeed_scraper/
├── scraper.py
├── requirements.txt
└── output/

Tất cả ví dụ code bên dưới đều dựa trên scraper.py.

Cách thu thập Indeed bằng Python với BeautifulSoup

Đây là cách thân thiện với người mới: dùng requests để tải trang và BeautifulSoup để parse HTML. Thiết lập nhanh nhất, nhưng cũng dễ hỏng nhất khi dùng với Indeed.

Bước 1: Tạo URL tìm kiếm của Indeed

URL tìm kiếm của Indeed theo một mẫu khá rõ:

https://www.indeed.com/jobs?q=<query>&l=<location>&start=<offset>

Ví dụ, tìm "data analyst" ở "Austin, TX" từ trang đầu tiên:

from urllib.parse import urlencode

params = {
    "q": "data analyst",
    "l": "Austin, TX",
    "start": 0,
}
url = f"https://www.indeed.com/jobs?{urlencode(params)}"
print(url)
# https://www.indeed.com/jobs?q=data+analyst&l=Austin%2C+TX&start=0

Indeed phân trang theo bước 10, với giới hạn cứng 1.000 kết quả (start <= 990). Bất kỳ giá trị offset nào lớn hơn 990 đều lặng lẽ trả về cùng một trang.

Bước 2: Gửi request HTTP với header phù hợp

Indeed chặn ngay các request có user-agent Python mặc định. Bạn cần header trông như trình duyệt thật:

import requests

headers = {
    "User-Agent": (
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
        "(KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36"
    ),
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
    "Accept-Language": "en-US,en;q=0.9",
    "Accept-Encoding": "gzip, deflate, br",
    "Referer": "https://www.indeed.com/",
}

response = requests.get(url, headers=headers, timeout=30)
print(response.status_code)

Nếu bạn nhận 200, hiện tại là ổn. Nếu là 403, nghĩa là Cloudflare đã bắt được bạn. (Phần dưới sẽ nói cách vượt qua.)

Bước 3: Parse danh sách việc làm từ HTML

Dùng BeautifulSoup để chọn các phần tử thẻ công việc. Hãy nhắm vào thuộc tính data-testid — chúng ổn định hơn nhiều so với các tên lớp CSS ngẫu nhiên của Indeed:

from bs4 import BeautifulSoup

soup = BeautifulSoup(response.text, "lxml")
cards = soup.find_all("div", attrs={"data-testid": "slider_item"})

jobs = []
for card in cards:
    title_el = card.find("h2", class_="jobTitle")
    title = title_el.get_text(strip=True) if title_el else None
    company = card.find(attrs={"data-testid": "company-name"})
    location = card.find(attrs={"data-testid": "text-location"})
    link = title_el.find("a")["href"] if title_el and title_el.find("a") else None

    jobs.append({
        "title": title,
        "company": company.get_text(strip=True) if company else None,
        "location": location.get_text(strip=True) if location else None,
        "url": f"https://www.indeed.com{link}" if link else None,
    })

print(f"Tìm thấy {len(jobs)} việc làm")

Bước 4: Xử lý phân trang

Lặp qua các trang bằng cách tăng tham số start:

import time, random

all_jobs = []
for page in range(0, 50, 10):  # 5 trang đầu
    params["start"] = page
    url = f"https://www.indeed.com/jobs?{urlencode(params)}"
    response = requests.get(url, headers=headers, timeout=30)
    # ... parse như trên ...
    all_jobs.extend(jobs)
    time.sleep(random.uniform(3, 6))

Hạn chế của cách này

Nói thẳng luôn: BS4 + Requests là cách yếu nhất để dùng với Indeed năm 2026. requests thuần dùng TLS của thư viện chuẩn Python, tạo ra một JA3 fingerprint mà Cloudflare nhận ra ngay là “không phải trình duyệt”. Nó cũng không hỗ trợ HTTP/2, trong khi Indeed có phục vụ HTTP/2. Bạn rất có thể sẽ bị chặn sau vài trang. Còn CSS selector thì sao? Indeed thay đổi tên lớp như css-1m4cuufjobsearch-JobComponent-embeddedBody-1n0gh5s rất thường xuyên, nên bất kỳ selector nào bám vào chúng đều như quả bom hẹn giờ.

Hãy dùng cách này cho mẫu thử nhanh trên một trang đơn. Nếu cần làm ở quy mô lớn, hãy dùng cách JSON ẩn.

Cách thu thập Indeed bằng Python với dữ liệu JSON ẩn

Đây là phương pháp tôi khuyên cho hầu hết lập trình viên Python. Thay vì parse các phần tử HTML dễ vỡ, bạn trích xuất dữ liệu có cấu trúc từ một biến JavaScript nhúng trong mã nguồn trang Indeed: window.mosaic.providerData["mosaic-provider-jobcards"].

Mọi trường bạn quan tâm — chức danh, công ty, địa điểm, lương, mã việc làm, ngày đăng, cờ làm từ xa — đều đã có sẵn trong blob JSON này. Không cần thực thi JavaScript. Lược đồ đã ổn định ít nhất từ năm 2023, nên bền hơn rất nhiều so với các selector DOM.

Bước 1: Tải HTML của trang

Dùng curl_cffi thay vì requests — nó giả lập fingerprint TLS của trình duyệt thật, rất quan trọng để sống sót trước Cloudflare:

from curl_cffi import requests as cffi_requests

response = cffi_requests.get(
    "https://www.indeed.com/jobs?q=python+developer&l=Remote&start=0",
    impersonate="chrome124",
    headers={
        "Accept-Language": "en-US,en;q=0.9",
        "Referer": "https://www.indeed.com/",
    },
    timeout=30,
)
print(response.status_code, len(response.text))

Tại sao dùng curl_cffi? Đây là binding Python của curl-impersonate, tái tạo đúng ClientHello TLS, HTTP/2 SETTINGS frame và thứ tự header của trình duyệt thật. Nó là client HTTP Python được duy trì tích cực duy nhất có thể vượt qua cả JA3/JA4 lẫn fingerprint Akamai H2 chỉ với một lần gọi. Các mục giả lập được hỗ trợ gồm chrome120, chrome124, chrome131, Safari và các biến thể Edge.

Bước 2: Trích xuất JSON bằng biểu thức chính quy

Blob JSON được nhúng trong một thẻ <script>. Hãy kéo nó ra bằng regex:

import re, json

MOSAIC_RE = re.compile(
    r'window\.mosaic\.providerData\["mosaic-provider-jobcards"\]=(\{.+?\});',
    re.DOTALL,
)

match = MOSAIC_RE.search(response.text)
if match:
    data = json.loads(match.group(1))
    results = data["metaData"]["mosaicProviderJobCardsModel"]["results"]
    print(f"Tìm thấy {len(results)} việc làm trong JSON ẩn")
else:
    print("Không tìm thấy JSON ẩn — có thể bị chặn hoặc trang đã thay đổi")

Bước 3: Parse các trường việc làm từ JSON

Mỗi mục trong results chứa nhiều dữ liệu hơn những gì nhìn thấy trên trang:

jobs = []
for job in results:
    jobs.append({
        "jobkey": job["jobkey"],
        "title": job["title"],
        "company": job.get("company"),
        "location": job.get("formattedLocation"),
        "remote": job.get("remoteLocation"),
        "salary": (job.get("salarySnippet") or {}).get("text"),
        "posted": job.get("formattedRelativeTime"),
        "job_type": job.get("jobTypes"),
        "easy_apply": job.get("indeedApplyEnabled"),
        "url": f"https://www.indeed.com/viewjob?jk={job['jobkey']}",
    })

JSON này thường có cả ước tính lương, thuộc tính phân loại (tag kỹ năng) và đánh giá công ty — những thứ không phải lúc nào cũng hiện ra trong HTML render.

Bước 4: Thu thập nhiều trang

Dùng tierSummaries trong JSON để hiểu tổng số kết quả, rồi lặp:

import time, random

all_jobs = []
for start in range(0, 50, 10):  # 5 trang đầu
    url = f"https://www.indeed.com/jobs?q=python+developer&l=Remote&start={start}&sort=date"
    response = cffi_requests.get(
        url,
        impersonate="chrome124",
        headers={"Accept-Language": "en-US,en;q=0.9", "Referer": "https://www.indeed.com/"},
        timeout=30,
    )
    match = MOSAIC_RE.search(response.text)
    if match:
        data = json.loads(match.group(1))
        results = data["metaData"]["mosaicProviderJobCardsModel"]["results"]
        all_jobs.extend([{
            "jobkey": j["jobkey"],
            "title": j["title"],
            "company": j.get("company"),
            "location": j.get("formattedLocation"),
            "salary": (j.get("salarySnippet") or {}).get("text"),
            "url": f"https://www.indeed.com/viewjob?jk={j['jobkey']}",
        } for j in results])
    time.sleep(random.uniform(3, 7))

print(f"Tổng cộng: đã thu thập {len(all_jobs)} việc làm")

Vì sao JSON ẩn bền hơn

Cấu trúc window.mosaic.providerData thay đổi ít thường xuyên hơn tên lớp CSS. Bạn nhận được dữ liệu sạch, có cấu trúc mà không phải parse HTML lộn xộn. Dù vậy, bạn vẫn cần giảm thiểu chống bot (header, độ trễ, proxy) — phần đó sẽ nói tiếp ngay sau đây.

Cách thu thập Indeed bằng Python với Selenium

Selenium là cách tự động hóa trình duyệt. Nó hữu ích khi bạn cần tương tác với trang — nhấp vào panel chi tiết việc làm, xử lý nội dung yêu cầu đăng nhập, hoặc scrape mô tả được tải động mà chưa có trong HTML ban đầu.

Khi nào nên dùng Selenium thay vì HTTP client

  • Indeed tải một số nội dung động (mô tả công việc đầy đủ ở panel bên phải)
  • Bạn cần scrape các trang yêu cầu trạng thái phiên hoặc đăng nhập
  • Bạn làm quy mô nhỏ, tốc độ không phải ưu tiên

Đi nhanh qua ví dụ

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.options import Options
import time

options = Options()
options.add_argument("--disable-blink-features=AutomationControlled")
# options.add_argument("--headless=new")  # Chạy headless dễ bị phát hiện hơn — dùng thận trọng

driver = webdriver.Chrome(options=options)
driver.get("https://www.indeed.com/jobs?q=data+engineer&l=New+York")
time.sleep(3)

cards = driver.find_elements(By.CSS_SELECTOR, "[data-testid='slider_item']")
for card in cards:
    try:
        title = card.find_element(By.CSS_SELECTOR, "h2.jobTitle").text
        company = card.find_element(By.CSS_SELECTOR, "[data-testid='company-name']").text
        location = card.find_element(By.CSS_SELECTOR, "[data-testid='text-location']").text
        print(f"{title} | {company} | {location}")
    except Exception:
        continue

driver.quit()

Hạn chế

Selenium chậm — mỗi trang cần render đầy đủ bằng trình duyệt. Chrome headless có thể bị hệ thống chống bot của Indeed phát hiện (Cloudflare kiểm tra navigator.webdriver, chuỗi vendor của WebGL, số lượng plugin, v.v.). Ngay cả undetected-chromedriver cũng chỉ trì hoãn việc bị phát hiện; nó không ngăn được vĩnh viễn. Và giống BS4, selector của bạn sẽ hỏng khi Indeed cập nhật giao diện.

Với hầu hết trường hợp, cách JSON ẩn cho bạn cùng dữ liệu nhưng nhanh hơn và ít phải bảo trì hơn. Hãy giữ Selenium cho những tình huống thật sự cần trình duyệt.

Cách tránh lỗi 403 khi thu thập Indeed bằng Python

Đây là phần quan trọng nhất. Nếu bạn đến đây sau một lần tìm kiếm Google đầy bực bội, thì bạn đang đúng chỗ.

indeed_antibot_374d080ff4.png

Vì sao Indeed chặn scraper của bạn

Indeed dùng Cloudflare Bot Management kèm Cloudflare Turnstile — không phải DataDome, không phải PerimeterX. Header phản hồi chứng minh điều đó: server: cloudflare, cf-ray, và cookie quản lý bot __cf_bm. Cloudflare kiểm tra fingerprint TLS (JA3/JA4), thứ tự header HTTP/2, kiểu request, và tín hiệu hành vi trình duyệt. Nếu bất kỳ thứ gì trông không giống người dùng thật, bạn sẽ nhận 403, 429, 503, hoặc — trường hợp tinh quái nhất — mã 200 OK nhưng lại là một trang thử thách Turnstile thay vì dữ liệu việc làm thật.

Xoay vòng User-Agent và header request

Một User-Agent cố định là cách nhanh nhất để bị chặn. Hãy xoay vòng từ một danh sách các chuỗi hiện đại, hợp lý. Lưu ý: các trường minor version của Chrome đã bị đóng băng ở 0.0.0 kể từ khi User-Agent Reduction — đừng bịa ra minor version khác 0, nếu không hệ thống chống bot sẽ phát hiện.

import random

USER_AGENTS = [
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
    "(KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 "
    "(KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36",
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
    "(KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36 Edg/145.0.3800.97",
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 "
    "(KHTML, like Gecko) Version/17.4 Safari/605.1.15",
]

headers = {
    "User-Agent": random.choice(USER_AGENTS),
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
    "Accept-Language": "en-US,en;q=0.9",
    "Accept-Encoding": "gzip, deflate, br, zstd",
    "Referer": "https://www.indeed.com/",
    "Sec-Fetch-Dest": "document",
    "Sec-Fetch-Mode": "navigate",
    "Sec-Fetch-Site": "same-origin",
}

Cũng hãy đảm bảo sec-ch-ua Client Hints khớp với phiên bản UA. Một sec-ch-ua: "Chrome";v="131" đi cùng User-Agent khai báo Chrome 145 là dấu hiệu quá rõ.

Thêm độ trễ ngẫu nhiên giữa các request

Khoảng cách cố định dễ bị nhận ra bởi cơ chế phát hiện mẫu. Hãy thêm jitter ngẫu nhiên:

import time, random

# Giữa mỗi request
time.sleep(random.uniform(3, 6))

# Khi retry sau khi bị chặn
def backoff_sleep(attempt):
    base = 4
    sleep_time = base * (2 ** attempt) + random.uniform(0, 2)
    time.sleep(min(sleep_time, 60))

Kinh nghiệm tổng hợp từ ScrapeOpsWebScraping.AI là chờ 3–6 giây giữa các request trên mỗi IP, với mức trần cứng khoảng 100 request/IP mỗi phiên trước khi xoay IP.

Dùng xoay proxy

Đây là yếu tố quyết định thành bại lớn nhất. Proxy datacenter từ dải AWS/GCP chỉ đạt khoảng 5–15% thành công trên các mục tiêu Cloudflare Enterprise — gần như vô dụng với Indeed. Proxy dân cư cộng với fingerprint TLS đúng cách có thể nâng lên 80–95%.

PROXIES = [
    "http://user:pass@us.residential.example:7777",
    "http://user:pass@us.residential.example:7778",
    "http://user:pass@us.residential.example:7779",
]

proxy = random.choice(PROXIES)
response = cffi_requests.get(
    url,
    impersonate="chrome124",
    headers=headers,
    proxies={"https": proxy},
    timeout=30,
)

Giá proxy dân cư năm 2026 vào khoảng 4–8,50 USD/GB tùy nhà cung cấp và mức cam kết. Riêng với Indeed, hãy bắt đầu bằng một pool nhỏ rồi mở rộng khi cần.

Xử lý mã trạng thái 403, 429 và 503 một cách khéo léo

Đừng chỉ retry mù quáng. Mỗi mã trạng thái mang ý nghĩa khác nhau:

def fetch_with_retry(url, proxy_pool, max_retries=5):
    for attempt in range(max_retries):
        proxy = random.choice(proxy_pool)
        headers["User-Agent"] = random.choice(USER_AGENTS)
        try:
            r = cffi_requests.get(
                url,
                impersonate=random.choice(["chrome124", "chrome120", "edge101"]),
                headers=headers,
                proxies={"https": proxy},
                timeout=30,
            )
            # Kiểm tra trường hợp tinh vi "200 nhưng vẫn có challenge"
            if r.status_code == 200 and "cf-turnstile" not in r.text and "Just a moment" not in r.text:
                return r

            if r.status_code == 403:
                print(f"403 — bị chặn. Đang xoay proxy, lần thử {attempt + 1}")
            elif r.status_code == 429:
                print(f"429 — bị giới hạn tốc độ. Giảm nhịp lại.")
            elif r.status_code == 503:
                print(f"503 — máy chủ quá tải hoặc đang có thử thách JS.")

            backoff_sleep(attempt)
        except Exception as e:
            print(f"Lỗi request: {e}")
            backoff_sleep(attempt)

    raise RuntimeError(f"Thất bại sau {max_retries} lần thử: {url}")

Trường hợp 200-with-challenge là khó chịu nhất. Luôn quét body phản hồi để tìm cf-turnstile hoặc Just a moment trước khi coi 200 là thành công.

Giải pháp dễ hơn: để Thunderbit xử lý chống bot cho bạn

Với người dùng không muốn tự xây và duy trì pool proxy, xoay header và giả lập fingerprint TLS, tính năng thu thập trên cloud của Thunderbit sẽ tự xử lý CAPTCHA, xoay proxy và các lớp chống bot. Không cần thiết lập proxy, không cần cấu hình curl_cffi, không cần thư viện giải CAPTCHA. Đây là con đường ít trở lực nhất khi bạn chỉ cần dữ liệu.

Vì sao scraper Indeed của bạn cứ hỏng liên tục (và cách sửa)

Bức tường 403 là nỗi đau cấp tính. Nỗi đau mạn tính là bảo trì — scraper chạy hôm nay có thể hỏng tuần sau, âm thầm trả về dữ liệu rỗng hoặc kết quả cũ.

Indeed phá selector của bạn như thế nào

Indeed xoay vòng tên lớp CSS rất gắt. Hướng dẫn của Bright Data cảnh báo rõ rằng các lớp như css-1m4cuufcss-1rqpxry “có vẻ được tạo ngẫu nhiên — có lẽ ngay lúc build.” A/B testing khiến các phiên khác nhau thấy các bố cục khác nhau. Và việc tái cấu trúc DOM diễn ra mà không báo trước.

Câu chuyện JobFunnel rất đáng chú ý. Một cộng tác viên báo cáo: "CaptchaBuster has successfully mitigated the captcha, and the reason for still unsuccessfully scraping the page [is] due to outdated beautiful soup selectors." Tức là scraper không bị chặn — nó chỉ đang parse nhầm phần tử.

Chiến lược: ưu tiên JSON ẩn thay vì parse DOM

Blob window.mosaic.providerData đã ổn định lược đồ từ ít nhất năm 2023. Đường dẫn metaData.mosaicProviderJobCardsModel.results[] vẫn là đường chuẩn năm 2026. Selector DOM hỏng theo tháng. Trích xuất JSON thì nếu có hỏng cũng thường theo năm, chứ không phải theo tuần.

Chiến lược: dùng thuộc tính dữ liệu thay vì tên lớp

Khi buộc phải chạm vào DOM, hãy nhắm vào thuộc tính chức năng:

SelectorMục đích
[data-testid="slider_item"]Mỗi khung tin tuyển dụng
[data-testid="job-title"] hoặc h2.jobTitle > aLiên kết chức danh việc làm
[data-testid="company-name"]Tên nhà tuyển dụng
[data-testid="text-location"]Văn bản địa điểm
data-jk="<jobkey>" trên mỗi thẻMóc ổn định nhất — không đổi từ 2019

Thêm kiểm tra assertion để phát hiện selector lỗi thời

Đừng bao giờ để scraper chạy âm thầm với kết quả bằng 0. Hãy thêm một bước kiểm tra sau mỗi lần lấy dữ liệu:

results = parse_hidden_json(html)
assert len(results) > 0, (
    f"Indeed trả về tập kết quả rỗng tại start={start} — "
    "có thể bị chặn, CAPTCHA hoặc selector đã lệch. "
    f"500 ký tự đầu của phản hồi: {html[:500]}"
)

Ghi log 500–2000 ký tự đầu của phản hồi thô khi lỗi xảy ra. Như vậy bạn sẽ biết ngay đó là challenge Turnstile, tường đăng nhập hay thay đổi lược đồ. Hãy chạy một bài kiểm tra nhanh hằng ngày ở mức CI với một truy vấn cố định (ví dụ q=python&l=remote) và đảm bảo số kết quả khác 0.

Giải pháp AI: scraper không bao giờ hỏng

AI của Thunderbit đọc cấu trúc trang mới mỗi lần — nó không phụ thuộc vào selector cứng hay mẫu regex cố định. Khi Indeed thay đổi HTML, Thunderbit tự thích ứng. Điều này trực tiếp giải quyết gánh nặng bảo trì mà người dùng trên các diễn đàn luôn xem là phiền nhất. Nếu bạn từng thức dậy và nhận tin nhắn Slack: “scraper lại đang trả về dòng rỗng”, bạn sẽ hiểu giá trị của việc không phải tự sửa nó.

Thu thập Indeed mà không cần viết Python: lựa chọn không cần code

Hầu hết các hướng dẫn cạnh tranh đều mặc định rằng bạn sẽ viết code Python. Nhưng dữ liệu trên diễn đàn lại kể một câu chuyện khác. Người dùng nói những câu như "it's just so difficult with constant bugs and errors" và có người còn gợi ý thuê ai đó trên Fiverr chỉ để lấy dữ liệu. Nếu nghe giống bạn, phần này là đường thoát.

Cách thu thập Indeed với Thunderbit (từng bước)

Bước 1: Cài Thunderbit Chrome Extension từ Chrome Web Store. Bạn có thể dùng miễn phí để bắt đầu.

Bước 2: Mở một trang kết quả tìm kiếm của Indeed trên trình duyệt — ví dụ https://www.indeed.com/jobs?q=data+analyst&l=Austin%2C+TX.

Bước 3: Nhấp vào biểu tượng Thunderbit trên thanh công cụ trình duyệt, rồi nhấp "AI Suggest Fields." AI của Thunderbit sẽ quét trang và tự phát hiện các cột như Chức danh, Công ty, Địa điểm, Mức lương, URL việc làm và Ngày đăng. Bạn có thể xem lại và chỉnh các trường được gợi ý — xóa cột không cần, hoặc thêm cột tùy chỉnh bằng cách mô tả điều bạn muốn bằng tiếng Anh tự nhiên.

Bước 4: Nhấp "Scrape." Thunderbit sẽ trích xuất dữ liệu từ trang và hiển thị dưới dạng bảng có cấu trúc. Bạn sẽ thấy các dòng tin tuyển dụng cùng những trường bạn đã cấu hình.

Làm giàu dữ liệu bằng thu thập trang con

Sau khi thu thập trang danh sách, hãy nhấp "Scrape Subpages" để Thunderbit ghé từng trang chi tiết công việc. Nó sẽ lấy đầy đủ mô tả, yêu cầu, phúc lợi và liên kết ứng tuyển — không cần thiết lập thêm. Đây tương đương với việc viết một scraper Python thứ hai để truy cập từng URL /viewjob?jk=<jobkey>, chỉ khác là làm bằng một cú nhấp.

Xử lý phân trang tự động

Thunderbit tự động xử lý phân trang dựa trên cú nhấp của Indeed. Không cần tự ghép URL offset hay viết vòng lặp phân trang. Nó sẽ đi qua các trang và gom kết quả lại.

Xuất sang công cụ bạn thích

Xuất dữ liệu đã thu thập sang CSV, Excel, Google Sheets, Airtable hoặc Notion — hoàn toàn miễn phí. Không cần viết code csv.writer() hay pandas.to_csv().

Khi nào nên dùng Python vs. Thunderbit

Tình huốngCông cụ phù hợp nhất
Pipeline dữ liệu tùy chỉnh, tự động hóa theo lịch bằng cron/AirflowPython
Tích hợp vào một codebase lớn hơnPython
Logic phân tích quá tùy biếnPython
Nghiên cứu một lần hoặc phân tích thị trườngThunderbit
Thành viên không kỹ thuật cần dữ liệuThunderbit
Cần dữ liệu ngay, không muốn gỡ lỗi 403Thunderbit
Làm giàu trang con mà không cần thiết lậpThunderbit

So sánh thời gian: thiết lập Python + gỡ lỗi chống bot = vài giờ đến vài ngày (đặc biệt là lần đầu). Thunderbit = chưa tới 2 phút cho cùng dữ liệu. Tôi không nói Python sai — tôi nói nó còn tùy bạn cần gì.

Thu thập Indeed có hợp pháp không? Những điều bạn cần biết

Không có hướng dẫn thu thập Indeed nào đứng top mà đề cập đến tính hợp pháp, điều này khá bất ngờ vì câu hỏi "scraping Indeed legal?" xuất hiện rất nhiều trên diễn đàn. Đây không phải là tư vấn pháp lý, nhưng đây là bức tranh tổng quan.

Điều khoản sử dụng của Indeed

Điều khoản của Indeed (indeed.com/legal) không có một câu cấm “không được scrape” bao trùm mọi trường hợp. Lệnh cấm tự động hóa rõ ràng duy nhất là ở Mục A.3.5, cấm "use of any automation, scripting, or bots to automate the Indeed Apply process." Phạm vi này hẹp, chỉ áp dụng cho luồng Apply, không phải việc đọc thụ động các tin tuyển dụng công khai. Công cụ thực thi chính của Indeed là kỹ thuật — thử thách Cloudflare, chặn IP, device fingerprinting — chứ không phải tòa án.

Tiền lệ pháp lý liên quan

Vụ án Mỹ được nhắc đến nhiều nhất là hiQ Labs v. LinkedIn. Tòa phúc thẩm khu vực 9 phán quyết vào tháng 4 năm 2022 rằng việc scrape dữ liệu công khai “nhiều khả năng không vi phạm CFAA” (Computer Fraud and Abuse Act). Tuy nhiên, sau đó hiQ lại bị xác định là vi phạm hợp đồng vì nhân viên của họ đã tạo hồ sơ LinkedIn giả và chấp nhận điều khoản dịch vụ.

Gần đây hơn, Meta v. Bright Data (Tòa Bắc California, tháng 1 năm 2024) đưa ra phán quyết còn rõ hơn. Thẩm phán Chen kết luận rằng điều khoản của Facebook và Instagram “không cấm việc scrape dữ liệu công khai khi đăng xuất.” Meta sau đó đã tự nguyện rút các yêu cầu còn lại vào tháng sau.

robots.txt của Indeed

robots.txt của Indeed nhìn chung không cho phép /jobs//job/ với User-agent: * mặc định, nhưng lại cho Googlebot và Bingbot truy cập /viewjob? — tức các trang chi tiết việc làm riêng lẻ. Các crawler huấn luyện AI (GPTBot, CCBot, anthropic-ai) bị hạn chế mạnh. robots.txt không có giá trị ràng buộc pháp lý ở Mỹ, nhưng tôn trọng nó là một thực hành tốt và là bằng chứng của thiện chí.

Nguyên tắc thực tế để scrape có trách nhiệm

  • Chỉ thu thập dữ liệu công khai — không đăng nhập, không tạo tài khoản giả
  • Tôn trọng giới hạn tốc độ: 1 request mỗi 3–6 giây trên mỗi IP, song song ở mức thấp
  • Đừng đăng lại dữ liệu đã thu thập như thể đó là job board của riêng bạn
  • Dùng dữ liệu cho nghiên cứu cá nhân hoặc nội bộ, không bán lại thương mại khi chưa có phép
  • Loại bỏ hoặc băm PII không cần thiết; đặt ngưỡng lưu trữ cho dữ liệu có liên quan đến cá nhân
  • Nếu vận hành ở quy mô lớn hoặc ở EU/UK, hãy hỏi luật sư — nghĩa vụ minh bạch theo Điều 14 của GDPR áp dụng với dữ liệu cá nhân được thu thập

Mức rủi ro: tự động hóa tìm việc cá nhân nằm ở mức thấp. Bán lại dữ liệu Indeed thương mại ở quy mô lớn nằm ở mức cao.

Kết luận và ý chính

Thu thập Indeed bằng Python là làm được, nhưng không phải dự án cuối tuần kiểu “thiết lập rồi quên”. Cloudflare của Indeed, selector thay đổi liên tục, và các biện pháp chống bot mạnh tay nghĩa là bạn cần tiếp cận việc này bằng đúng công cụ và đúng kỳ vọng.

Đây là những điều tôi rút ra:

  • Indeed là nguồn dữ liệu thị trường việc làm phong phú nhất trên web — 350 triệu lượt truy cập mỗi tháng, 130 triệu tin — nhưng nó chống scraper rất mạnh.
  • Trích xuất JSON ẩn (window.mosaic.providerData) là cách Python bền nhất. Lược đồ đã ổn định nhiều năm, trong khi CSS selector hỏng hằng tháng.
  • curl_cffi với giả lập trình duyệt là HTTP client mặc định của năm 2026 cho các trang được Cloudflare bảo vệ. requestshttpx thuần bị chặn chỉ vì fingerprint TLS.
  • Luôn dùng header xoay vòng, độ trễ ngẫu nhiên và proxy dân cư để tránh lỗi 403. Proxy datacenter gần như vô dụng trước Cloudflare Enterprise.
  • Thêm kiểm tra assertion để biết ngay khi selector hỏng hoặc khi bạn nhận trang challenge thay vì dữ liệu việc làm.
  • Với người không kỹ thuật hoặc ai chỉ muốn có kết quả nhanh, Thunderbit cung cấp một con đường không cần code, được AI hỗ trợ, tự thích ứng với thay đổi của trang — không proxy, không gỡ lỗi, không bảo trì.

Nếu bạn muốn thử theo hướng không cần code, Thunderbit có gói miễn phí để bạn test trên Indeed mà không cần cam kết gì. Còn nếu bạn đi theo hướng Python, các ví dụ code ở trên là điểm khởi đầu khá tốt — chỉ cần nhớ xem khả năng chống bot là yêu cầu hàng đầu, không phải chuyện để làm sau.

Muốn tìm hiểu thêm về các cách và công cụ web scraping, hãy xem các hướng dẫn của chúng tôi về cách web scrape bằng Python, các công cụ web scraping tự động tốt nhất, và web scraping mà không bị chặn. Bạn cũng có thể xem video hướng dẫn trên kênh YouTube của Thunderbit.

Dùng Thunderbit để thu thập dữ liệu Indeed nhanh hơn Get Started Free

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

Thư viện Python nào tốt nhất để thu thập Indeed?

Với request HTTP, curl_cffi là lựa chọn mạnh nhất năm 2026 — nó giả lập fingerprint TLS của trình duyệt thật, rất cần để vượt qua Cloudflare. httpx với HTTP/2 là phương án dự phòng khá ổn cho mục tiêu ít được bảo vệ hơn. Với phân tích HTML, BeautifulSoup4 kết hợp lxml vẫn là tiêu chuẩn. Với tự động hóa trình duyệt, Playwright (kèm playwright-stealth) hoặc undetected-chromedriver đều hoạt động, dù cả hai ngày càng dễ bị phát hiện. Cách dùng regex trên JSON ẩn (window.mosaic.providerData) giúp bỏ qua phần parse nặng nề.

Vì sao tôi cứ bị lỗi 403 khi thu thập Indeed?

Indeed dùng Cloudflare Bot Management, kiểm tra fingerprint TLS (JA3/JA4), thứ tự header HTTP/2, kiểu request và hành vi trình duyệt. Nếu bạn dùng requests thuần, fingerprint TLS sẽ tố ngay bạn là một script Python — lỗi 403 đến trước cả khi header của bạn được đọc. Hãy sửa bằng cách chuyển sang curl_cffi với giả lập trình duyệt, xoay vòng User-Agent hợp lý, thêm độ trễ ngẫu nhiên (3–6 giây), và dùng proxy dân cư. Cũng nên kiểm tra trường hợp “200 nhưng có challenge” — quét body phản hồi để tìm dấu cf-turnstile.

Tôi có thể thu thập Indeed mà không cần code không?

Có. Các công cụ như Thunderbit cho phép bạn trích xuất tin tuyển dụng Indeed chỉ trong vài cú nhấp — cài extension Chrome, mở trang tìm kiếm Indeed, nhấp “AI Suggest Fields”, rồi nhấp “Scrape.” AI của Thunderbit tự nhận diện các trường như chức danh, công ty, địa điểm và mức lương. Nó tự xử lý phân trang, làm giàu trang con (mô tả công việc đầy đủ) và các biện pháp chống bot. Xuất ra CSV, Google Sheets, Airtable hoặc Notion miễn phí.

Indeed thay đổi cấu trúc HTML thường xuyên thế nào?

Indeed thường xuyên xoay vòng tên lớp CSS (ví dụ css-1m4cuuf, các chuỗi hash ngẫu nhiên) và tái cấu trúc phần tử DOM mà không báo trước. A/B testing khiến các người dùng khác nhau có thể thấy bố cục khác nhau cùng lúc. Cách JSON ẩn (window.mosaic.providerData) ổn định hơn đáng kể — lược đồ đã giữ nguyên ít nhất từ năm 2023. Khi buộc phải dùng selector DOM, hãy nhắm vào thuộc tính data-testiddata-jk (job key) thay vì tên lớp CSS.

Thu thập Indeed có hợp pháp không?

Theo phán quyết hiQ v. LinkedIn của tòa khu vực 9 (2022) và quyết định Meta v. Bright Data (2024), việc scrape các URL việc làm công khai khi đã đăng xuất ở Mỹ khó có khả năng tạo ra trách nhiệm theo CFAA. Điều khoản của Indeed chỉ cấm tự động hóa quy trình Apply, không cấm việc đọc thụ động các tin công khai. Dù vậy, hãy luôn thu thập có trách nhiệm: không đăng nhập, không tạo tài khoản giả, tôn trọng giới hạn tốc độ, không đăng lại dữ liệu như job board của riêng bạn, và xử lý cẩn thận mọi dữ liệu cá nhân (tên nhà tuyển dụng, email) theo GDPR/CCPA. Với hoạt động thương mại quy mô lớn, hãy hỏi luật sư.

Tìm hiểu thêm

Fawad Khan
Fawad Khan
Fawad sống bằng nghề viết, và nói thật là anh ấy khá yêu công việc này. Anh đã dành nhiều năm để tìm hiểu điều gì khiến một câu chữ in đậm trong tâm trí người đọc — và điều gì khiến họ lướt qua. Hỏi anh về marketing, anh có thể nói hàng giờ. Hỏi anh về carbonara, anh sẽ nói còn lâu hơn.
Mục lục

Cào một trang web chỉ bằng cách hỏi

Nói bạn cần gì bằng tiếng Anh đơn giản. Hoặc tốt hơn, không cần nói gì cả.

Dùng Thunderbit ngay miễn phí
Trích xuất dữ liệu bằng AI
Dễ dàng chuyển dữ liệu sang Google Sheets, Airtable hoặc Notion
Chrome Store Rating
PRODUCT HUNT#1 Product of the Week