核心要点:
- Python、Requests 和 BeautifulSoup 足以为服务器渲染页面构建基础的 Indeed 抓取脚本,或用于带有各种集成的类似项目。
- 一个实用的数据集可以包含公司名称、所在地、薪资、职位名称、链接等职位信息。
- 通过分页处理,您可以采集多页职位列表数据,而不止于第一页结果。
- 较低的请求频率、合理的请求头设置以及细致的错误处理,能让获得授权的数据抓取更加稳定。
- HTML 选择器可能发生变化,因此抓取脚本需要定期维护。
Indeed 是网络上规模较大的招聘平台之一,是研究招聘需求、薪资水平、技能要求、地理分布和企业动态的理想数据来源。如果您获得了采集该网站数据的许可,使用 Python 可以直接将可见的职位列表转化为结构化数据集以供分析。
在这篇 Indeed 数据抓取指南中,我们将介绍如何使用 Python、Requests 和 BeautifulSoup 抓取 Indeed 数据。
您将学习如何构建搜索 URL、下载 HTML、解析公司信息和职位详情、处理分页并保存结果,我们还会介绍常见错误以及提升授权抓取脚本可靠性的实用方法。
在开始之前,请务必查阅 Indeed 的服务条款及其 robots.txt 文件,以及与您项目相关的任何合同条款。Indeed 现行的服务条款不允许在未获得明确书面许可的情况下进行数据抓取,因此以下示例仅适用于教学或测试用途,或在您已获得许可的情况下使用。
分步教程:用 Python 抓取 Indeed 职位数据
基础的数据抓取流程并不复杂:构建搜索 URL、发送 HTTP 请求、解析返回的 HTML、提取所需字段,再以结构化格式保存。不过,Indeed 可能会调整页面结构,并根据地区、会话或反自动化机制返回不同的内容。
因此,请将下文的选择器视为起点,而非固定不变的值,检查您获得授权访问的 HTML,并在必要时更新选择器。
前置条件
要按照本教程抓取 Indeed 数据,需要安装 Python 3 以及三个库:用于发送 HTTP 请求的 Requests、用于解析的 BeautifulSoup,以及用于导出结果的 pandas。
pip install requests beautifulsoup4 pandas openpyxl
然后导入这些库:
import time
import random
import requests
import pandas as pd
from bs4 import BeautifulSoup
from urllib.parse import urlencode
学会在浏览器中查看 HTML 代码同样重要。具体方法是:右键点击某个元素并选择“检查”,然后查找 data-testid 等静态标识符、语义化标签或稳定的链接模式,并尽量避免使用长串自动生成的类名。
构建搜索 URL
常见的 Indeed 搜索会使用查询参数来指定职位和地点,与其手动拼接字符串,不如使用 Python 的 urlencode() 函数。
base_url = "https://www.indeed.com/jobs"
params = {
"q": "python developer",
"l": "New York, NY",
"start": 0
}
url = f"{base_url}?{urlencode(params)}"
print(url)
其中 q 参数表示搜索词,l 用于控制地点,start 通常用于分页处理,这让您的 Indeed 抓取脚本能够以可复用的方式生成不同的搜索结果,而无需为每个职位单独硬编码 URL。
如果想研究招聘市场的规律,可以循环遍历多个职位或城市,并将结果分别保存到不同文件中,保持每次查询的范围集中,能让最终的数据集更便于比较。
发送请求
接下来发送一个 GET 请求。由于普通浏览器在每次请求时都会携带请求头,因此提供标准的 User-Agent 和语言请求头是合理的做法。
headers = {
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/129.0.0.0 Safari/537.36"
),
"Accept-Language": "en-US,en;q=0.9"
}
response = requests.get(url, headers=headers, timeout=20)
response.raise_for_status()
html = response.text
print(response.status_code)
对于已获授权的测试,这足以判断页面是否返回了可用的 HTML。如果响应是验证页面、登录页面或拒绝访问页面,请不要尝试绕过安全防护机制,应立即停止操作并检查连接安全性,确认您的访问方式是否获得许可。
如果需要重复抓取 Indeed 数据,请务必添加超时设置和状态码检查,否则连接问题可能导致程序卡死,或让异常 HTML 进入解析环节。
使用 BeautifulSoup 解析 HTML
获取 HTML 后,将其传入 BeautifulSoup:
soup = BeautifulSoup(html, "html.parser")
接下来定位代表各条职位列表的容器元素。由于 Indeed 可能会调整标签结构,请通过浏览器检查来确认当前结构,一种可行的方法是先查找可能的职位卡片容器,再在每张卡片内使用备用选择器。
cards = soup.select("div.job_seen_beacon")
if not cards:
cards = soup.select("[data-testid='slider_item']")
在开发过程中,打印匹配数量会很有帮助:
print(f"Found {len(cards)} cards")
如果浏览器中明明显示了职位,但输出结果却为零,可能是内容加载方式不同、选择器已发生变化,或服务器返回了不同的页面。
抓取公司数据
公司信息通常显示在每张卡片内部,使用辅助函数可以让提取过程更简洁,并在字段缺失时避免脚本崩溃。
def get_text(card, selectors):
for selector in selectors:
element = card.select_one(selector)
if element:
return element.get_text(" ", strip=True)
return None
然后提取公司名称和所在地:
company = get_text(card, [
"[data-testid='company-name']",
".companyName"
])
location = get_text(card, [
"[data-testid='text-location']",
".companyLocation"
])
这些字段有助于开展雇主调研、地理分析,以及比较不同城市的招聘活动,根据页面情况,您可能还会找到公司评分等其他元数据。
提取公司信息时,应妥善处理缺失值,而不要假设每张卡片结构完全相同,赞助内容或格式特殊的卡片可能与标准结果存在差异。
抓取职位数据
接下来添加主要的职位信息,这些信息是大多数数据集所需的核心字段,在可获取的情况下,采集职位名称、链接、薪资文本和简短描述。
title_el = card.select_one("h2 a")
title = title_el.get_text(" ", strip=True) if title_el else None
href = title_el.get("href") if title_el else None
job_url = f"https://www.indeed.com{href}" if href and href.startswith("/") else href
salary = get_text(card, [
"[data-testid='attribute_snippet_testid']",
".salary-snippet-container"
])
snippet = get_text(card, [
".job-snippet",
"[data-testid='jobsnippet_footer']"
])
接着可以构建一个 Python 字典:
job = {
"title": title,
"company": company,
"location": location,
"salary": salary,
"snippet": snippet,
"url": job_url
}
将每个字典追加到一个列表中,这样就能得到整洁的职位列表数据,之后可转换为 DataFrame。
jobs = []
jobs.append(job)
如果需要更完整的职位描述,可能需要访问各个详情页面,这会大幅增加请求数量,因此只有在项目确实需要这些职位信息、且您的授权允许的情况下才应该这样做。
仅基于搜索页面的基础流程,通常就足以提取用于职位名称频率统计、企业需求分析、地区分析或薪资研究的数据,将范围控制得较小,也能让职位数据抓取更易于维护。
处理分页
单个页面只能展示数量有限的结果,如果想在 Indeed 的多个页面上进行抓取,需要修改分页变量。
all_jobs = []
for start in range(0, 50, 10):
params["start"] = start
page_url = f"{base_url}?{urlencode(params)}"
response = requests.get(page_url, headers=headers, timeout=20)
response.raise_for_status()
soup = BeautifulSoup(response.text, "html.parser")
cards = soup.select("div.job_seen_beacon")
for card in cards:
# extract fields here
pass
time.sleep(random.uniform(2, 5))
具体的每页数量或分页行为可能会发生变化,请在您自己的搜索结果中进行核实,当某一页没有结果、达到项目设定的上限,或下一页的模式发生变化时,应停止抓取。
在采集职位发布数据时,不要想当然地认为抓取的页面越多,数据集就一定越好,其中可能存在重复记录、赞助内容等情况,这些都会影响您的数据分析,您可以使用 URL、职位 ID,或职位名称、公司和地点的组合来进行去重。
保存为 CSV 或 Excel
准备好列表后,将其转换为 pandas 的 DataFrame:
df = pd.DataFrame(all_jobs)
将其保存为 CSV 文件:
df.to_csv("indeed_jobs.csv", index=False, encoding="utf-8-sig")
或导出为 Excel 文件:
df.to_excel("indeed_jobs.xlsx", index=False)
CSV 文件简单易用且便于迁移,而如果需要筛选、审阅或快速分发,Excel 文件会更加方便。
保存前可以先去除重复数据:
df = df.drop_duplicates(subset=["title", "company", "location", "url"])
如果分页处理或重复查询返回了重叠的职位信息,这一步会特别有用。
完整代码
下面的示例将各个步骤整合为一个精简的 Indeed 抓取脚本,请仅在获得自动化访问许可的情况下使用,并根据您获准采集的 HTML 更新选择器。
import time
import random
import requests
import pandas as pd
from bs4 import BeautifulSoup
from urllib.parse import urlencode
BASE_URL = "https://www.indeed.com/jobs"
HEADERS = {
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/129.0.0.0 Safari/537.36"
),
"Accept-Language": "en-US,en;q=0.9"
}
def get_text(card, selectors):
for selector in selectors:
el = card.select_one(selector)
if el:
return el.get_text(" ", strip=True)
return None
def parse_card(card):
title_el = card.select_one("h2 a")
title = title_el.get_text(" ", strip=True) if title_el else None
href = title_el.get("href") if title_el else None
if href and href.startswith("/"):
job_url = "https://www.indeed.com" + href
else:
job_url = href
return {
"title": title,
"company": get_text(card, [
"[data-testid='company-name']",
".companyName"
]),
"location": get_text(card, [
"[data-testid='text-location']",
".companyLocation"
]),
"salary": get_text(card, [
"[data-testid='attribute_snippet_testid']",
".salary-snippet-container"
]),
"snippet": get_text(card, [
".job-snippet",
"[data-testid='jobsnippet_footer']"
]),
"url": job_url
}
def scrape_page(query, location, start=0):
params = {
"q": query,
"l": location,
"start": start
}
url = f"{BASE_URL}?{urlencode(params)}"
response = requests.get(url, headers=HEADERS, timeout=20)
response.raise_for_status()
soup = BeautifulSoup(response.text, "html.parser")
cards = soup.select("div.job_seen_beacon")
if not cards:
cards = soup.select("[data-testid='slider_item']")
return [parse_card(card) for card in cards]
all_jobs = []
for start in range(0, 50, 10):
page_jobs = scrape_page("python developer", "New York, NY", start)
if not page_jobs:
break
all_jobs.extend(page_jobs)
time.sleep(random.uniform(2, 5))
df = pd.DataFrame(all_jobs)
df = df.drop_duplicates(
subset=["title", "company", "location", "url"]
)
df.to_csv("indeed_jobs.csv", index=False, encoding="utf-8-sig")
print(f"Saved {len(df)} jobs")
这段脚本可以作为各类数据抓取工作的良好起点,您可以在此基础上轻松实现日志记录、重试机制、数据校验等功能。
抓取 Indeed 数据时的防封锁技巧
如果您获准抓取 Indeed 数据,可靠性就显得尤为重要,请求过于频繁、未处理错误,或对同一页面反复发起多次请求的抓取脚本,不仅效率低下,还可能触发防护机制。
进行这类数据抓取,目的应是提升合法自动化流程的可靠性,而不是用来规避访问限制。
使用代理
代理服务器会将请求通过另一个 IP 地址转发,对于已获授权的项目,如果需要针对特定地区进行测试、实现环境隔离,或使用可控的代理池,代理服务器会很有帮助。
在 Requests 中,代理配置方式如下:
proxies = {
"http": "http://username:password@host:port",
"https": "http://username:password@host:port"
}
response = requests.get(
url,
headers=headers,
proxies=proxies,
timeout=20
)
如果需要从多个获批地区抓取 Indeed 数据,相比单一的数据中心 IP,住宅代理或 ISP 代理能提供更贴近真实的区域路由,不过,代理服务器本身并不等于获得了访问内容的许可,也不能用来规避明确的封锁。
轮换 User-Agent
User-Agent 标识用于标明发起请求的客户端,在获准的测试环境中,在一小组最新的浏览器字符串之间轮换,有助于测试页面对不同浏览器配置的响应方式。
不要使用随机或明显伪造的值,请保持请求头内部的一致性,也不要把轮换 User-Agent 当作绕过访问控制的手段。
对许多小型项目来说,使用一个有效的浏览器 User-Agent 就已足够,只有当您获授权的测试明确涉及多种浏览器类型时,轮换才会变得更有意义。
控制请求频率
添加延迟是降低负载最简单的方法之一。
time.sleep(random.uniform(2, 5))
出现错误后,还可以进一步放慢请求速度,例如在收到 429 响应后延长等待时间,并在连续失败多次后停止请求,而不是持续对服务器发起请求。
合理控制请求节奏,既能保护目标服务,也能让您自己的抓取脚本避免不必要的错误,同时还能让调试更轻松,因为您可以清楚看到是哪个页面引发了问题。
避免单次运行抓取过多页面
不要仅仅因为脚本可以无限循环,就采集所有数据,开始前请先确定实际需要抓取多少页面。
如果 100 条最新的职位列表就足以满足您的分析需求,那就在此停止,规模较小的抓取任务能减少重复数据、流量和存储占用,也能降低采集到无关记录的风险,同时更便于检查选择器是否仍能返回正确的字段。
将大规模的授权职位数据抓取项目拆分为定时批次执行,通常比一次性发出成千上万条请求更易于管理
排查常见错误
即使是简单的爬虫,也可能因网络、HTML 或访问规则的变化而失效,以下是最常见的几类问题
403 禁止访问错误
HTTP 403状态码表示服务器拒绝了该请求。请先检查 URL 是否正确,以及您的程序是否有权执行自动化查询,同时确认请求在请求头设置和流量频率方面没有违反相关规范
如果 Indeed 有意屏蔽您的自动化查询,请不要采取更激进的手段,而应改用官方 API、申请授权,或仅处理您有权访问的数据
HTML 响应为空
有时状态码看起来正常,但 HTML 中并不包含预期的职位列表,可以打印响应内容的开头部分查看:
print(response.text[:1000])
您收到的可能是同意页面、验证挑战、备用布局,或是以不同方式加载内容的页面。建议先将 HTML 保存到本地文件中查看,再决定是否修改解析逻辑
同时请确认所需数据是否直接出现在页面源代码中,因为 BeautifulSoup 只解析其接收到的 HTML,并不会执行 JavaScript
元素缺失或发生变化
如果公司名称、薪资或其他职位信息突然变为 None,可能是页面结构发生了变化
请检查当前的职位卡片并与您的选择器进行比对,优先选用稳定的属性,添加备用选择器,并对每个字段单独测试。这也是生产环境中的爬虫应当记录字段缺失率的原因之一
长期对 Indeed 进行网络爬取时,维护 HTML 解析逻辑是工作的一部分,今天有效的选择器在网站改版后可能就会失效
实际使用场景
有了结构化数据之后,真正有价值的工作才刚刚开始。一份干净的职位列表数据可以支撑多种研究和分析项目,尤其是在分析前先对职位数据进行规范化处理的情况下
就业市场研究
研究人员可以按职位类型、地区、公司或技能对招聘活跃度进行比较。持续重复同一获授权的查询,可以揭示就业市场的变化趋势,例如某些工具需求的增长,或地理分布的变化
薪资字段也可用于大致估算薪酬水平,但在比较之前,需要先清理缺失数据和格式问题
人力资源数据分析
招聘和人事团队可以利用公开且合规获取的职位发布数据,分析招聘需求、研究竞争对手的职位结构,并发现最常被要求的技能
例如,可以按公司对职位进行聚类,统计职位描述中某些技术出现的频率,并分析远程和现场岗位的占比。这类分析对内部人力规划有参考价值,但只能作为了解劳动力市场的一项指标
简历与职位匹配项目
开发者可以利用采集到的职位描述,开展一个小型 NLP 实验,将简历与职位描述进行匹配。基础算法可能包括文本清洗、技能提取和相似度评分
不过在处理个人信息和自动化决策时,开发者需要格外谨慎,任何决策都应由真人完成最终审核
总结与最佳实践
抓取 Indeed 数据需要掌握发起 HTTP 请求、解析 HTML、翻页处理、数据清洗以及维护爬虫等多项技能。借助 Python,这项工作会变得容易许多,而 BeautifulSoup 在从 HTML 页面提取信息方面尤其实用
这里的关键原则在于区分技术上能否实现与是否获得许可。Indeed 现行服务条款明确禁止在未获书面许可的情况下进行任何网络爬取,也禁止尝试绕过系统限制。在采取任何行动之前,请先查阅适用于您情况的相关条款
此外,该公司针对部分职位、雇主、求职者和劳动力市场相关场景提供了官方 API。如果有合适的 API 可供使用,通常会比解析网页 HTML 更有效
最后,请持续关注 HTML 结构的变化:备份示例页面、记录缺失字段、避免高频发送请求,并保持选择器易于修改。可靠的爬虫不仅体现在代码正常运行时,当代码出错时,它同样能告诉您目标网站发生了哪些变化
常见问题
Indeed 允许网络爬取吗?
Indeed 现行服务条款限制在未获明确书面许可的情况下进行自动化数据抓取。请在运行爬虫之前查阅最新条款、robots.txt 规则以及其他适用协议
抓取 Indeed 数据需要用到代理吗?
并非总是如此。代理可以用于获授权的地区测试或流量路由,但并不能赋予访问权限,也不应被用于绕过访问限制
抓取职位发布信息是否合法?
这取决于网站条款、所采集的数据类型、您所在的司法辖区,以及数据的使用方式。信息公开可见并不意味着自动化采集一定被允许,因此对于高风险或商业项目,建议寻求法律意见
可以从 Indeed 导出数据吗?
您可以将通过授权方式采集或获取的数据导出为 CSV 或 Excel 等格式。官方导出功能的可用性取决于您所使用的 Indeed 产品或集成方式
Indeed 是否提供 API?
有。Indeed 目前针对职位发布、求职者、雇主和劳动力市场数据等领域提供合作伙伴 API 和集成方案,但具体的访问权限和允许用途因产品而异