Integrate ZTO site module and add debug/single-site mode

- Add site_zto.py: ZTO expected-arrival data download with menu
  navigation, export polling, and Excel merge
- Wire ZTO into main_router: site URL, login detection, menu items
  [5]/[6], and a global offline Left Anti-Join compare entry [9]
- Read config.yaml debug flags to launch only the target site for
  focused debugging; guard each site init and route handler behind
  site-loaded checks via is_site_ready()
- Expand config.example.yaml with documented debug, baishi, and zto
  config keys

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Misaka
2026-06-20 12:46:46 +08:00
parent 7f8d81af21
commit 9375b6efd6
3 changed files with 535 additions and 88 deletions

View File

@@ -1,4 +1,37 @@
# config.example.yaml # config.example.yaml
# 复制本文件为 config.yaml 并填入真实凭据后使用 # ============================================================================
# 复制本文件为 config.yaml 并填入真实凭据后使用:
# cp config.example.yaml config.yaml
#
# 说明:
# - config.yaml 已被 .gitignore 忽略,不会提交到仓库,可安全存放凭据。
# - 所有配置项均设有默认值,未填写时会自动回退到默认行为,不会报错。
# ============================================================================
# ----------------------------------------------------------------------------
# 调试模式:用于单网点联调,仅挂载并启动指定网点,其余网点不加载。
# ----------------------------------------------------------------------------
debug:
# 是否启用调试模式true=仅启动 target_site 一个网点false=全量启动所有网点)。
enabled: false
# 调试模式下要单独启动的网点名称。
# 仅在 enabled: true 时生效。可选值:顺心 / 百世 / 中通。
# 留空或不匹配时将回退为全量模式。
target_site: ""
# ----------------------------------------------------------------------------
# 百世快运 (https://v5.800best.com)
# ----------------------------------------------------------------------------
baishi: baishi:
# 应到未到数据导出时需要填写的“登录密码”。
# 留空会导致导出授权校验失败,无法下载数据。
password: "YOUR_PASSWORD_HERE" password: "YOUR_PASSWORD_HERE"
# ----------------------------------------------------------------------------
# 中通快运 (https://ws.zto56.com/)
# ----------------------------------------------------------------------------
zto:
# 应到货物数据的查询时间范围(单位:天,向前回溯 N 天至今天)。
# 建议保持 1。设置过大且超出日历视窗时会自动降级为仅查询当天。
query_days: 1

View File

@@ -1,17 +1,20 @@
# main_router.py # main_router.py
import os import os
import yaml
import pandas as pd import pandas as pd
from playwright.sync_api import sync_playwright from playwright.sync_api import sync_playwright
# 导入抽离出去的各个网点模块 # 导入抽离出去的各个网点模块
import site_shunxin import site_shunxin
import site_baishi import site_baishi
import site_zto
# 定义网点及对应的初始登录 URL # 定义网点及对应的初始登录 URL
SITES_CONFIG = { SITES_CONFIG = {
"顺心": "https://sxne.sxjdfreight.com", "顺心": "https://sxne.sxjdfreight.com",
"百世": "https://v5.800best.com", "百世": "https://v5.800best.com",
"中通": "https://ws.zto56.com/",
} }
@@ -20,7 +23,6 @@ def task_process_undelivered_data(site_name="顺心"):
print(f"\n▶ 开始执行【{site_name} - 应到未到数据处理】任务...") print(f"\n▶ 开始执行【{site_name} - 应到未到数据处理】任务...")
download_dir = os.path.join(os.getcwd(), "downloads") download_dir = os.path.join(os.getcwd(), "downloads")
# 动态拼接带网点前缀的文件路径
expected_path = os.path.join(download_dir, f"{site_name}-应到货物数据.xlsx") expected_path = os.path.join(download_dir, f"{site_name}-应到货物数据.xlsx")
actual_path = os.path.join(download_dir, f"{site_name}-实到货物数据.xlsx") actual_path = os.path.join(download_dir, f"{site_name}-实到货物数据.xlsx")
output_path = os.path.join(download_dir, f"{site_name}-应到未到货物数据.xlsx") output_path = os.path.join(download_dir, f"{site_name}-应到未到货物数据.xlsx")
@@ -57,7 +59,7 @@ def task_process_undelivered_data(site_name="顺心"):
df_output.to_excel(output_path, index=False) df_output.to_excel(output_path, index=False)
print(f"====================================================") print(f"====================================================")
print(f" 🎉 异常比采取流完成!独立数据已安全输出。") print(f" 🎉 异常比流完成!独立数据已安全输出。")
print(f" 📁 成果归档路径: {output_path}") print(f" 📁 成果归档路径: {output_path}")
print(f"====================================================") print(f"====================================================")
@@ -67,6 +69,27 @@ def task_process_undelivered_data(site_name="顺心"):
def run_multi_site_daemon(): def run_multi_site_daemon():
"""多网点自动化主控引擎""" """多网点自动化主控引擎"""
# 1. 读取配置文件
debug_mode = False
debug_target = ""
try:
if os.path.exists("config.yaml"):
with open("config.yaml", "r", encoding="utf-8") as f:
config = yaml.safe_load(f)
debug_mode = config.get("debug", {}).get("enabled", False)
debug_target = config.get("debug", {}).get("target_site", "")
except Exception as e:
print(f"⚠️ 读取 config.yaml 异常,将使用全量模式启动: {e}")
# 动态确定需要启动的站点
active_sites = {}
if debug_mode and debug_target in SITES_CONFIG:
print(f"\n🛠️ 【调试模式激活】当前仅挂载并启动目标站点: [{debug_target}]")
active_sites = {debug_target: SITES_CONFIG[debug_target]}
else:
active_sites = SITES_CONFIG
with sync_playwright() as p: with sync_playwright() as p:
browser = p.chromium.launch(headless=False) browser = p.chromium.launch(headless=False)
context = browser.new_context(viewport={"width": 1920, "height": 1080}) context = browser.new_context(viewport={"width": 1920, "height": 1080})
@@ -77,7 +100,7 @@ def run_multi_site_daemon():
print("【初始化阶段】正在构建多网点运行环境...") print("【初始化阶段】正在构建多网点运行环境...")
print("====================================================") print("====================================================")
for site_name, url in SITES_CONFIG.items(): for site_name, url in active_sites.items():
print(f">> 正在打开【{site_name}】页面: {url}") print(f">> 正在打开【{site_name}】页面: {url}")
page = context.new_page() page = context.new_page()
page.goto(url) page.goto(url)
@@ -86,7 +109,7 @@ def run_multi_site_daemon():
print("\n====================================================") print("\n====================================================")
print("⚠️ 【等待人工介入】") print("⚠️ 【等待人工介入】")
print("请在弹出的浏览器中,依次切换标签页,人工完成所有网点的登录!") print("请在弹出的浏览器中,人工完成已加载网点的登录!")
print("====================================================") print("====================================================")
input(">> 登录全部完成后,请在此处按下【回车键】正式接管中控台...") input(">> 登录全部完成后,请在此处按下【回车键】正式接管中控台...")
@@ -96,6 +119,7 @@ def run_multi_site_daemon():
print("====================================================") print("====================================================")
# 顺心环境净化 # 顺心环境净化
if "顺心" in pages_map:
try: try:
sx_page = pages_map["顺心"] sx_page = pages_map["顺心"]
sx_page.bring_to_front() sx_page.bring_to_front()
@@ -112,11 +136,11 @@ def run_multi_site_daemon():
sx_page.get_by_role("button", name="不再询问").click() sx_page.get_by_role("button", name="不再询问").click()
sx_page.wait_for_timeout(1000) sx_page.wait_for_timeout(1000)
print(" ✅ 【顺心】弹窗清理完毕,状态就绪!") print(" ✅ 【顺心】弹窗清理完毕,状态就绪!")
except Exception as e: except Exception as e:
print(f" ⚠️ 【顺心】初始化或弹窗清理异常 (如无弹窗可忽略): {e}") print(f" ⚠️ 【顺心】初始化或弹窗清理异常 (如无弹窗可忽略): {e}")
# 百世环境检测与“打地鼠”清理引擎 # 百世环境检测与“打地鼠”清理引擎
if "百世" in pages_map:
try: try:
bs_page = pages_map["百世"] bs_page = pages_map["百世"]
bs_page.bring_to_front() bs_page.bring_to_front()
@@ -126,14 +150,10 @@ def run_multi_site_daemon():
print(" 🎉 登录成功!系统已接管百世浏览器。") print(" 🎉 登录成功!系统已接管百世浏览器。")
print(" >> 启动无序弹窗清理机制 (打地鼠模式)...") print(" >> 启动无序弹窗清理机制 (打地鼠模式)...")
# 留出一点时间让所有前端请求返回并渲染弹窗
bs_page.wait_for_timeout(2000) bs_page.wait_for_timeout(2000)
# 最多循环 5 轮,每次尝试点掉视野内的所有垃圾弹窗
for round_idx in range(5): for round_idx in range(5):
handled_any = False handled_any = False
# 1. 清理【阅读完毕】(可能叠加出现多个)
try: try:
read_btns = bs_page.locator("button:has-text('阅读完毕')") read_btns = bs_page.locator("button:has-text('阅读完毕')")
if read_btns.count() > 0: if read_btns.count() > 0:
@@ -142,57 +162,73 @@ def run_multi_site_daemon():
read_btns.nth(i).click() read_btns.nth(i).click()
bs_page.wait_for_timeout(500) bs_page.wait_for_timeout(500)
handled_any = True handled_any = True
print(" -> 已点击【阅读完毕】")
except Exception: except Exception:
pass pass
# 2. 清理【配置检查 -> 关 闭】
try: try:
close_config = bs_page.locator("button:has-text('关 闭')") close_config = bs_page.locator("button:has-text('关 闭')")
if close_config.is_visible(): if close_config.is_visible():
close_config.click() close_config.click()
bs_page.wait_for_timeout(500) bs_page.wait_for_timeout(500)
handled_any = True handled_any = True
print(" -> 已关闭【配置检查】")
except Exception: except Exception:
pass pass
# 3. 清理【通知提示】
try: try:
notice_close = bs_page.locator("a.ant-notification-notice-close") notice_close = bs_page.locator(
"a.ant-notification-notice-close"
)
if notice_close.is_visible(): if notice_close.is_visible():
notice_close.click() notice_close.click()
bs_page.wait_for_timeout(500) bs_page.wait_for_timeout(500)
handled_any = True handled_any = True
print(" -> 已关闭【通知提示】")
except Exception: except Exception:
pass pass
# 4. 清理【广告】(捕获具有 close-circle 图标的 SVG)
try: try:
ad_close = bs_page.locator("svg[data-icon='close-circle']") ad_close = bs_page.locator("svg[data-icon='close-circle']")
if ad_close.is_visible(): if ad_close.is_visible():
ad_close.click() ad_close.click()
bs_page.wait_for_timeout(500) bs_page.wait_for_timeout(500)
handled_any = True handled_any = True
print(" -> 已关闭【广告弹窗】")
except Exception: except Exception:
pass pass
# 如果这一轮没有任何可见的弹窗被处理,说明页面已经干净了,跳出循环
if not handled_any: if not handled_any:
break break
bs_page.wait_for_timeout(1000) bs_page.wait_for_timeout(1000)
print(" ✅ 【百世】界面净化完毕,状态就绪!") print(" ✅ 【百世】界面净化完毕,状态就绪!")
except Exception as e: except Exception as e:
print(f" ⚠️ 【百世】初始化异常: {e}") print(f" ⚠️ 【百世】初始化异常: {e}")
# 中通环境检测
if "中通" in pages_map:
try:
zto_page = pages_map["中通"]
zto_page.bring_to_front()
print("\n>> 正在处理【中通】网点初始状态...")
zto_page.wait_for_selector('.logo:has-text("网点版")', timeout=30000)
print(" 🎉 登录成功!系统已接管中通浏览器。")
print(" ✅ 【中通】状态就绪!")
except Exception as e:
print(f" ⚠️ 【中通】初始化异常: {e}")
# 路由拦截器
def is_site_ready(site_name):
if site_name not in pages_map:
print(
f"\n🚫 【拒绝执行】当前处于单站点调试模式,或【{site_name}】未加载,无法执行此操作!"
)
return False
return True
while True: while True:
print("\n====================================================") print("\n====================================================")
print(" 物流数据多端提取总枢纽 ") print(" 物流数据多端提取总枢纽 ")
if debug_mode:
print(f" [ 🛠️ 调试模式已激活 : {debug_target} ]")
print("====================================================") print("====================================================")
print(" 模块一:【顺心】数据处理流") print(" 模块一:【顺心】数据处理流")
print(" [1] 执行 - 应到货物数据下载") print(" [1] 执行 - 应到货物数据下载")
@@ -202,27 +238,46 @@ def run_multi_site_daemon():
print(" 模块二:【百世】数据处理流") print(" 模块二:【百世】数据处理流")
print(" [4] 执行 - 一键提取应到未到异常数据") print(" [4] 执行 - 一键提取应到未到异常数据")
print("-" * 52) print("-" * 52)
print(" 模块三:【中通】数据处理流")
print(" [5] 执行 - 应到货物数据下载")
print(" [6] 执行 - 实到货物数据下载 (待开发)")
print("-" * 52)
print(" 全局数据引擎")
print(" [9] 执行 - 离线异常数据清洗比对 (Left Anti-Join)")
print("-" * 52)
print(" [0] 退出系统") print(" [0] 退出系统")
print("====================================================") print("====================================================")
choice = input("请输入任务编号并回车: ") choice = input("请输入任务编号并回车: ")
try: try:
if choice == "1": if choice == "1" and is_site_ready("顺心"):
pages_map["顺心"].bring_to_front() pages_map["顺心"].bring_to_front()
site_shunxin.shunxin_expected_download(pages_map["顺心"]) site_shunxin.shunxin_expected_download(pages_map["顺心"])
elif choice == "2": elif choice == "2" and is_site_ready("顺心"):
pages_map["顺心"].bring_to_front() pages_map["顺心"].bring_to_front()
site_shunxin.shunxin_actual_download(pages_map["顺心"]) site_shunxin.shunxin_actual_download(pages_map["顺心"])
elif choice == "3": elif choice == "3" and is_site_ready("顺心"):
task_process_undelivered_data("顺心") task_process_undelivered_data("顺心")
elif choice == "4": elif choice == "4" and is_site_ready("百世"):
pages_map["百世"].bring_to_front() pages_map["百世"].bring_to_front()
site_baishi.baishi_download_undelivered_data(pages_map["百世"]) site_baishi.baishi_download_undelivered_data(pages_map["百世"])
elif choice == "5" and is_site_ready("中通"):
pages_map["中通"].bring_to_front()
site_zto.zto_expected_download(pages_map["中通"])
elif choice == "6" and is_site_ready("中通"):
pages_map["中通"].bring_to_front()
site_zto.zto_actual_download(pages_map["中通"])
elif choice == "9":
site_name = input("请输入要比对的网点名称 (如 顺心/中通): ").strip()
if not site_name:
site_name = "顺心"
task_process_undelivered_data(site_name)
elif choice == "0": elif choice == "0":
print("\n准备退出程序,释放浏览器资源...") print("\n准备退出程序,释放浏览器资源...")
break break
else: else:
if choice not in ["1", "2", "3", "4", "5", "6", "9", "0"]:
print("\n⚠️ 无效输入,请重新选一下。") print("\n⚠️ 无效输入,请重新选一下。")
except Exception as e: except Exception as e:
print(f"❌ 调度执行异常: {e}") print(f"❌ 调度执行异常: {e}")

359
site_zto.py Normal file
View File

@@ -0,0 +1,359 @@
# 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:
# 1. 探测最外层
try:
if page.get_by_text(text_indicator).count() > 0:
return page
except:
pass
# 2. 穿透扫描所有活跃的子 iframe
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 _ensure_menu_expanded(page, level1_name, level2_name=None):
"""MiniUI 智能菜单展开器"""
print(f">> 正在智能路由菜单...")
if level2_name:
if not page.locator(
"li.shrink span.menu-name", has_text=level2_name
).is_visible():
page.locator(
"a.treeview-title span.menu-name", has_text=level1_name
).click()
page.wait_for_timeout(500)
else:
if not page.locator(
"a.treeview-title span.menu-name", has_text=level1_name
).is_visible():
page.locator(
"a.treeview-title span.menu-name", has_text=level1_name
).click()
page.wait_for_timeout(500)
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▶ 开始执行【中通 - 应到货物数据下载】任务...")
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(">> 正在点击【查询】按钮并等待数据响应...")
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)
empty_flag = ewb_frame.locator("span", has_text="没有搜索到符合条件的数据记录")
if empty_flag.is_visible():
print(" ⚠️ 当前查询范围内【没有搜索到符合条件的数据记录】,终止导出。")
return
ticket_count_str = ewb_frame.locator("#inEwbCount").inner_text().strip()
print(f" ✅ 数据渲染完毕!当前进站实际票数为: [{ticket_count_str}]")
# 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 as e:
print(" ✅ 成功提示框及其容器已自动销毁 (触发 Detached 拦截)。")
export_times.append(datetime.now())
# 同域无缝 Tab 切换回交接单信息
print(" >> 切换回【交接单信息】标签页...")
ewb_frame.locator("#ewbsListNo").click()
page.wait_for_timeout(1000)
if count > 0:
print("✅ 所有交接单的导出任务已成功提交!")
else:
print("⚠️ 未发现任何数据,直接跳转至下载环节。")
# 6. 前往【导出任务管理】
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(2000)
# 7. 轮询任务状态
print(">> 列表中已渲染,开始匹配并检查后端处理状态...")
while True:
task_rows = taskdone_frame.locator(
"#taskdoneDatagrid .mini-grid-rows-view .mini-grid-row"
)
row_count = task_rows.count()
pending_tasks = 0
current_ready_timestamps = []
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 == "进站交接单查询-运单信息":
try:
row_time = datetime.strptime(
submit_time_str, "%Y-%m-%d %H:%M:%S"
)
matched = any(
abs((row_time - et).total_seconds()) <= 120
for et in export_times
)
if matched:
if status_str != "成功执行":
pending_tasks += 1
if submit_time_str not in current_ready_timestamps:
print(
f" ⏳ 任务 [{submit_time_str}] 状态为【{status_str}】,数据生成中..."
)
else:
if submit_time_str not in current_ready_timestamps:
current_ready_timestamps.append(submit_time_str)
except Exception as e:
print(f" ⚠️ 解析时间时出错: {e}")
if pending_tasks > 0:
print(
f">> 共有 {pending_tasks} 个匹配任务还在处理中,等待 5 秒后刷新..."
)
page.wait_for_timeout(5000)
page.locator("li.leaf span.menu-name", has_text="导出任务管理").click()
page.wait_for_timeout(2000)
else:
target_task_timestamps = current_ready_timestamps
if len(target_task_timestamps) > 0:
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}')"
)
)
print(f" 🎯 触发下载 -> 任务 [{time_str}] ...")
# 闭环 Checkbox 勾选逻辑
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}")
# 9. 合并数据
if downloaded_files:
print("\n>> 🧪 正在开始执行扁平数据高能合并流程...")
all_data_frames = []
for file_path in downloaded_files:
try:
# ====================================================================
# 🛡️ 核心修复:强制以字符串类型 (str) 读取所有列,彻底防止长单号变科学计数法或丢失精度
# ====================================================================
df = pd.read_excel(file_path, dtype=str)
if not df.empty:
all_data_frames.append(df)
except Exception as e:
pass
if all_data_frames:
combined_df = pd.concat(all_data_frames, ignore_index=True)
final_output_path = os.path.join(download_dir, "中通-应到货物数据.xlsx")
combined_df.to_excel(final_output_path, index=False)
print(f"====================================================")
print(f" 🎉 恭喜!合并成功!最终输出路径: {final_output_path}")
print(f"====================================================")
for file_path in downloaded_files:
os.remove(file_path)
print("✅ 临时数据清理完毕。")
print("\n🎉 【中通 - 应到货物数据下载】全流程测试完毕!")
except Exception as e:
print(f"\n❌ 任务执行过程中发生异常: {e}")
def zto_actual_download(page):
"""中通:实到货物数据下载"""
print("\n▶ 开始执行【中通 - 实到货物数据下载】任务...")
print("🚧 逻辑开发中 (pass)...")
pass