现代招聘平台普遍采用前后端分离架构,职位数据经由 XHR 异步下发,且核心字段分散于列表接口与详情接口。以requests直采静态 HTML 仅能获取渲染骨架,难以保证薪资区间、HR 活跃状态等字段的完整性与保真度。本文基于 Playwright 构建采集流水线,以「接口实时监听」完成字段初采,以「新 Tab 精准加载」完成详情补全,并引入亿牛云(16yun)代理 IP 分散出口,形成一套可复用、抗风控的职位采集方案。
一、问题建模
招聘类站点的采集瓶颈可归纳为三类约束:
- 数据异步化:页面 HTML 为渲染骨架,真实数据来自 XHR 接口,静态直采仅得空壳。
- 字段跨接口:列表接口返回职位名、公司、城市等概要字段,薪资结构与 HR 在线状态通常仅由详情接口下发。
- 上下文依赖:详情接口需在「列表→详情」跳转链路中携带正确参数与
referer,脱离该上下文直接构造请求易触发 403 或返回空载荷。
由此确立两层采集策略:监听层负责发现与初采,加载层负责补全与深采;二者以信号量约束并发,叠加代理层实现出口分散。
二、武器一:接口实时监听(监听层)
通过page.on("response")在浏览器网络层挂载响应钩子,接口返回即拦截 JSON 响应体,并按路径特征过滤目标接口。相较于 DOM 抽取,XHR 响应拦截直接获取结构化原始载荷,规避渲染时序依赖与选择器脆弱性,对前端结构变更具备更强鲁棒性。
fromplaywright.sync_apiimportsync_playwright LIST_API_HINT="/api/i/zl/zlgj"defon_response(response):ifLIST_API_HINTnotinresponse.url:returntry:payload=response.json()exceptException:returnjobs=(payload.get("data",{}).get("list")orpayload.get("result",{}).get("list")or[])forjobinjobs:print(job.get("jobName"),job.get("salary"),job.get("hrStatus"))withsync_playwright()asp:browser=p.chromium.launch(headless=False,args=["--disable-blink-features=AutomationControlled"])page=browser.new_page()page.on("response",on_response)page.goto("https://www.zhaopin.com/sou?kw=Python&city=765",wait_until="networkidle")page.wait_for_timeout(3000)browser.close()工程要点:以路径片段而非完整 URL 匹配,可兼容接口版本演进;networkidle配合超时以捕获懒加载触发的后续请求;禁用自动化特征标志以降低被识别为自动化流量的概率。
三、武器二:新 Tab 精准加载(加载层)
列表层获取jobId后,模拟真实用户跳转行为,为每条职位新建 Tab,触发携带正确referer与业务参数的详情接口,补全薪资与 HR 状态。
importthreading DETAIL_API_HINT="/api/i/zl/job/detail"results,lock=[],threading.Lock()defcollect_detail(response):ifDETAIL_API_HINTnotinresponse.url:returntry:d=response.json().get("data",{})exceptException:returnwithlock:results.append({"jobId":d.get("jobId"),"salary":d.get("salary"),"hrActiveStatus":d.get("hrInfo",{}).get("activeStatus"),"hrActiveTime":d.get("hrInfo",{}).get("activeTime"),})defopen_detail_tab(context,job_id):tab=context.new_page()tab.on("response",collect_detail)tab.goto(f"https://www.zhaopin.com/jobdetail/{job_id}",wait_until="networkidle")tab.wait_for_timeout(1500)tab.close()新建 Tab 继承上下文的登录态与referer,可规避 403;单实例用毕即释放,内存占用可控,适配长列表的批量深采。
四、组合流水线
监听层填充job_index后,以信号量约束并发详情 Tab 数,逐条深采并合并落库。信号量本质是一种并发令牌机制,在吞吐量与对端压力之间取得平衡。
job_index,detail_buf={},[]lock=threading.Lock()sema=threading.Semaphore(3)defdeep_collect(context,job_id):withsema:tab=context.new_page()tab.on("response",on_detail)tab.goto(f"https://www.zhaopin.com/jobdetail/{job_id}",wait_until="networkidle")tab.wait_for_timeout(1200)tab.close()# on_list / on_detail 监听逻辑同前,合并写入 zhilian_final.json五、数据建模
薪资字段(如"15-25K")归一为min/max/unit,便于后续聚合统计;HR 活跃状态(online/offline配合activeTime)是判定职位时效性的关键信号——HR 长期离线通常意味着投递转化率低,可作为「活跃职位」过滤阈值。
importredefparse_salary(raw):ifnotraw:returnNonem=re.search(r"(\d+(?:\.\d+)?)\s*[-~]\s*(\d+(?:\.\d+)?)\s*([Kk万]?)",raw)ifnotm:returnNonelo,hi,u=float(m.group(1)),float(m.group(2)),m.group(3)mult=1000ifu.upper()=="K"else(10000ifu=="万"else1)return{"min":lo*mult,"max":hi*mult,"currency":"CNY"}六、亿牛云代理:出口分散与风控对抗
规模化采集时,单一出口 IP 的请求指纹高度集中,易超出平台风控的频率阈值而触发限流乃至封禁。亿牛云(16yun)提供企业级代理 IP 服务,按形态可分为二类:
- 隧道代理:单一入口地址,后端自动轮转出口节点,客户端无需维护 IP 池,契合高频、无状态采集。
- 动态短效代理:按存活时长(如 1–5 分钟)分配出口 IP,支持短时会话亲和,适合需维持会话状态的场景。
针对本文招聘采集,隧道代理最为适配——开箱即用、出口自动轮转,与信号量限流协同即可在稳定性与成本间取得平衡。Playwright 通过proxy参数接入,鉴权链路由代理层承载:
YINIU_PROXY={"server":"http://t.16yun.cn:31111",# 隧道代理入口,以亿牛云后台为准"username":"YOUR_16YUN_USER","password":"YOUR_16YUN_PASS",}browser=p.chromium.launch(headless=False,proxy=YINIU_PROXY,args=["--disable-blink-features=AutomationControlled"])接入后,监听层与加载层逻辑无需改动。若选用动态短效代理,则需将 IP 列表纳入请求轮询,并在会话超时后重新获取出口。实践上,隧道代理按流量计费,应结合限流与重试控制单位成本;对多城市/多行业任务可拆分至不同隧道入口以进一步分散出口;需明确的是,代理仅解决出口分布问题,不豁免合规义务(见下节)。
七、稳定性与合规
稳定性:以信号量约束并发、引入随机请求间隔、goto设置超时与重试、已采jobId持久化以支持断点续跑;代理层异常时应具备降级与告警能力。
合规:仅采集公开展示的职位信息,不抓取求职者个人数据(PII);遵守robots.txt与平台用户协议,控制请求频率;禁止商用转售或用于不正当竞争;企业内用须获授权并做脱敏处理。优先评估平台官方开放 API。
八、小结
监听层解决「数据从何而来」,加载层解决「详情如何补全」,代理层解决「出口如何分散」。三层以信号量约束并发协同运作,可产出结构干净、状态可辨的职位数据集,为薪资分布分析、HR 响应率评估等下游场景提供可靠的数据底座。