Files
twoJob-demo/scripts/dl_report.py
T
2026-09-03 15:35:23 +08:00

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()