126 lines
5.1 KiB
Python
126 lines
5.1 KiB
Python
#!/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()
|