如何构建职位聚合器:来源、去重和新鲜度
一个好的职位聚合器涉及三个工程问题:从哪里获取职位列表,如何去重,以及如何保持新鲜度。解决这些问题后,抓取就变得简单了——以下是蓝图。
职位聚合器是一个职位搜索引擎:它从多个来源提取列表并在一个地方展示。难点不在于HTTP请求,而在于选择正确的来源,去重出现在五个网站上的同一职位,以及保持一切新鲜,以免展示上个月已填补的职位。做好这三点,你就有了一个有竞争力的产品;做不好,你就会有一个充满重复和过时信息的源,损害信任。此指南是工程蓝图:来源组合、规范去重、新鲜度、职位SERP垂直,以及需要遵守的法律界限。这是信息性指导,而非法律建议。
来源层级1:公共ATS端点
互联网上最干净的职位数据不在职位板上,而是在由申请追踪系统支持的公司招聘页面上,其中大多数提供结构化JSON。Greenhouse、Lever、Ashby、BambooHR、iCIMS、Paylocity和Workday都提供可直接请求的列表,无需HTML解析。一个开源聚合器可以从这七个来源并行提取数据,并根据每个平台的速率限制调整工作者数量——大约50个并发用于Workday,30个用于Greenhouse/Lever/iCIMS,10个用于BambooHR,5个用于限制最严格的。这一层提供来自雇主的规范职位、地点、薪资范围和申请URL,是任何严肃聚合器的基础。
import httpx
# Greenhouse exposes a public board JSON endpoint per company slug
def fetch_greenhouse(slug: str) -> list[dict]:
url = f"https://boards-api.greenhouse.io/v1/boards/{slug}/jobs?content=true"
r = httpx.get(url, timeout=20)
r.raise_for_status()
jobs = r.json().get("jobs", [])
return [{
"source": "greenhouse",
"external_id": str(j["id"]),
"title": j["title"],
"company": slug,
"location": (j.get("location") or {}).get("name"),
"apply_url": j["absolute_url"],
"updated_at": j.get("updated_at"),
} for j in jobs]
rows = fetch_greenhouse("examplecompany")
print(len(rows), "roles")
要找到要轮询的公司,可以从网络索引大规模收集ATS标识符,而不是手动操作——扫描Common Crawl的URL档案以查找ATS域模式可以发现成千上万的公司标识符。这个发现抓取本身就是一个抓取任务;通过一个数据中心代理池路由,因为索引宽松,速度比IP声誉更重要。
来源层级2:职位SERP垂直
ATS源漏掉了只发布到聚合器网站、地区板或Google自己的职位体验的内容。覆盖这一长尾的高效方式是在职位SERP垂直——一个结构化查询,返回Google为某个职位和地点展示的职位,已经标准化。我们的SERP API在新闻、图片和购物旁边提供职位垂直,因此你可以通过关键词和地理位置以JSON格式提取职位列表,并将其整合到同一管道中:
import httpx
def fetch_jobs_serp(query: str, location: str) -> list[dict]:
r = httpx.get(
"https://api.quantumproxies.io/serp",
params={"engine": "google_jobs", "q": query,
"location": location, "api_key": "QP_API_KEY"},
timeout=30,
)
jobs = r.json().get("jobs", [])
return [{
"source": "jobs_serp",
"external_id": j.get("job_id"),
"title": j.get("title"),
"company": j.get("company_name"),
"location": j.get("location"),
"apply_url": (j.get("apply_options") or [{}])[0].get("link"),
} for j in jobs]
serp_rows = fetch_jobs_serp("react developer", "Austin, TX")
在目标城市定期运行相同的查询,你就有了单一ATS源无法提供的地理覆盖。因为SERP结果因地点而异,地理定位每个查询——我们关于2026年SERP抓取工作原理的文章涵盖了地理和分页机制。

通过规范键去重
同一个职位经常出现在公司网站、两个聚合器和一个Google职位卡上。四次展示它是看起来出问题的最快方式。标准修复是规范键:标准化并哈希职位名称、公司、地点的组合,以及——如果可用——来源的外部职位ID。哈希之前先小写、去除标点并合并空白,以便“Sr. Software Engineer”和“senior software engineer”合并在一起。
import re, hashlib
def canonical_key(job: dict) -> str:
def norm(s):
return re.sub(r"[^a-z0-9]+", " ", (s or "").lower()).strip()
basis = "|".join([
norm(job.get("title")),
norm(job.get("company")),
norm(job.get("location")),
(job.get("external_id") or ""),
])
return hashlib.sha1(basis.encode()).hexdigest()
def dedupe(rows: list[dict]) -> list[dict]:
seen, out = set(), []
for job in rows:
k = canonical_key(job)
if k not in seen:
seen.add(k)
out.append(job)
return out
新鲜度是一个特性,而不是事后考虑
职位板的可信度在于其新鲜度。两个机制保持其诚实:按计划重新获取每个来源(高流量ATS源每小时,长尾每日),并删除在滚动窗口中未重新看到的任何内容——30天是一个合理的默认值,快速变化的市场则更短。跟踪每个来源的最后看到时间戳和每日计数,以便在来源无声中断时发现;一个监控自身流量的聚合器可以在平台数据量骤降时立即发出警报。我们关于将抓取工具作为生产软件运行的笔记涵盖了调度、漂移检测和警报。
需要遵守的法律界限
职位数据是抓取法律变得真实的地方,因为职位发布可能包含个人数据(招聘者姓名、联系方式)和详细描述是有版权的。法国的数据保护机构因未经适当同意抓取LinkedIn联系数据而对KASPR公司罚款24万欧元;GDPR罚款可达2000万欧元或全球营业额的4%,美国案件中的版权损害赔偿已达每项作品15万美元。更安全的姿态是结构性的:优先选择授权的ATS源和职位SERP,而不是强行登录有墙的板,存储事实字段(职位、公司、地点)而不是重新发布完整的版权描述,并删除不需要的个人数据。我们关于2026年网络抓取的合法性和LinkedIn合规的概述更深入。

代理的作用
ATS层很少需要住宅IP,但发现抓取、SERP垂直和任何直接板抓取需要——Google和更大的板按IP限速并根据地理位置变化结果。通过旋转住宅代理进行路由,每次请求旋转和地理定位,以便每个城市查询来自匹配的位置。保持层级分开:宽松索引使用廉价数据中心,敏感目标使用住宅。
常见问题解答
什么是职位聚合器?
它是一个职位发布的搜索引擎,从多个来源收集列表——公司招聘页面、ATS平台、其他板和职位SERP——将它们标准化为一个架构,去除重复,并在一个可搜索的地方展示给求职者。工程工作在于来源、去重和新鲜度,而不仅仅是抓取。
如何从多个来源去重职位列表?
通过标准化和哈希职位名称、公司、地点和来源的外部职位ID来构建规范键。首先小写、去除标点并合并空白,以便变体拼写合并在一起。保留第一次出现并删除匹配项。这可以捕捉到同一职位出现在雇主网站、聚合器和Google职位上的情况。
抓取职位板合法吗?
这取决于来源、数据和你的司法管辖区。公共事实列表字段的风险低于个人数据或完整的版权描述,而绕过登录墙会增加CFAA风格的风险。授权的ATS源和职位SERP是更干净的路径。公司因抓取个人数据而被重罚——商业用途请获得法律建议。
聚合的职位数据应该有多新鲜?
每小时重新获取高流量来源如ATS源,长尾每日,然后删除在滚动窗口中未重新看到的任何列表——30天是一个常见的默认值。跟踪每个职位的最后看到时间戳并监控每个来源的量,以便在用户看到过时职位之前发现破损的来源。
将聚合作为三个问题——来源、去重、新鲜度——处理,抓取成为支持细节。以干净的ATS源和职位SERP垂直为主,基于规范键去重,积极修剪,并保持敏感抓取在旋转住宅IP上。这是人们信任的产品与死链接墓地的区别。