# site_yunda.py import os import re import yaml from datetime import datetime, timedelta 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 _wait_and_get_frame_by_selector(page, selector, timeout_ms=20000): """【控件雷达探测器】无视 IFrame 的 src 或层级,直接扫描谁包含指定 CSS 控件""" start_time = datetime.now() while (datetime.now() - start_time).total_seconds() * 1000 < timeout_ms: try: if page.locator(selector).count() > 0: return page except: pass for frame in page.frames: try: if frame.locator(selector).count() > 0: return frame except: pass page.wait_for_timeout(300) raise TimeoutError(f"爆栈超时:全域未探测到包含控件 [{selector}] 的业务框架。") def yunda_smart_menu_click(page, menu_path): """韵达排他性二级树形菜单智能展开器""" print(f">> 正在导航韵达菜单: {' -> '.join(menu_path)}") for item in menu_path: locator = page.locator(".el-submenu__title, .el-menu-item", has_text=item).first locator.click() page.wait_for_timeout(600) def yunda_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. 验证首页并智能路由 page.locator(".el-menu-item", has_text="首页").wait_for( state="visible", timeout=15000 ) print("✅ 韵达工作台首页成功加载") yunda_smart_menu_click(page, ["运营管理", "进站管理", "进站交接单查询"]) print(">> [雷达扫描] 正在跨域动态追踪【进站交接单查询】业务窗体...") ws_frame = _wait_and_get_frame_by_selector(page, "#startTime") ws_frame.locator("#startTime").wait_for(state="attached", timeout=15000) print("✅ 进站交接单查询工作区就绪") # 2. 从配置文件中解析并计算绝对日期跨度 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("yunda", {}).get("query_days", 1)) except Exception as e: print(f" ⚠️ 读取 config.yaml 失败,默认查询 1 天: {e}") today = datetime.now() start_date = today - timedelta(days=(query_days - 1)) today_ymd = f"{today.year}-{today.month}-{today.day}" start_date_ymd = f"{start_date.year}-{start_date.month}-{start_date.day}" print(f">> 正在精准对焦时间区间: [{start_date_ymd}] 至 [{today_ymd}]") # 设定起始时间 print(" >> 呼出起始时间控件...") page.wait_for_timeout(1000) ws_frame.locator("#startTime").click(force=True) calendar1 = ws_frame.locator(".layui-laydate:visible").first calendar1.wait_for(state="visible", timeout=5000) calendar1.locator(f"td[lay-ymd='{start_date_ymd}']").click() calendar1.locator(".laydate-btns-confirm").click() page.wait_for_timeout(400) # 设定截止时间 print(" >> 呼出截止时间控件...") ws_frame.locator("#endTime").click(force=True) calendar2 = ws_frame.locator(".layui-laydate:visible").first calendar2.wait_for(state="visible", timeout=5000) calendar2.locator(f"td[lay-ymd='{today_ymd}']").click() calendar2.locator(".laydate-btns-confirm").click() page.wait_for_timeout(500) # 3. 多维状态机静默加载校验 print(">> 正在触发现场查询数据流...") ws_frame.locator("a.btn-success", has_text="查询").click() page.wait_for_timeout(1000) sum_panel = ws_frame.locator("#sum") has_data = False if sum_panel.is_visible(): sum_text = sum_panel.inner_text() match_tickets = re.search(r"进站实际票数:(\d+)", sum_text) if match_tickets and int(match_tickets.group(1)) > 0: has_data = True print( f" ✅ 深度断言:局部统计面板加载完毕,实际票数: [{match_tickets.group(1)}],执行穿透。" ) if not has_data: if ws_frame.locator( ".no-records-found", has_text="没有找到匹配的记录" ).is_visible(): print( " ⚠️ 确认为空数据环境:系统提示【没有找到匹配的记录】,正在执行复位净化..." ) page.locator(".tags-view-item", has_text="进站交接单查询").locator( ".el-icon-close" ).click() return # 4. 深度等待表格第一行数据行渲染就绪 ws_frame.locator("#exampleTable1 tbody tr[data-index='0']").wait_for( state="visible", timeout=10000 ) main_rows = ws_frame.locator("#exampleTable1 tbody tr[data-index]") row_count = main_rows.count() print(f">> 当前视窗共捕获到活跃交接单记录: {row_count} 条") # 5. 循环双击穿透提交 for i in range(row_count): print(f" ⏳ 正在处理第 {i+1}/{row_count} 个交接单模块...") current_row = ws_frame.locator("#exampleTable1 tbody tr[data-index]").nth(i) raw_no = current_row.locator("td").nth(1).inner_text().strip() print(f" -> 锁定交接单号: {raw_no}") current_row.dblclick() ws_frame.locator("#docSum").wait_for(state="visible", timeout=15000) page.wait_for_timeout(500) ws_frame.locator('a.btn-info[onclick*="exportFile"]').click() ws_frame.locator(".layui-layer-title", has_text="数据导出").wait_for( state="visible", timeout=15000 ) export_frame = ws_frame.frame_locator('iframe[name="target1"]') # 首次触发全选所有字段 export_frame.locator(".allRight").click() page.wait_for_timeout(400) # ==================================================================== # 🛡️ 智能容错重试状态机:完美兼容【离线建立失败】与【字段全选漏掉报错】 # ==================================================================== print(" >> 正在建立后台离线任务...") task_success = False for attempt in range(5): export_frame.locator("#submitbutton", has_text="导出数据").click() print( f" -> 已触发第 {attempt + 1} 次【导出数据】指令,等待校验响应..." ) # 依靠具防抖特性的 Link 角色定位“确定”按钮 confirm_link = export_frame.get_by_role("link", name="确定") try: # 等待提交重载后的新文档渲染出提示框 confirm_link.wait_for(state="visible", timeout=6000) # 场景 1:建立成功 if export_frame.get_by_text("导出任务建立成功").is_visible(): print(" ✅ 判定通过:成功捕获到【导出任务建立成功】特征!") confirm_link.click() task_success = True break # 场景 2:核心优化 - 发现字段全选未生效报错 elif export_frame.get_by_text( "请选择格式相应的导出字段" ).is_visible(): print( " ⚠️ 警告:检测到【未选择字段】错误提示!正在清除弹窗并强行补点【全选】按钮..." ) confirm_link.click() page.wait_for_timeout(500) # 再次执行强行补点全选 export_frame.locator(".allRight").click() print( " -> 【全选】按钮已重新触发补点,准备进入下一轮提交。" ) page.wait_for_timeout(500) # 场景 3:其他网络或未知异常提示 else: print( " ⚠️ 警告:检测到其他异常提示消息,正在清除弹窗并重试..." ) confirm_link.click() page.wait_for_timeout(1000) except Exception as e: print( f" ⚠️ 提示:第 {attempt + 1} 次提交后未能在安全时间内捕获到提示框 ({e}),准备重试..." ) page.wait_for_timeout(1000) if not task_success: raise RuntimeError( "致命异常:连续 5 次尝试均无法成功建立任务,请检查网点后台事务层。" ) # ==================================================================== # 关闭高层 Layui 挂载的数据导出窗口 ws_frame.locator(".layui-layer-close1").click() page.wait_for_timeout(500) # 回退到主表标签页 ws_frame.locator("#myTab a", has_text="交接单信息").click() page.wait_for_timeout(800) export_times.append(datetime.now()) # 6. 一阶段全量数据提交闭环,销毁当前业务 Tab print(">> 📤 任务提交流闭环,正在执行【进站交接单查询】工作台销毁...") page.locator(".tags-view-item", has_text="进站交接单查询").locator( ".el-icon-close" ).click() page.wait_for_timeout(500) # 7. 进入【导出服务】队列收割 _yunda_poll_and_download_tasks( page, export_times, target_task_title, download_dir, "韵达-应到货物数据.xlsx", ) except Exception as e: print(f"\n❌ 任务执行过程中发生异常: {e}") def _yunda_poll_and_download_tasks( page, export_times, target_task_title, download_dir, final_filename ): """韵达专属离线任务轮询下载引擎""" print("\n>> 正在前往【导出服务】中心...") yunda_smart_menu_click(page, ["基础数据", "导出服务"]) export_ws_frame = page.frame_locator("section iframe") export_ws_frame.get_by_role("cell", name="模块名称", exact=True).wait_for( state="visible", timeout=15000 ) page.wait_for_timeout(1000) print(">> 离线文件队列已对接,启动【1分钟高频精确校对+缺单局部重载刷新】断言...") total_expected = len(export_times) while True: task_rows = export_ws_frame.locator( ".datagrid-view2 .datagrid-btable tbody tr.datagrid-row" ) row_count = task_rows.count() ready_indices = [] processing_indices = [] for idx in range(row_count): row = task_rows.nth(idx) module_name = row.locator("td[field='modueName']").inner_text().strip() status_name = row.locator("td[field='fileStatus']").inner_text().strip() create_time_str = ( row.locator("td[field='createdTime']").inner_text().strip() ) if module_name == target_task_title: try: row_time = datetime.strptime(create_time_str, "%Y-%m-%d %H:%M:%S") matched = any( abs((row_time - et).total_seconds()) <= 60 for et in export_times ) if matched: if status_name == "导出完成": ready_indices.append(idx) else: processing_indices.append(idx) except Exception: pass total_found = len(ready_indices) + len(processing_indices) print( f" 📊 状态研编:自建期望数 [{total_expected}],实际入表 [{total_found}] (完成 [{len(ready_indices)}],生成中 [{len(processing_indices)}])" ) if total_found < total_expected or len(processing_indices) > 0: print(" ⏳ 队列未齐,执行【点击查询按钮】触发局部无痕刷新...") export_ws_frame.locator( "#ydkyimport_basic_export_searchData1_ky_export_common" ).click() page.wait_for_timeout(3000) else: print(">> ✅ 所有目标离线任务全量就绪!开始依序接入文件流...") break # 8. 下载成果流 downloaded_files = [] for row_idx in ready_indices: try: target_row = export_ws_frame.locator( ".datagrid-view2 .datagrid-btable tbody tr.datagrid-row" ).nth(row_idx) time_flag = ( target_row.locator("td[field='createdTime']").inner_text().strip() ) print(f" 🎯 触发下载 -> 离线任务时间节点: [{time_flag}] ...") with page.expect_download() as download_info: target_row.locator("td[field='extreFile'] a").get_by_text( "下载" ).first.click() download = download_info.value safe_timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f") custom_filename = f"韵达_temp_{safe_timestamp}.xlsx" save_path = os.path.join(download_dir, custom_filename) download.save_as(save_path) downloaded_files.append(save_path) print(f" ⬇️ 文件已用安全序列号落盘: downloads/{custom_filename}") page.wait_for_timeout(500) except Exception as e: print(f" ❌ 文件流接收失败: {e}") # 环境清理:彻底关闭“导出服务”Tab print(">> 📥 【导出服务】数据提取链闭环,正在执行当前 Tab 窗口销毁...") try: page.locator(".tags-view-item", has_text="导出服务").locator( ".el-icon-close" ).click() print(" ✅ 【导出服务】工作区已安全关闭。") except: pass # 9. 合并扁平数据集 if downloaded_files: print("\n>> 🧪 正在启动离线数据清洗与高能扁平合并流...") all_dfs = [] for file_path in downloaded_files: try: df = pd.read_excel(file_path, dtype=str) if not df.empty: all_dfs.append(df) except: pass if all_dfs: combined_df = pd.concat(all_dfs, ignore_index=True) final_output = os.path.join(download_dir, final_filename) combined_df.to_excel(final_output, index=False) print(f"====================================================") print(f" 🎉 恭喜!韵达网点数据拉取合流大功告成!") print(f" 📁 最终存储位置: {final_output}") print(f"====================================================") for file_path in downloaded_files: os.remove(file_path) print(" ✅ 临时缓存阵列已无缝净化。") def yunda_actual_download(page): """韵达:实到货物数据下载""" print("\n▶ 开始执行【韵达 - 实到货物数据下载】任务...") print("🚧 韵达实到数据功能规划中...") pass