- Poll #inEwbCount until it updates before reading the result, then branch on zero-count (confirm empty-data area vs missing flag) - Wait for real grid rows to render in the expected-arrival flow - Close the active site tab (进站交接单查询 / 到件扫描监控 / 导出任务管理) after each stage completes to reset the page state - Tighten export-task time-match window from 60s to 20s - Trim verbose log messages and stale comments Co-Authored-By: Claude <noreply@anthropic.com>
561 lines
22 KiB
Python
561 lines
22 KiB
Python
# site_zto.py
|
||
|
||
import os
|
||
import re
|
||
import yaml
|
||
from datetime import datetime
|
||
import pandas as pd
|
||
|
||
|
||
def _wait_and_get_frame(page, text_indicator, timeout_ms=20000):
|
||
"""动态雷达探测器:全域扫描所有视窗"""
|
||
start_time = datetime.now()
|
||
while (datetime.now() - start_time).total_seconds() * 1000 < timeout_ms:
|
||
try:
|
||
if page.get_by_text(text_indicator).count() > 0:
|
||
return page
|
||
except:
|
||
pass
|
||
|
||
for frame in page.frames:
|
||
try:
|
||
if frame.get_by_text(text_indicator).count() > 0:
|
||
return frame
|
||
except:
|
||
pass
|
||
|
||
page.wait_for_timeout(300)
|
||
raise TimeoutError(f"爆栈超时:全域未死守到包含 [{text_indicator}] 的弹窗视窗。")
|
||
|
||
|
||
def zto_smart_menu_click(page, menu_path):
|
||
"""中通智能菜单导航器"""
|
||
print(f">> 正在导航: {' -> '.join(menu_path)}")
|
||
for i in range(len(menu_path)):
|
||
current_menu = menu_path[i]
|
||
if i < len(menu_path) - 1:
|
||
next_menu = menu_path[i + 1]
|
||
next_locator = page.locator("span.menu-name", has_text=next_menu).first
|
||
if not next_locator.is_visible():
|
||
page.locator("span.menu-name", has_text=current_menu).first.click()
|
||
page.wait_for_timeout(800)
|
||
else:
|
||
page.locator("span.menu-name", has_text=current_menu).first.click()
|
||
page.wait_for_timeout(1000)
|
||
|
||
|
||
def zto_expected_download(page):
|
||
"""中通:应到货物数据下载"""
|
||
print("\n▶ 开始执行【中通 - 应到货物数据下载】任务...")
|
||
target_task_title = "进站交接单查询-运单信息"
|
||
|
||
download_dir = os.path.join(os.getcwd(), "downloads")
|
||
if not os.path.exists(download_dir):
|
||
os.makedirs(download_dir)
|
||
|
||
export_times = []
|
||
|
||
try:
|
||
# 1. 智能导航
|
||
zto_smart_menu_click(page, ["运营管理", "进站管理", "进站交接单查询"])
|
||
|
||
ewb_frame = page.frame_locator('iframe[src*="inEwbsListNoSearch"]')
|
||
|
||
print(">> 正在探针检测表格统计面板 (#inEwbCount)...")
|
||
ewb_frame.locator("#inEwbCount").wait_for(state="attached", timeout=15000)
|
||
|
||
# 读取 YAML 配置设定天数
|
||
query_days = 1
|
||
try:
|
||
if os.path.exists("config.yaml"):
|
||
with open("config.yaml", "r", encoding="utf-8") as f:
|
||
config = yaml.safe_load(f) or {}
|
||
query_days = int(config.get("zto", {}).get("query_days", 1))
|
||
except Exception as e:
|
||
print(f" ⚠️ 读取 config.yaml 失败,默认查询 1 天: {e}")
|
||
|
||
print(f">> 正在设定查询时间范围为近【{query_days}】天...")
|
||
ewb_frame.locator("#beginDate").click()
|
||
page.wait_for_timeout(500)
|
||
|
||
today_cell = ewb_frame.locator("td div.day.real-today").first
|
||
today_cell.wait_for(state="visible")
|
||
|
||
today_time_str = today_cell.get_attribute("time")
|
||
if today_time_str:
|
||
today_time = int(today_time_str)
|
||
start_time = today_time - (query_days - 1) * 86400000
|
||
start_cell = ewb_frame.locator(f"td div.day[time='{start_time}']").first
|
||
|
||
if start_cell.is_visible():
|
||
start_cell.click()
|
||
page.wait_for_timeout(300)
|
||
today_cell.click()
|
||
else:
|
||
print(" ⚠️ 设定的天数过大,不在当前日历视窗内,自动降级为查询当天。")
|
||
today_cell.click()
|
||
page.wait_for_timeout(300)
|
||
today_cell.click()
|
||
else:
|
||
today_cell.click()
|
||
page.wait_for_timeout(300)
|
||
today_cell.click()
|
||
|
||
page.wait_for_timeout(500)
|
||
|
||
# 3. 触发查询与深度状态机判定
|
||
print(">> 正在点击【查询】按钮并等待数据响应...")
|
||
old_count = ewb_frame.locator("#inEwbCount").inner_text().strip()
|
||
ewb_frame.locator("#searchbtn").click()
|
||
|
||
page.wait_for_timeout(1000)
|
||
|
||
loading_mask = ewb_frame.locator(".mini-mask-loading", has_text="加载中")
|
||
if loading_mask.is_visible():
|
||
print(" ⏳ 检测到数据加载遮罩层,等待系统渲染...")
|
||
loading_mask.wait_for(state="hidden", timeout=30000)
|
||
|
||
page.wait_for_timeout(1000)
|
||
|
||
# 轮询判定统计面板更新
|
||
wait_cycles = 0
|
||
while wait_cycles < 20:
|
||
current_count = ewb_frame.locator("#inEwbCount").inner_text().strip()
|
||
if current_count != old_count:
|
||
break
|
||
page.wait_for_timeout(500)
|
||
wait_cycles += 1
|
||
|
||
ticket_count_str = ewb_frame.locator("#inEwbCount").inner_text().strip()
|
||
print(f" ✅ 数据面板就绪!当前进站实际票数为: [{ticket_count_str}]")
|
||
|
||
# 数据分流
|
||
if (
|
||
ticket_count_str == "0"
|
||
or not ticket_count_str.isdigit()
|
||
or int(ticket_count_str) == 0
|
||
):
|
||
print(" >> 票数为 0,正在验证是否为确切的空数据区...")
|
||
empty_flag = ewb_frame.locator("#datagrid1").get_by_text(
|
||
"没有搜索到符合条件的数据记录"
|
||
)
|
||
if empty_flag.is_visible():
|
||
print(" ⚠️ 确认为空数据区,正在关闭当前标签页清理环境...")
|
||
# 即使为空数据,我们也清理关闭当前已经打开的 Tab
|
||
try:
|
||
page.locator(".mini-tab", has_text="进站交接单查询").locator(
|
||
".mini-tab-close"
|
||
).click()
|
||
except:
|
||
pass
|
||
return
|
||
else:
|
||
print(" ⚠️ 票数为 0 但未出现空记录提示,终止流程。")
|
||
try:
|
||
page.locator(".mini-tab", has_text="进站交接单查询").locator(
|
||
".mini-tab-close"
|
||
).click()
|
||
except:
|
||
pass
|
||
return
|
||
else:
|
||
print(" >> 票数验证通过,等待主数据表格渲染实体行...")
|
||
ewb_frame.locator(
|
||
"#datagrid1 .mini-grid-rows-view .mini-grid-row"
|
||
).first.wait_for(state="visible", timeout=15000)
|
||
|
||
# 4. 提取主表记录并循环双击
|
||
main_rows = ewb_frame.locator("#datagrid1 .mini-grid-rows-view .mini-grid-row")
|
||
count = main_rows.count()
|
||
print(f">> 共发现 {count} 个交接单需要导出。")
|
||
|
||
for i in range(count):
|
||
print(f" ⏳ 正在处理第 {i+1}/{count} 个交接单...")
|
||
row = ewb_frame.locator(
|
||
"#datagrid1 .mini-grid-rows-view .mini-grid-row"
|
||
).nth(i)
|
||
|
||
raw_text = row.locator("td").nth(3).inner_text()
|
||
match = re.search(r"\d{18}", raw_text)
|
||
handover_no = match.group(0) if match else raw_text.strip()
|
||
print(f" -> 锁定提取单号:{handover_no}")
|
||
|
||
row.dblclick()
|
||
|
||
ewb_frame.locator("#datagrid2").get_by_text("运单号").wait_for(
|
||
state="visible"
|
||
)
|
||
ewb_frame.locator(
|
||
"#datagrid2 .mini-grid-row", has_text=handover_no
|
||
).first.wait_for(state="visible")
|
||
|
||
# 5. 执行导出流程
|
||
ewb_frame.locator("#exportExcel").click()
|
||
|
||
page.locator(".mini-panel-title", has_text="导出选择列").wait_for(
|
||
state="visible"
|
||
)
|
||
export_frame = page.frame_locator('iframe[src*="download"]')
|
||
|
||
export_frame.locator(".mini-button-text", has_text=">>").click()
|
||
page.wait_for_timeout(300)
|
||
export_frame.locator(".mini-button-text", has_text="确定").click()
|
||
|
||
print(" >> [雷达扫描] 正在跨域动态追踪【温馨提示】所在的隐身视窗...")
|
||
ctx_alert = _wait_and_get_frame(page, "温馨提示")
|
||
ctx_alert.locator(
|
||
".mini-messagebox-buttons .mini-button-text", has_text="确定"
|
||
).click()
|
||
print(" ✅ 【温馨提示】已成功通过父级过滤并确认!")
|
||
|
||
print(" >> 正在等待服务器建立后台离线任务...")
|
||
try:
|
||
ctx_tips = _wait_and_get_frame(
|
||
page, "生成离线导出任务成功", timeout_ms=10000
|
||
)
|
||
ctx_tips.locator(".mini-tips-success").wait_for(
|
||
state="hidden", timeout=15000
|
||
)
|
||
print(" ✅ 成功提示框已平滑隐藏。")
|
||
except Exception:
|
||
print(" ✅ 成功提示框及其容器已自动销毁。")
|
||
|
||
export_times.append(datetime.now())
|
||
|
||
print(" >> 切换回【交接单信息】标签页...")
|
||
ewb_frame.locator("#ewbsListNo").click()
|
||
page.wait_for_timeout(1000)
|
||
|
||
# ====================================================================
|
||
# 🛡️ 优化优化:完成交接单主表的所有穿透提交后,立刻销毁本 Tab 净化内存
|
||
# ====================================================================
|
||
print(">> 📥 【进站交接单查询】任务已就绪,正在精准执行 Tab 窗口销毁复位...")
|
||
try:
|
||
page.locator(".mini-tab", has_text="进站交接单查询").locator(
|
||
".mini-tab-close"
|
||
).click()
|
||
page.wait_for_timeout(500)
|
||
print(" ✅ 【进站交接单查询】Tab 已成功关闭。")
|
||
except Exception as e:
|
||
print(f" ⚠️ 尝试关闭【进站交接单查询】Tab 时遇到轻微阻碍: {e}")
|
||
|
||
# 核心逻辑交由统一的轮询下载引擎处理
|
||
_zto_poll_and_download_tasks(
|
||
page,
|
||
export_times,
|
||
target_task_title,
|
||
download_dir,
|
||
"中通-应到货物数据.xlsx",
|
||
)
|
||
|
||
except Exception as e:
|
||
print(f"\n❌ 任务执行过程中发生异常: {e}")
|
||
|
||
|
||
def zto_actual_download(page):
|
||
"""中通:实到货物数据下载"""
|
||
print("\n▶ 开始执行【中通 - 实到货物数据下载】任务...")
|
||
target_task_title = "到件扫描管理"
|
||
|
||
download_dir = os.path.join(os.getcwd(), "downloads")
|
||
if not os.path.exists(download_dir):
|
||
os.makedirs(download_dir)
|
||
|
||
export_times = []
|
||
|
||
try:
|
||
# 1. 智能导航
|
||
zto_smart_menu_click(page, ["运营管理", "扫描操作与监控", "到件扫描监控"])
|
||
|
||
arr_frame = page.frame_locator('iframe[src*="ArriveScan"]')
|
||
|
||
print(">> 正在探针检测主页面 (#daterange)...")
|
||
arr_frame.locator("#daterange").wait_for(state="attached", timeout=15000)
|
||
|
||
# 2. 读取 YAML 并设定时间范围
|
||
query_days = 1
|
||
try:
|
||
if os.path.exists("config.yaml"):
|
||
with open("config.yaml", "r", encoding="utf-8") as f:
|
||
config = yaml.safe_load(f) or {}
|
||
query_days = int(config.get("zto", {}).get("query_days", 1))
|
||
except Exception:
|
||
pass
|
||
|
||
print(f">> 正在设定查询时间范围为近【{query_days}】天...")
|
||
arr_frame.locator("#daterange").click()
|
||
page.wait_for_timeout(500)
|
||
|
||
today_cell = arr_frame.locator("td div.day.real-today").first
|
||
today_cell.wait_for(state="visible")
|
||
|
||
today_time_str = today_cell.get_attribute("time")
|
||
if today_time_str:
|
||
today_time = int(today_time_str)
|
||
start_time = today_time - (query_days - 1) * 86400000
|
||
start_cell = arr_frame.locator(f"td div.day[time='{start_time}']").first
|
||
|
||
if start_cell.is_visible():
|
||
start_cell.click()
|
||
page.wait_for_timeout(300)
|
||
today_cell.click()
|
||
else:
|
||
today_cell.click()
|
||
page.wait_for_timeout(300)
|
||
today_cell.click()
|
||
else:
|
||
today_cell.click()
|
||
page.wait_for_timeout(300)
|
||
today_cell.click()
|
||
|
||
page.wait_for_timeout(500)
|
||
|
||
# 3. 设定单号类型
|
||
print(">> 正在设定单号类型为【子单】...")
|
||
arr_frame.locator('[id="bandEwbType$text"]').click()
|
||
page.wait_for_timeout(500)
|
||
|
||
arr_frame.locator(".mini-tree-nodeshow").filter(
|
||
has_text=re.compile(r"^子单$")
|
||
).locator(".mini-tree-checkbox").click()
|
||
page.wait_for_timeout(300)
|
||
|
||
# 4. 触发查询与深度状态机判定
|
||
print(">> 正在点击【查询】按钮并等待数据响应...")
|
||
arr_frame.locator("#searchbtn").click()
|
||
|
||
page.wait_for_timeout(1000)
|
||
|
||
loading_mask = arr_frame.locator(".mini-mask-loading", has_text="加载中")
|
||
if loading_mask.is_visible():
|
||
print(" ⏳ 检测到数据加载遮罩层,等待系统渲染...")
|
||
loading_mask.wait_for(state="hidden", timeout=30000)
|
||
|
||
page.wait_for_timeout(1000)
|
||
|
||
empty_flag = arr_frame.locator("#datagrid1").get_by_text(
|
||
"没有搜索到符合条件的数据记录"
|
||
)
|
||
if empty_flag.is_visible():
|
||
print(
|
||
" ⚠️ 当前查询范围内【没有搜索到符合条件的数据记录】,终止并关闭环境。"
|
||
)
|
||
try:
|
||
page.locator(".mini-tab", has_text="到件扫描监控").locator(
|
||
".mini-tab-close"
|
||
).click()
|
||
except:
|
||
pass
|
||
return
|
||
|
||
arr_frame.locator("#page1").wait_for(state="visible", timeout=15000)
|
||
print(" ✅ 数据渲染完毕!(底部分页统计控件已就绪)")
|
||
|
||
# 5. 执行导出流程
|
||
arr_frame.locator("#exportExcel").click()
|
||
|
||
page.locator(".mini-panel-title", has_text="导出选择列").wait_for(
|
||
state="visible"
|
||
)
|
||
export_frame = page.frame_locator('iframe[src*="download"]')
|
||
|
||
export_frame.locator(".mini-button-text", has_text=">>").click()
|
||
page.wait_for_timeout(300)
|
||
export_frame.locator(".mini-button-text", has_text="确定").click()
|
||
|
||
print(" >> [雷达扫描] 正在跨域动态追踪【温馨提示】所在的隐身视窗...")
|
||
ctx_alert = _wait_and_get_frame(page, "温馨提示")
|
||
ctx_alert.locator(
|
||
".mini-messagebox-buttons .mini-button-text", has_text="确定"
|
||
).click()
|
||
print(" ✅ 【温馨提示】已成功通过父级过滤并确认!")
|
||
|
||
print(" >> 正在等待服务器建立后台离线任务...")
|
||
try:
|
||
ctx_tips = _wait_and_get_frame(
|
||
page, "生成离线导出任务成功", timeout_ms=10000
|
||
)
|
||
ctx_tips.locator(".mini-tips-success").wait_for(
|
||
state="hidden", timeout=15000
|
||
)
|
||
print(" ✅ 成功提示框已平滑隐藏。")
|
||
except Exception:
|
||
print(" ✅ 成功提示框及其容器已自动销毁。")
|
||
|
||
export_times.append(datetime.now())
|
||
|
||
# ====================================================================
|
||
# 🛡️ 优化优化:完成实到主表的数据导出提交后,立刻销毁本 Tab 恢复空态
|
||
# ====================================================================
|
||
print(">> 📥 【到件扫描监控】任务已就绪,正在精准执行 Tab 窗口销毁复位...")
|
||
try:
|
||
page.locator(".mini-tab", has_text="到件扫描监控").locator(
|
||
".mini-tab-close"
|
||
).click()
|
||
page.wait_for_timeout(500)
|
||
print(" ✅ 【到件扫描监控】Tab 已成功关闭。")
|
||
except Exception as e:
|
||
print(f" ⚠️ 尝试关闭【到件扫描监控】Tab 时遇到轻微阻碍: {e}")
|
||
|
||
# 核心逻辑交由统一的轮询下载引擎处理
|
||
_zto_poll_and_download_tasks(
|
||
page,
|
||
export_times,
|
||
target_task_title,
|
||
download_dir,
|
||
"中通-实到货物数据.xlsx",
|
||
)
|
||
|
||
except Exception as e:
|
||
print(f"\n❌ 任务执行过程中发生异常: {e}")
|
||
|
||
|
||
def _zto_poll_and_download_tasks(
|
||
page, export_times, target_task_title, download_dir, final_filename
|
||
):
|
||
"""进站中通任务专属轮询、验证及即选即隐下载引擎"""
|
||
print("\n>> 正在前往【导出任务管理】界面...")
|
||
zto_smart_menu_click(page, ["系统配置", "导出任务管理"])
|
||
|
||
taskdone_frame = page.frame_locator('iframe[src*="taskdone"]')
|
||
taskdone_frame.locator("#taskdoneDatagrid").get_by_text("任务标题").wait_for(
|
||
state="visible"
|
||
)
|
||
page.wait_for_timeout(1000)
|
||
|
||
print(">> 列表中已渲染,开始匹配并检查后端处理状态...")
|
||
total_expected = len(export_times)
|
||
|
||
while True:
|
||
task_rows = taskdone_frame.locator(
|
||
"#taskdoneDatagrid .mini-grid-rows-view .mini-grid-row"
|
||
)
|
||
row_count = task_rows.count()
|
||
|
||
ready_timestamps = set()
|
||
processing_timestamps = set()
|
||
|
||
for i in range(row_count):
|
||
tds = task_rows.nth(i).locator("td")
|
||
if tds.count() < 10:
|
||
continue
|
||
|
||
title_str = tds.nth(3).inner_text().strip()
|
||
submit_time_str = tds.nth(4).inner_text().strip()
|
||
status_str = tds.nth(6).inner_text().strip()
|
||
|
||
if title_str == target_task_title:
|
||
try:
|
||
row_time = datetime.strptime(submit_time_str, "%Y-%m-%d %H:%M:%S")
|
||
matched = any(
|
||
abs((row_time - et).total_seconds()) <= 20
|
||
for et in export_times
|
||
)
|
||
|
||
if matched:
|
||
if status_str == "成功执行":
|
||
ready_timestamps.add(submit_time_str)
|
||
else:
|
||
processing_timestamps.add(submit_time_str)
|
||
except Exception:
|
||
pass
|
||
|
||
total_found = len(ready_timestamps) + len(processing_timestamps)
|
||
print(
|
||
f" 📊 状态盘点:期望任务数 [{total_expected}],实际入表 [{total_found}] (就绪 [{len(ready_timestamps)}],处理中 [{len(processing_timestamps)}])"
|
||
)
|
||
|
||
if total_found < total_expected or len(processing_timestamps) > 0:
|
||
print(" ⏳ 发现任务缺失或正在生成,执行【点击查询按钮】触发局部重载...")
|
||
try:
|
||
taskdone_frame.locator(
|
||
".mini-button-text", has_text="查询"
|
||
).first.click()
|
||
except Exception as e:
|
||
print(f" ⚠️ 局部刷新按钮失效,使用菜单后备刷新: {e}")
|
||
page.locator("li.leaf span.menu-name", has_text="导出任务管理").click()
|
||
|
||
page.wait_for_timeout(3000)
|
||
else:
|
||
target_task_timestamps = list(ready_timestamps)
|
||
print(">> ✅ 目标任务已全部生成,解除锁定,开始接收文件流...")
|
||
break
|
||
|
||
# 8. 下载逻辑
|
||
downloaded_files = []
|
||
for time_str in target_task_timestamps:
|
||
try:
|
||
target_row = (
|
||
taskdone_frame.locator(
|
||
"#taskdoneDatagrid .mini-grid-rows-view .mini-grid-row"
|
||
)
|
||
.filter(
|
||
has=taskdone_frame.locator(
|
||
f"td:nth-child(5):has-text('{time_str}')"
|
||
)
|
||
)
|
||
.first
|
||
)
|
||
print(f" 🎯 触发下载 -> 任务 [{time_str}] ...")
|
||
|
||
checkbox = target_row.locator(".mini-grid-checkbox")
|
||
if checkbox.is_visible():
|
||
checkbox.click()
|
||
print(" -> 已勾选当前记录的 Checkbox")
|
||
page.wait_for_timeout(500)
|
||
|
||
with page.expect_download() as download_info:
|
||
target_row.locator("td").nth(10).locator(".ui-btn-download").click()
|
||
|
||
download = download_info.value
|
||
save_path = os.path.join(download_dir, download.suggested_filename)
|
||
download.save_as(save_path)
|
||
downloaded_files.append(save_path)
|
||
print(f" ⬇️ 文件已落盘: downloads/{download.suggested_filename}")
|
||
|
||
if checkbox.is_visible():
|
||
checkbox.click()
|
||
print(" -> 已取消勾选,清理现场进入下一条")
|
||
page.wait_for_timeout(500)
|
||
|
||
except Exception as e:
|
||
print(f" ❌ 下载任务 [{time_str}] 失败: {e}")
|
||
|
||
# ====================================================================
|
||
# 🛡️ 优化优化:所有目标任务文件全部下载完毕后,立刻销毁“导出任务管理” Tab
|
||
# ====================================================================
|
||
print(">> 📥 【导出任务管理】所有数据拉取流闭环,正在执行 Tab 窗口销毁复位...")
|
||
try:
|
||
page.locator(".mini-tab", has_text="导出任务管理").locator(
|
||
".mini-tab-close"
|
||
).click()
|
||
page.wait_for_timeout(500)
|
||
print(" ✅ 【导出任务管理】Tab 已安全关闭。页面归档空态。")
|
||
except Exception as e:
|
||
print(f" ⚠️ 尝试收尾关闭【导出任务管理】Tab 时有波动: {e}")
|
||
|
||
# 9. 合并数据
|
||
if downloaded_files:
|
||
print("\n>> 🧪 正在开始执行扁平数据高能合并与清洗流程...")
|
||
all_data_frames = []
|
||
for file_path in downloaded_files:
|
||
try:
|
||
df = pd.read_excel(file_path, dtype=str)
|
||
if not df.empty:
|
||
all_data_frames.append(df)
|
||
except Exception:
|
||
pass
|
||
|
||
if all_data_frames:
|
||
combined_df = pd.concat(all_data_frames, ignore_index=True)
|
||
final_output_path = os.path.join(download_dir, final_filename)
|
||
combined_df.to_excel(final_output_path, index=False)
|
||
print(f"====================================================")
|
||
print(f" 🎉 恭喜!数据流闭环完成!")
|
||
print(f" 📁 最终输出成果归档至: {final_output_path}")
|
||
print(f"====================================================")
|
||
|
||
for file_path in downloaded_files:
|
||
os.remove(file_path)
|
||
print(" ✅ 临时缓存数据清理完毕。")
|