如何使用 Python 抓取 Indeed(并避开 403 错误)

最后更新于 April 16, 2026

大概在我第 50 次把 Indeed 上的职位名称复制粘贴到表格里时,我开始怀疑自己的职业选择了。如果你也试过用程序从 Indeed 提取结构化数据,你大概已经知道答案:403 错误不是 bug,而是 Indeed 防御系统的功能之一。

Indeed 是全球最大的求职网站,月独立访问量约 ,随时拥有约 ,业务覆盖 。这让它成为全球最有价值的招聘市场数据来源之一——同时也是最难抓取的网站之一。开源爬虫 JobFunnel(GitHub 上有数千星标)在和反爬系统长期拉扯后,甚至在 2025 年 12 月被维护者本人 了。维护者自己都说:“所有用户都能抓到一些职位,但很快就会碰到验证码,随后抓取失败,最终拿不到任何职位数据。” 还有贡献者反馈,第一次请求就触发了 CAPTCHA 。所以没错——这绝不是一个轻松的抓取目标。在这篇指南里,我会带你走一遍用 Python 抓取 Indeed 的所有实用方法,教你怎么真正扛住 403 这一关;如果你想直接跳过调试,我也会演示一种使用 的无代码替代方案。

用 Python 抓取 Indeed 到底是什么意思?

网页抓取,本质上就是自动从网页里提取结构化数据。当我们说“用 Python 抓取 Indeed”时,指的是写一个脚本去访问 Indeed 的搜索结果页和职位详情页,读取底层 HTML(或内嵌数据),再把职位名称、公司、地点、薪资、描述等字段整理成可用格式——比如 CSV、数据库或 Google 表格。

常见的 Python 工具包括 Requests(发 HTTP 请求)、BeautifulSoup(解析 HTML),以及 Selenium 或 Playwright(浏览器自动化)。但 Indeed 不是一个简单的静态网站,它是一个混合型站点:既有服务端渲染的 HTML,也嵌入了 JSON 状态数据,还前置了 Cloudflare Bot Management。也就是说,在你解析任何职位名称之前,脚本就得先处理 JavaScript 渲染内容、不断变化的 CSS 类名,以及高强度的反爬机制。

而且到了 2026 年,也没有官方、免费的、只读 Indeed API 可用。以前的 Publisher Jobs API 大约在 2020 年前后就被弃用了,剩下的只有面向雇主的接口(Job Sync、Sponsored Jobs)。所以现实里可行的选择,基本只剩下抓取网页或付费购买第三方数据服务。

为什么要抓取 Indeed 的职位数据?

抓取 Indeed 的商业价值其实很直接:手动浏览成千上万条招聘信息既不现实,而这些信息本身又很有价值。

indeed_stats_dca2a43cec.png

应用场景受益对象示例
潜在客户开发销售与招聘团队汇总正在招聘的公司名单并附上联系方式
招聘市场研究分析师、人力资源团队找出热门技能、按地区对比薪资基准
竞争情报雇主、猎头公司监测竞争对手的招聘动向和薪资水平
个人求职自动化求职者汇总符合条件、分布在不同地区的职位
机器学习训练数据数据科学家基于历史职位数据构建薪资预测模型

Indeed Hiring Lab 自身的研究也 招聘发布数据与美国劳工统计局 BLS 的 JOLTS 数据高度相关,可作为美国劳动力市场的近实时代理指标。对冲基金会把职位发布速度当作另类数据信号。人力资源团队会用抓取到的薪资区间做薪酬对标。招聘人员则会根据正在招人的公司整理潜在客户名单。

不过有一点要注意:Indeed 上的薪资数据虽然在改善,但仍然不完整。截至 2025 年年中,大约 包含薪资信息,但只有约 给出精确数值,其余大多只是区间。凡是基于 Indeed 数据做薪资分析,都要把这种缺失情况考虑进去。

选择适合你的 Indeed Python 抓取方式

抓取 Indeed 没有唯一“正确”的方法。最佳方案取决于你的技术水平、需要的数据量,以及你愿意投入多少维护成本。我测试过四种主流方式,下面是它们的对比:

对比维度BS4 + RequestsSelenium隐藏 JSON(window.mosaic无代码(Thunderbit)
难度初学者中级中级到进阶无(2 次点击)
速度慢(需要浏览器渲染)快(云端抓取)
JS 渲染内容是(内嵌数据)
反爬抗性中等(可被检测)中到高高(自动处理)
HTML 变化后的维护成本高(选择器容易失效)中(JSON 结构更稳定)无(AI 自动适配)
最适合快速原型动态页面、登录后页面批量结构化数据非开发者、快速出结果

这篇指南会逐一讲解这些方法。如果你是 Python 开发者,建议重点看 BS4、隐藏 JSON 和 Selenium 三部分。如果你不是程序员,或者已经被 403 搞烦了,直接跳到 Thunderbit 部分就行。

开始前需要准备什么

  • 难度: 初学者到中级(Python 部分);无(Thunderbit 部分)
  • 所需时间: Python 环境配置和第一次抓取约 20–60 分钟;使用 Thunderbit 约 2 分钟
  • 你需要: Python 3.9+、代码编辑器、Chrome 浏览器,以及(无代码方案)

为 Indeed 抓取配置 Python 环境

在写任何抓取代码之前,先把环境准备好。

安装所需库

创建虚拟环境并安装需要的包:

1python -m venv indeed_env
2source indeed_env/bin/activate  # Windows 上:indeed_env\Scripts\activate
3# 用于 HTTP + 解析的方案
4pip install requests beautifulsoup4 lxml httpx
5# 用于隐藏 JSON 的方案(推荐)
6pip install curl_cffi parsel tenacity
7# 用于浏览器自动化的方案
8pip install selenium

几点说明:

  • curl_cffi 是 2026 年抓取 Cloudflare 保护网站时的默认选择。它能伪装成真实浏览器的 TLS 指纹,而普通的 requestshttpx 做不到。后面反爬部分会详细解释为什么这很重要。
  • Selenium 4.6+ 自带 Selenium Manager,所以你不再需要手动下载 ChromeDriver,它会自动管理浏览器二进制文件。
  • BeautifulSoup 建议配合 lxml 作为解析后端。它的速度大约比标准库的 html.parser

创建项目结构

保持简单即可:

1indeed_scraper/
2├── scraper.py
3├── requirements.txt
4└── output/

下面所有代码示例都以 scraper.py 为基础。

如何使用 BeautifulSoup 抓取 Indeed

这是最适合初学者的方法:用 requests 拉取页面,再用 BeautifulSoup 解析 HTML。上手最快,但在 Indeed 上也最脆弱。

第 1 步:构造 Indeed 搜索 URL

Indeed 的搜索 URL 结构很固定:

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

比如搜索 Austin, TX 地区的“data analyst”,并从第一页开始:

1from urllib.parse import urlencode
2params = {
3    "q": "data analyst",
4    "l": "Austin, TX",
5    "start": 0,
6}
7url = f"https://www.indeed.com/jobs?{urlencode(params)}"
8print(url)
9# https://www.indeed.com/jobs?q=data+analyst&l=Austin%2C+TX&start=0

Indeed 的分页步长是 10,最多只给到 1,000 条结果(start &lt;= 990)。超过 990 的偏移量通常会静默返回同一页。

第 2 步:带上合适的请求头发送 HTTP 请求

如果你直接用 Python 默认的 user-agent,Indeed 基本会立刻拦截。你需要更像真实浏览器的请求头:

1import requests
2headers = {
3    "User-Agent": (
4        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
5        "(KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36"
6    ),
7    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
8    "Accept-Language": "en-US,en;q=0.9",
9    "Accept-Encoding": "gzip, deflate, br",
10    "Referer": "https://www.indeed.com/",
11}
12response = requests.get(url, headers=headers, timeout=30)
13print(response.status_code)

如果返回 200,说明暂时通过了。如果返回 403,那就是 Cloudflare 已经识别出你了。(后面会讲怎么尽量绕过。)

第 3 步:从 HTML 中解析职位列表

使用 BeautifulSoup 选择职位卡片元素。优先用 data-testid 属性,它比 Indeed 的随机 CSS 类名稳定得多:

1from bs4 import BeautifulSoup
2soup = BeautifulSoup(response.text, "lxml")
3cards = soup.find_all("div", attrs={"data-testid": "slider_item"})
4jobs = []
5for card in cards:
6    title_el = card.find("h2", class_="jobTitle")
7    title = title_el.get_text(strip=True) if title_el else None
8    company = card.find(attrs={"data-testid": "company-name"})
9    location = card.find(attrs={"data-testid": "text-location"})
10    link = title_el.find("a")["href"] if title_el and title_el.find("a") else None
11    jobs.append({
12        "title": title,
13        "company": company.get_text(strip=True) if company else None,
14        "location": location.get_text(strip=True) if location else None,
15        "url": f"https://www.indeed.com{link}" if link else None,
16    })
17print(f"Found {len(jobs)} jobs")

第 4 步:处理分页

通过递增 start 参数遍历页面:

1import time, random
2all_jobs = []
3for page in range(0, 50, 10):  # 前 5 页
4    params["start"] = page
5    url = f"https://www.indeed.com/jobs?{urlencode(params)}"
6    response = requests.get(url, headers=headers, timeout=30)
7    # ... 按上面的方式解析 ...
8    all_jobs.extend(jobs)
9    time.sleep(random.uniform(3, 6))

这种方法的局限

我先说结论:到了 2026 年,BS4 + Requests 是抓 Indeed 最弱的一种方式。普通 requests 依赖 Python 标准库的 TLS 实现,会产生 Cloudflare 一眼就能识别的 ——它会直接把你当成“不是浏览器”。而且它也不支持 Indeed 所使用的 HTTP/2。结果很可能是,抓了没几页就被封。至于 CSS 选择器,Indeed 会频繁轮换 css-1m4cuufjobsearch-JobComponent-embeddedBody-1n0gh5s 这类类名 ,所以任何依赖这些类名的选择器都像定时炸弹。

这个方法适合单页快速原型验证。要是想大规模抓取,还是推荐隐藏 JSON 方案。

如何使用隐藏 JSON 数据抓取 Indeed

这是我最推荐给大多数 Python 开发者的方法。它不去解析脆弱的 HTML 元素,而是直接提取 Indeed 页面源码里嵌入的 JavaScript 变量:window.mosaic.providerData["mosaic-provider-jobcards"]

你关心的每个字段——职位名称、公司、地点、薪资、job key、发布时间、是否远程——都已经在这个 JSON 里了,不需要执行 JavaScript。这个结构至少从 2023 年起就 ,比 DOM 选择器可靠得多。

第 1 步:获取页面 HTML

这里建议用 curl_cffi,而不是 requests——因为它可以伪装真实浏览器的 TLS 指纹,这对扛住 Cloudflare 很关键:

1from curl_cffi import requests as cffi_requests
2response = cffi_requests.get(
3    "https://www.indeed.com/jobs?q=python+developer&l=Remote&start=0",
4    impersonate="chrome124",
5    headers={
6        "Accept-Language": "en-US,en;q=0.9",
7        "Referer": "https://www.indeed.com/",
8    },
9    timeout=30,
10)
11print(response.status_code, len(response.text))

为什么要用 curl_cffi?它基于 curl-impersonate,可以复现真实浏览器的 TLS ClientHello、HTTP/2 SETTINGS 帧和请求头顺序。它是目前少数仍在积极维护的 Python HTTP 客户端之一,能够在一次调用里同时对抗 。支持的伪装目标包括 chrome120chrome124chrome131、Safari 和 Edge 变体。

第 2 步:用正则提取 JSON

这个 JSON 是嵌在 <script> 标签里的。可以用正则把它提出来:

1import re, json
2MOSAIC_RE = re.compile(
3    r'window\.mosaic\.providerData\["mosaic-provider-jobcards"\]=(\{.+?\});',
4    re.DOTALL,
5)
6match = MOSAIC_RE.search(response.text)
7if match:
8    data = json.loads(match.group(1))
9    results = data["metaData"]["mosaicProviderJobCardsModel"]["results"]
10    print(f"Found {len(results)} jobs in hidden JSON")
11else:
12    print("没有找到隐藏 JSON——可能是被拦截了,也可能页面结构变了")

第 3 步:从 JSON 中解析职位字段

results 里的每一项,包含的信息比页面上直接看到的还多:

1jobs = []
2for job in results:
3    jobs.append({
4        "jobkey": job["jobkey"],
5        "title": job["title"],
6        "company": job.get("company"),
7        "location": job.get("formattedLocation"),
8        "remote": job.get("remoteLocation"),
9        "salary": (job.get("salarySnippet") or {}).get("text"),
10        "posted": job.get("formattedRelativeTime"),
11        "job_type": job.get("jobTypes"),
12        "easy_apply": job.get("indeedApplyEnabled"),
13        "url": f"https://www.indeed.com/viewjob?jk={job['jobkey']}",
14    })

这个 JSON 往往还包含薪资估算、分类属性(技能标签)和公司评分,而这些信息在渲染后的 HTML 里并不总是可见。

第 4 步:抓取多页数据

先利用 JSON 中的 tierSummaries 了解总结果数,再循环抓取:

1import time, random
2all_jobs = []
3for start in range(0, 50, 10):  # 前 5 页
4    url = f"https://www.indeed.com/jobs?q=python+developer&l=Remote&start={start}&sort=date"
5    response = cffi_requests.get(
6        url,
7        impersonate="chrome124",
8        headers={"Accept-Language": "en-US,en;q=0.9", "Referer": "https://www.indeed.com/"},
9        timeout=30,
10    )
11    match = MOSAIC_RE.search(response.text)
12    if match:
13        data = json.loads(match.group(1))
14        results = data["metaData"]["mosaicProviderJobCardsModel"]["results"]
15        all_jobs.extend([{
16            "jobkey": j["jobkey"],
17            "title": j["title"],
18            "company": j.get("company"),
19            "location": j.get("formattedLocation"),
20            "salary": (j.get("salarySnippet") or {}).get("text"),
21            "url": f"https://www.indeed.com/viewjob?jk={j['jobkey']}",
22        } for j in results])
23    time.sleep(random.uniform(3, 7))
24print(f"总计抓取到 {len(all_jobs)} 条职位")

为什么隐藏 JSON 更稳

window.mosaic.providerData 的结构变化频率远低于 CSS 类名。这样你能拿到干净、结构化的数据,而不用去硬啃乱糟糟的 HTML。即便如此,你仍然需要做反爬处理(请求头、延迟、代理)——这部分我们下一节讲。

如何使用 Selenium 抓取 Indeed

Selenium 是浏览器自动化方案。它适合需要与页面交互的场景,比如点击职位详情面板、处理登录后才显示的内容,或者抓取初始 HTML 里没有的动态加载描述。

什么时候用 Selenium,而不是 HTTP 客户端

  • Indeed 会动态加载部分内容(比如右侧面板中的完整职位描述)
  • 你需要抓取需要会话状态或登录的页面
  • 你只是做小规模抓取,对速度没那么敏感

快速示例

1from selenium import webdriver
2from selenium.webdriver.common.by import By
3from selenium.webdriver.chrome.options import Options
4import time
5options = Options()
6options.add_argument("--disable-blink-features=AutomationControlled")
7# options.add_argument("--headless=new")  # 无头模式更容易被识别,谨慎使用
8driver = webdriver.Chrome(options=options)
9driver.get("https://www.indeed.com/jobs?q=data+engineer&l=New+York")
10time.sleep(3)
11cards = driver.find_elements(By.CSS_SELECTOR, "[data-testid='slider_item']")
12for card in cards:
13    try:
14        title = card.find_element(By.CSS_SELECTOR, "h2.jobTitle").text
15        company = card.find_element(By.CSS_SELECTOR, "[data-testid='company-name']").text
16        location = card.find_element(By.CSS_SELECTOR, "[data-testid='text-location']").text
17        print(f"{title} | {company} | {location}")
18    except Exception:
19        continue
20driver.quit()

局限性

Selenium 很慢——每个页面都要完整渲染浏览器。无头 Chrome 会被 Indeed 的反爬系统 (Cloudflare 会检查 navigator.webdriver、WebGL 的 vendor 字符串、插件数量等等)。即便是 undetected-chromedriver,也只是延迟被识别,并不能永久避免。而且跟 BS4 一样,Indeed 一更新界面,你的选择器就可能失效。

对大多数场景来说,隐藏 JSON 方案既更快又更省维护。只有在你确实需要完整浏览器时,才建议用 Selenium。

如何在抓取 Indeed 时避免 403 错误

这一节最关键。如果你是带着一肚子火从 Google 搜索结果里点进来的,那你来对地方了。

indeed_antibot_374d080ff4.png

Indeed 为什么会拦你的脚本

Indeed 使用的是 ,而不是 DataDome 或 PerimeterX。响应头已经把这点写得很清楚:server: cloudflarecf-ray,以及 __cf_bm 这个 bot 管理 cookie。Cloudflare 会检查你的 TLS 指纹(JA3/JA4)、HTTP/2 请求头顺序、请求模式以及浏览器行为信号。只要其中任何一项看起来“不像真人”,你就可能收到 403、429、503,或者更隐蔽的情况——返回 200 OK,但页面其实是 Turnstile 验证页,而不是职位数据。

轮换 User-Agent 和请求头

固定不变的 User-Agent 是最快被封的方式。应该从一组最新、真实的字符串中轮换。注意:从 User-Agent Reduction 之后,Chrome 的小版本字段已经 ,不要自己编造非零小版本号,否则很容易被反爬系统识别。

1import random
2USER_AGENTS = [
3    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
4    "(KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36",
5    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 "
6    "(KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36",
7    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
8    "(KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36 Edg/145.0.3800.97",
9    "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0",
10    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 "
11    "(KHTML, like Gecko) Version/17.4 Safari/605.1.15",
12]
13headers = {
14    "User-Agent": random.choice(USER_AGENTS),
15    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
16    "Accept-Language": "en-US,en;q=0.9",
17    "Accept-Encoding": "gzip, deflate, br, zstd",
18    "Referer": "https://www.indeed.com/",
19    "Sec-Fetch-Dest": "document",
20    "Sec-Fetch-Mode": "navigate",
21    "Sec-Fetch-Site": "same-origin",
22}

同时要确保 sec-ch-ua 这类 Client Hints 和 UA 版本一致。比如 sec-ch-ua: "Chrome";v="131" 却配了一个声称是 Chrome 145 的 User-Agent,这种矛盾会立刻暴露。

在请求之间加入随机延迟

固定间隔很容易被模式识别。应该使用随机抖动:

1import time, random
2# 每次请求之间
3time.sleep(random.uniform(3, 6))
4# 遇到封禁时的退避
5def backoff_sleep(attempt):
6    base = 4
7    sleep_time = base * (2 ** attempt) + random.uniform(0, 2)
8    time.sleep(min(sleep_time, 60))

根据 的经验,在每个 IP 下最好保持 3–6 秒的请求间隔,并且在单会话内每个 IP 大约不要超过 100 次请求就要轮换。

使用代理轮换

这通常是成功与否的关键因素。来自 AWS/GCP 等数据中心的代理,在 Cloudflare Enterprise 目标上的成功率大概只有 5–15%,对 Indeed 来说基本不可用。使用住宅代理,再配合正确的 TLS 指纹,成功率可以提升到 80–95%。

1PROXIES = [
2    "http://user:pass@us.residential.example:7777",
3    "http://user:pass@us.residential.example:7778",
4    "http://user:pass@us.residential.example:7779",
5]
6proxy = random.choice(PROXIES)
7response = cffi_requests.get(
8    url,
9    impersonate="chrome124",
10    headers=headers,
11    proxies={"https": proxy},
12    timeout=30,
13)

到了 2026 年,住宅代理的价格大致在 之间,具体取决于服务商和套餐承诺。就 Indeed 而言,建议先从一个小型代理池开始,需要时再扩容。

优雅处理 403、429 和 503 状态码

不要盲目重试。不同状态码代表不同问题:

1def fetch_with_retry(url, proxy_pool, max_retries=5):
2    for attempt in range(max_retries):
3        proxy = random.choice(proxy_pool)
4        headers["User-Agent"] = random.choice(USER_AGENTS)
5        try:
6            r = cffi_requests.get(
7                url,
8                impersonate=random.choice(["chrome124", "chrome120", "edge101"]),
9                headers=headers,
10                proxies={"https": proxy},
11                timeout=30,
12            )
13            # 检查最隐蔽的“200 但其实是挑战页”情况
14            if r.status_code == 200 and "cf-turnstile" not in r.text and "Just a moment" not in r.text:
15                return r
16            if r.status_code == 403:
17                print(f"403 —— 被拦截了。正在切换代理,第 {attempt + 1} 次尝试")
18            elif r.status_code == 429:
19                print(f"429 —— 触发限流,放慢速度")
20            elif r.status_code == 503:
21                print(f"503 —— 服务器繁忙,或遇到了 JS 挑战")
22            backoff_sleep(attempt)
23        except Exception as e:
24            print(f"请求错误:{e}")
25            backoff_sleep(attempt)
26    raise RuntimeError(f"重试 {max_retries} 次后仍失败:{url}")

“200 但页面是挑战页”这种情况最麻烦。只要看到 cf-turnstileJust a moment 之类的标记,就别把 200 当成成功。

更省事的替代方案:交给 Thunderbit 处理反爬

如果你不想自己维护代理池、请求头轮换和 TLS 指纹伪装, 的云端抓取会自动处理 CAPTCHA、代理轮换和反爬机制。无需配置代理、无需设置 curl_cffi、也不用接验证码库。对只想尽快拿到数据的人来说,这是阻力最小的路径。

为什么你的 Indeed 爬虫总是坏掉,以及如何修复

403 只是急性痛点,真正长期折磨人的是维护成本——今天还能跑的爬虫,下周就可能悄悄返回空数据或者过期结果。

Indeed 是怎么把你的选择器搞坏的

Indeed 会频繁轮换 CSS 类名。Bright Data 的指南 ,像 css-1m4cuufcss-1rqpxry 这样的类名“看起来像是随机生成的——很可能是在构建时生成的”。A/B 测试意味着不同会话会看到不同页面布局。DOM 结构也会在不通知的情况下重构。

JobFunnel 的经历就很能说明问题。某位贡献者写道:“CaptchaBuster 已经成功缓解了验证码问题,而页面仍然抓取失败的原因,是因为 BeautifulSoup 的选择器已经过时了。” 也就是说,问题不是被封,而是解析到了错误的元素。

策略:优先用隐藏 JSON,而不是直接解析 DOM

window.mosaic.providerData 这个数据块至少从 2023 年起就一直比较稳定。到了 2026 年,metaData.mosaicProviderJobCardsModel.results[] 这条路径 。DOM 选择器可能每月都坏一次,而 JSON 提取往往是一年才需要动一次,甚至根本不用动。

策略:用数据属性,而不是类名

如果确实要碰 DOM,就优先找功能性属性:

选择器用途
[data-testid="slider_item"]每个职位卡片容器
[data-testid="job-title"]h2.jobTitle > a职位标题链接
[data-testid="company-name"]雇主名称
[data-testid="text-location"]地点文本
每个卡片上的 data-jk="<jobkey>"最稳定的定位锚点之一——自 2019 年以来基本没变

加断言检查,尽早发现过时的选择器

不要让爬虫在静默状态下抓到 0 条结果。每次请求后都加一个检查:

1results = parse_hidden_json(html)
2assert len(results) &gt; 0, (
3    f"Indeed 在 start={start} 时返回了空结果 —— "
4    "可能是被拦截、遇到验证码,或者选择器失效。 "
5    f"响应前 500 个字符:{html[:500]}"
6)

出错时把原始响应的前 500 到 2000 个字符记录下来。这样你能立刻判断自己拿到的是 Turnstile 验证页、登录墙,还是页面结构变了。最好每天跑一个固定查询(比如 q=python&l=remote)的 CI 冒烟测试,确认结果条数不为 0。

AI 替代方案:不会轻易坏掉的抓取器

Thunderbit 的 AI 每次都会重新读取页面结构,不依赖写死的选择器或正则模式。Indeed 一旦改了 HTML,Thunderbit 会自动适配。这正好解决了论坛用户最常提到的痛点:维护负担。如果你曾经半夜醒来,看到 Slack 消息说“爬虫又空行了”,你一定懂不用修这种问题有多值钱。

不写 Python 也能抓 Indeed:无代码替代方案

几乎所有竞品教程都默认你会写 Python。但论坛里的声音告诉我们,现实往往不是这样。很多用户会说:“真的太难了,总是出 bug 和报错。” 甚至有人会建议干脆去 Fiverr 找人代抓数据。如果这说中了你,那这一节就是你的出口。

如何用 Thunderbit 抓 Indeed(分步教程)

第 1 步: 从 Chrome 网上应用店安装 。可免费开始使用。

第 2 步: 在浏览器里打开一个 Indeed 搜索结果页,比如 https://www.indeed.com/jobs?q=data+analyst&l=Austin%2C+TX

第 3 步: 点击浏览器工具栏里的 Thunderbit 图标,然后点击 “AI Suggest Fields”。Thunderbit 的 AI 会扫描页面,自动识别出职位名称、公司、地点、薪资、职位链接、发布日期等列。你可以检查并调整系统建议的字段——删除不需要的列,或者直接用自然语言描述你想要的内容,新增自定义字段。

第 4 步: 点击 “Scrape”。Thunderbit 会把页面数据提取出来,并显示成结构化表格。你会看到你配置好的字段对应的职位行。

通过子页面抓取补充详情

抓完列表页之后,点击 “Scrape Subpages”,让 Thunderbit 自动访问每一个职位详情页。它会提取完整职位描述、任职要求、福利和申请链接——无需额外配置。这相当于你又写了一个 Python 爬虫去访问每个 /viewjob?jk=<jobkey> 链接,但 Thunderbit 只需要点一下。

自动处理分页

Thunderbit 会自动处理 Indeed 的点击式分页。不需要你手动拼接 offset URL,也不需要写分页循环。它会自动翻页并汇总结果。

导出到你常用的工具

可以把抓取数据导出为 CSV、Excel、Google Sheets、Airtable 或 Notion——而且 。不用再写 csv.writer()pandas.to_csv() 之类的代码。

什么时候选 Python,什么时候选 Thunderbit

场景最佳工具
自定义数据管道、通过 cron/Airflow 做定时自动化Python
集成到大型代码库中Python
需要高度定制的解析逻辑Python
一次性研究或市场分析Thunderbit
非技术团队成员也需要数据Thunderbit
现在就要数据,不想调试 403Thunderbit
零配置补充子页面信息Thunderbit

时间对比:Python 环境搭建 + 反爬调试 = 数小时到数天,尤其是第一次。Thunderbit = 同样的数据,2 分钟内搞定。我不是说 Python 不对,我只是说要看你的需求是什么。

抓取 Indeed 合法吗?你需要知道的事

目前排名靠前的 Indeed 抓取指南,大多没有讨论合法性,这其实挺让人意外的,因为论坛里经常有人问“抓 Indeed 合法吗?”——以下内容不是法律意见,但可以先帮你看清整体情况。

Indeed 的服务条款

Indeed 的服务条款()并没有一刀切的“禁止抓取”条款。唯一明确的自动化限制是 A.3.5 条,禁止 “使用任何自动化、脚本或机器人来自动化 Indeed Apply 流程。” 这条限制范围很窄,只针对 Apply 申请流程,而不是对公开职位列表的被动读取。Indeed 主要靠技术手段来执行规则——Cloudflare 挑战、IP 封禁、设备指纹识别——而不是法庭诉讼。

相关法律判例

美国最常被引用的案例是 hiQ Labs v. LinkedIn。第九巡回法院在 认定,抓取公开可访问数据“很可能不违反 CFAA(计算机欺诈与滥用法)”。不过,后来 hiQ 仍因 被判有责任,因为其员工创建了虚假的 LinkedIn 账号并接受了服务条款。

更近一些的 Meta v. Bright Data(加州北区法院,2024 年 1 月)给出了更明确的裁定。Chen 法官 Facebook 和 Instagram 的条款“并不禁止对公开数据进行已登出状态下的抓取”。Meta 在次月主动撤回了其余诉求。

Indeed 的 robots.txt

Indeed 的 对默认 User-agent: * 大体上禁止 /jobs//job/ 路径,但明确允许 Googlebot 和 Bingbot 访问 /viewjob?——也就是单个职位详情页。AI 训练爬虫(GPTBot、CCBot、anthropic-ai)则被严格限制。robots.txt 在美国并不具有法律约束力,但遵守它是最佳实践,也能体现善意。

负责任抓取的实用建议

  • 只抓取公开可访问的数据——不要登录,也不要创建假账号
  • 遵守限速:每个 IP 每 3–6 秒 1 次请求,并保持低并发
  • 不要把抓来的数据重新包装成你自己的招聘网站
  • 数据只用于个人或内部研究,不要未经许可商业转售
  • 删除或哈希化你不需要的个人信息;对涉及个人的数据设置保留上限
  • 如果你要大规模运营,或者面向欧盟/英国用户,请先咨询律师——GDPR 第 14 条对抓取到的个人数据有透明度要求

风险层级上看:个人求职自动化属于低风险;大规模商业转售 Indeed 数据则属于高风险。

结论与核心要点

用 Python 抓取 Indeed 是可以做到的,但它不是那种“周末写完就能长期放着跑”的项目。Indeed 的 Cloudflare 防护、不断变化的选择器以及强力反爬机制,意味着你必须用对工具,也要有正确预期。

我会从这篇文章里提炼出以下几点:

  • Indeed 是全网最丰富的招聘市场数据来源之一——3.5 亿月访问量、1.3 亿条职位信息——但它对爬虫的反制也很强。
  • 隐藏 JSON 提取(window.mosaic.providerData)是最稳的 Python 方案。 它的结构多年保持稳定,而 CSS 选择器几乎每月都可能失效。
  • 带浏览器伪装的 curl_cffi 是 2026 年抓 Cloudflare 站点的默认 HTTP 客户端。 单靠 requestshttpx,光是 TLS 指纹就可能被拦。
  • 一定要使用轮换请求头、随机延迟和住宅代理 来避免 403。数据中心代理对 Cloudflare Enterprise 几乎没什么用。
  • 加上断言检查,这样一旦选择器失效,或者返回的是挑战页而不是职位数据,你能马上发现。
  • 对于非技术用户,或者只想快速拿结果的人来说, 提供了无代码、AI 驱动的方案,能自动适应网站变化——无需代理、无需调试、无需维护。

如果你想试试无代码路线,,你可以直接在 Indeed 上测试,不用任何承诺。如果你选择 Python 路线,上面的代码示例已经是很好的起点——只是要记住,把反爬抗性当作核心问题,而不是事后补救。

想了解更多网页抓取方法和工具,可以看看我们关于 的指南。你也可以在 看教程。

试用 Thunderbit,更快抓取 Indeed 数据

常见问题

抓取 Indeed 最适合用哪些 Python 库?

对于 HTTP 请求,curl_cffi 是 2026 年最强的选择——它能伪装真实浏览器的 TLS 指纹,这对绕过 Cloudflare 至关重要。对于不那么严格的目标,带 HTTP/2 的 httpx 也可以作为备选。HTML 解析方面,BeautifulSoup4 搭配 lxml 仍然是标准配置。浏览器自动化方面,Playwright(配合 playwright-stealth)或 undetected-chromedriver 都能用,但它们也越来越容易被识别。隐藏 JSON 正则方案(window.mosaic.providerData)则基本不用做重型解析。

为什么我抓 Indeed 时总是遇到 403 错误?

Indeed 使用 Cloudflare Bot Management,它会检查你的 TLS 指纹(JA3/JA4)、HTTP/2 请求头顺序、请求模式和浏览器行为。如果你用的是普通 requests,TLS 指纹会直接暴露你是 Python 脚本——甚至在请求头被读取之前就先收到 403。解决方法是改用带浏览器伪装的 curl_cffi,轮换真实的 User-Agent,增加随机延迟(3–6 秒),并使用住宅代理。同时也要检查“返回 200 但其实是挑战页”的情况——记得在响应体里搜索 cf-turnstile 标记。

我可以不写代码抓取 Indeed 吗?

可以。 这类工具可以让你只点几下就提取 Indeed 职位列表——安装 Chrome 扩展,打开 Indeed 搜索页,点击“AI Suggest Fields”,再点击“Scrape”即可。Thunderbit 的 AI 会自动识别职位名称、公司、地点和薪资等字段。它还能自动处理分页、子页面补充抓取(完整职位描述)以及反爬机制。数据可以免费导出到 CSV、Google Sheets、Airtable 或 Notion。

Indeed 的 HTML 结构多久会变一次?

Indeed 会经常轮换 CSS 类名(例如 css-1m4cuuf 这类随机哈希字符串),并且在不通知的情况下重构 DOM 元素。A/B 测试也意味着不同用户可能同时看到不同布局。隐藏 JSON 方案(window.mosaic.providerData)稳定得多——这个结构至少从 2023 年起就相当一致。只有在必须解析 DOM 时,才建议优先使用 data-testiddata-jk(job key)这类属性,而不是 CSS 类名。

抓取 Indeed 合法吗?

根据第九巡回法院在 hiQ v. LinkedIn(2022)和 Meta v. Bright Data(2024)中的裁定,对公开可访问的 Indeed 职位 URL 进行已登出状态下的抓取,在美国不太可能构成 CFAA 责任。Indeed 的服务条款明确禁止的是自动化 Apply 流程,而不是对公开列表的被动阅读。尽管如此,仍然要负责任地抓取:不要登录、不要创建假账号、遵守限速、不要把数据当成你自己的招聘站重新发布,并且要谨慎处理任何个人数据(招聘人员姓名、邮箱等),同时遵守 GDPR/CCPA。若是商业规模运营,建议咨询律师。

了解更多

Fawad Khan
Fawad Khan
Fawad writes for a living, and honestly, he kind of loves it. He's spent years figuring out what makes a line of copy stick — and what makes readers scroll past. Ask him about marketing, and he'll talk for hours. Ask him about carbonara, and he'll talk longer.
目录

试试 Thunderbit

只需 2 次点击即可抓取线索和其他数据。AI 驱动。

获取 Thunderbit 免费使用
使用 AI 提取数据
轻松将数据传输到 Google Sheets、Airtable 或 Notion
Chrome Store Rating
PRODUCT HUNT#1 Product of the Week