#!/usr/bin/env python3 # -*- coding: utf-8 -*- """招聘报表专用下载器。 报表页(/recruitment/stats/report/list)为逐行报表, 每行 hover 出「预览/下载」, 点「下载」-> 弹参数窗(默认参数) -> 点「确认」-> 服务端异步生成 -> 进任务中心。 本脚本: 触发报表列表区全部「下载」并确认参数窗, 随后按招聘关键词白名单从任务中心 收割新生成的 Excel(避免误收历史的电子合同/参保/薪酬任务), 落盘到 04_招聘/统计分析。 复用 dl_export 的 hook/同步XHR落盘/AGREE防登出/非均匀拟人间隔。""" import sys, time, json, os, base64 sys.path.insert(0, "scripts") import dl_export as X REPORT_URL = "https://2haohr.com/recruitment/stats/report" NODE = "招聘::/recruitment/stats/report" FP = "招聘 / 统计分析 / 招聘报表" ODIR = "downloads/04_招聘/统计分析/招聘报表" # 只收割任务名命中这些招聘特征词的新任务(历史的电子合同/参保档案/薪酬档案不含) REPORT_KW = ["招聘", "候选人", "面试", "offer", "渠道", "淘汰", "门户", "趋势", "进度", "转化", "工作量", "时效", "等待期", "贡献"] # 报表列表区(排除任务中心浮层)未处理的「下载」计数/点击 COUNT_DL = r"""(()=>{return [...document.querySelectorAll('span,a,button')].filter(x=>x.childElementCount===0&&(x.innerText||'').trim()==='下载'&&!x.closest('[class*=taskCenter]')&&!x.hasAttribute('data-dl')).length;})()""" CLICK_DL = r"""(()=>{const d=[...document.querySelectorAll('span,a,button')].find(x=>x.childElementCount===0&&(x.innerText||'').trim()==='下载'&&!x.closest('[class*=taskCenter]')&&!x.hasAttribute('data-dl'));if(!d)return 'none';d.setAttribute('data-dl','1');d.scrollIntoView({block:'center'});d.click();return 'ok';})()""" def match_kw(name): n = (name or "").lower() return any(k.lower() in n for k in REPORT_KW) def trigger_all(): """逐个点报表「下载」并确认参数窗, 返回触发数量。""" fired = 0 total = X.ev(COUNT_DL) X.log("报表列表区可下载报表: %s" % total) for _ in range(30): if X.ev(COUNT_DL) in (0, "0"): break if X.ev(CLICK_DL) != "ok": break fired += 1 # 参数窗异步加载, 循环 AGREE(防登出) + CONFIRM(默认参数确认) confirmed = False for _ in range(12): time.sleep(1.2) X.ev(X.AGREE) r = X.ev(X.CONFIRM) if isinstance(r, str) and r.startswith("modal:") and r != "modal:noconfirm" and r != "modal:nomodal": confirmed = True break X.log(" 触发报表#%d 确认=%s" % (fired, confirmed)) X.ev(X.CLOSE) X.human_delay("click") return fired def harvest_reports(): """逐个点任务中心下载, 用捕获到的真实文件名做招聘白名单过滤(历史的 电子合同/参保/薪酬文件名不命中自动跳过), 命中则落盘到招聘目录。""" X.ev(X.HOOK) try: n = len(json.loads(X.ev(X.TC_LIST))) except Exception: n = 0 X.log("任务中心任务: %d" % n) got = 0 for i in range(n): X.ev("window.__cap=[]") if X.ev(X.TC_CLICK % i) != "ok": continue if not X.wait_capture(20): continue meta = X.ev(X.FETCH_LAST) if not (isinstance(meta, str) and meta.startswith("{")): continue name = X.safe(json.loads(meta)["name"]) if not match_kw(name): X.log(" 跳过(非招聘报表): %s" % name) X.ev("window.__cap=[]"); continue if name in X.SAVED: X.ev("window.__cap=[]"); continue try: data = base64.b64decode(X.ev("window.__b64||''")) except Exception: data = b"" if len(data) <= 200: continue os.makedirs(ODIR, exist_ok=True) dst = os.path.join(ODIR, name) if os.path.exists(dst) and os.path.getsize(dst) > 2000: X.SAVED.add(name); got += 1; continue open(dst, "wb").write(data); X.SAVED.add(name) kind = "xlsx" if name.lower().endswith((".xlsx", ".xls")) else ( "docx" if name.lower().endswith((".docx", ".doc")) else "file") X.rec(NODE, FP, REPORT_URL, kind, dst, "ok", len(data), "招聘报表 " + name) X.log(" 收割: %s (%d bytes)" % (name, len(data))) got += 1 time.sleep(2.5) return got def main(): X.ensure() X.call("find_tab", {"url": "2haohr.com"}) X.call("navigate", {"url": REPORT_URL, "newTab": False}) time.sleep(6) if X.logged_out(): X.log("!! 登录态失效, 中止"); return X.ev(X.HOOK) X.ev(X.AGREE) X.log("=== 招聘报表导出: 触发阶段 ===") fired = trigger_all() X.log("=== 已触发 %d 个报表, 等待服务端生成 ===" % fired) time.sleep(25) X.log("=== 招聘报表导出: 收割阶段 ===") got = harvest_reports() # 二次收割(有的报表生成较慢) if fired > got: time.sleep(20) got += harvest_reports() X.log("=== REPORT DONE 触发=%d 收割=%d ===" % (fired, got)) if __name__ == "__main__": main()