first commit
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user