# site_yunda.py import os import re import yaml from datetime import datetime, timedelta import pandas as pd from paths import DOWNLOAD_DIR, CONFIG_PATH def yunda_login(page): """韵达自动登录:未登录则填充表单并提交,已登录则跳过。""" print(">> 正在检查韵达登录状态...") try: # 定位“账号密码登录”切换按钮 switch_btn = page.locator("span", has_text="账号密码登录") # 5 秒内若出现该按钮,说明当前未登录 if switch_btn.is_visible(timeout=5000): print(" -> 检测到未登录界面,正在切换到【账号密码登录】...") switch_btn.click() page.wait_for_timeout(500) # 从配置读取凭证(默认空串;真实凭据仅存于被忽略的 config.yaml) username = "" password = "" if os.path.exists(CONFIG_PATH): with open(CONFIG_PATH, "r", encoding="utf-8") as f: config = yaml.safe_load(f) or {} yd_cfg = config.get("yunda", {}) username = str(yd_cfg.get("username", "")) password = str(yd_cfg.get("password", "")) print(f" -> 正在填充登录表单 (账号: {username})...") page.locator("#username").fill(username) page.locator("#password").fill(password) page.wait_for_timeout(300) print(" -> 正在点击【登录】按钮并提交表单...") page.locator('button[type="submit"]', has_text="登录").click() page.wait_for_timeout(1000) else: print( " -> 未发现登录按钮,判定为已登录,跳过。" ) except Exception as e: print(f" ⚠️ 登录检测出错(可能已在工作台内): {e}") def yunda_smart_menu_click(page, menu_path): """韵达多级菜单导航:展开父级菜单并点击目标项(已展开则跳过,避免误折叠)。""" print(f">> 导航韵达菜单: {' -> '.join(menu_path)}") for item in menu_path: title_locator = page.locator( f"xpath=//div[contains(@class, 'el-submenu__title') and .//span[normalize-space(.)='{item}']]" ).first parent_li = page.locator( f"xpath=//div[contains(@class, 'el-submenu__title') and .//span[normalize-space(.)='{item}']]/.." ).first leaf_locator = page.locator( f"xpath=//li[contains(@class, 'el-menu-item') and .//span[normalize-space(.)='{item}']]" ).first if title_locator.is_visible(): current_class = parent_li.get_attribute("class") or "" is_opened = "is-opened" in current_class if not is_opened: print(f" -> 父菜单 [{item}] 处于收起状态,点击展开") title_locator.click() page.wait_for_timeout(500) else: print( f" -> 父菜单 [{item}] 已展开,跳过点击" ) elif leaf_locator.is_visible(): print(f" -> 点击菜单项 [{item}]") leaf_locator.click() page.wait_for_timeout(1000) def yunda_expected_download(page): """韵达:应到货物数据下载""" print("\n▶ 开始执行【韵达 - 应到货物数据下载】任务...") target_task_title = "进站主单表" download_dir = DOWNLOAD_DIR 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(">> 正在定位【进站交接单查询】iframe...") # 使用 frame_locator 定位业务 iframe ws_frame = page.frame_locator("section iframe") ws_frame.locator("#startTime").wait_for(state="attached", timeout=15000) print("✅ 进站交接单查询页面就绪") # 2. 从配置文件中解析并计算绝对日期跨度 query_days = 1 try: if os.path.exists(CONFIG_PATH): with open(CONFIG_PATH, "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(800) # ==================================================================== # 将 Loading 蒙层与数据判断限制在 #tab-1 内 # 避免多标签页命中多个元素 (Strict Mode) # ==================================================================== loading_mask = ws_frame.locator( "#tab-1 .fixed-table-loading", has_text="正在努力地加载数据中" ).first if loading_mask.is_visible(): print(" ⏳ 检测到数据加载遮罩,等待加载完成...") loading_mask.wait_for(state="hidden", timeout=30000) page.wait_for_timeout(500) sum_panel = ws_frame.locator("#sum").first 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: # 空数据提示同样限制在当前 tab 内 if ws_frame.locator( "#tab-1 .no-records-found", has_text="没有找到匹配的记录" ).first.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() # ==================================================================== # 跳过已绑定的交接单 # ==================================================================== bind_status = current_row.locator("td").nth(2).inner_text().strip() print(f" -> 交接单号: {raw_no} [绑定状态: {bind_status}]") if bind_status == "已绑定": print(" ⏭️ 该交接单已绑定,跳过。") continue # ==================================================================== 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() confirm_link = export_frame.get_by_role("link", name="确定") try: confirm_link.wait_for(state="visible", timeout=6000) if export_frame.get_by_text("导出任务建立成功").is_visible(): print(" ✅ 已确认导出任务建立成功。") confirm_link.click() task_success = True break elif export_frame.get_by_text( "请选择格式相应的导出字段" ).is_visible(): print( " ⚠️ 检测到未选择字段,重新点击全选..." ) confirm_link.click() page.wait_for_timeout(500) export_frame.locator(".allRight").click() page.wait_for_timeout(500) else: confirm_link.click() page.wait_for_timeout(1000) except Exception: page.wait_for_timeout(1000) if not task_success: raise RuntimeError( "连续 5 次尝试均未能建立应到数据离线任务。" ) 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()) print(">> 任务提交完成,正在关闭【进站交接单查询】标签页...") page.locator(".tags-view-item", has_text="进站交接单查询").locator( ".el-icon-close" ).click() page.wait_for_timeout(500) # 若所有记录都被跳过,export_times 为空,直接结束 if not export_times: print( ">> ⚠️ 本次未产生任何离线下载任务(无数据或已全部跳过),结束。" ) return _yunda_poll_and_download_tasks( page, export_times, target_task_title, download_dir, "韵达-应到货物数据.xlsx", ) except Exception as e: print(f"\n❌ 任务执行过程中发生异常: {e}") return False def yunda_actual_download(page): """韵达:实到货物数据下载""" print("\n▶ 开始执行【韵达 - 实到货物数据下载】任务...") target_task_title = "扫描记录数据" download_dir = DOWNLOAD_DIR if not os.path.exists(download_dir): os.makedirs(download_dir) export_times = [] try: page.locator(".el-menu-item", has_text="首页").wait_for( state="visible", timeout=15000 ) print("✅ 韵达工作台首页已加载") yunda_smart_menu_click(page, ["报表管理", "扫描记录查询"]) print(">> 正在定位【扫描记录查询】iframe...") ws_frame = page.frame_locator("section iframe") ws_frame.locator( ".no-records-found", has_text="没有找到匹配的记录" ).first.wait_for(state="visible", timeout=15000) print("✅ 扫描记录查询页面已初始化") query_days = 1 try: if os.path.exists(CONFIG_PATH): with open(CONFIG_PATH, "r", encoding="utf-8") as f: config = yaml.safe_load(f) or {} query_days = int(config.get("yunda", {}).get("query_days", 1)) except Exception: pass today = datetime.now() start_date = today - timedelta(days=(query_days - 1)) print(f">> 设置实到查询时间范围: [近 {query_days} 天]") print(" >> 正在设定起始时间...") ws_frame.locator("#startDate").click() page.wait_for_timeout(400) box1 = ws_frame.locator("#laydate_box:visible").first box1.locator( f"td[y='{start_date.year}'][m='{start_date.month}'][d='{start_date.day}']" ).click() page.wait_for_timeout(400) print(" >> 正在设定截止时间...") ws_frame.locator("#endDate").click() page.wait_for_timeout(400) box2 = ws_frame.locator("#laydate_box:visible").first box2.locator( f"td[y='{today.year}'][m='{today.month}'][d='{today.day}']" ).click() page.wait_for_timeout(500) print(" >> 正在变更扫描类型为【到件】...") ws_frame.locator("#scanRecordTyp").select_option(value="03") page.wait_for_timeout(500) print(">> 正在执行查询...") ws_frame.locator('input[type="button"][value="查询"]').click() page.wait_for_timeout(800) loading_mask = ws_frame.locator( ".fixed-table-loading", has_text="正在努力地加载数据中" ).first if loading_mask.is_visible(): print(" ⏳ 检测到数据加载遮罩,等待加载完成...") loading_mask.wait_for(state="hidden", timeout=30000) page.wait_for_timeout(500) pg_info = ws_frame.locator(".pagination-info").first has_records = False if pg_info.is_visible(): info_text = pg_info.inner_text() match_total = re.search(r"总共\s*(\d+)\s*条记录", info_text) if match_total and int(match_total.group(1)) > 0: has_records = True print( f" ✅ 实到数据已加载,总记录数: [{match_total.group(1)}] 条。" ) if not has_records: if ws_frame.locator( ".no-records-found", has_text="没有找到匹配的记录" ).first.is_visible(): print( " ⚠️ 当前查询范围内为空数据,终止并关闭标签页。" ) page.locator(".tags-view-item", has_text="扫描记录查询").locator( ".el-icon-close" ).click() return else: print(" ⚠️ 未找到数据,也未出现空数据提示,结束。") page.locator(".tags-view-item", has_text="扫描记录查询").locator( ".el-icon-close" ).click() return # 5. 执行导出流 print(">> 正在发起导出...") ws_frame.locator('input[type="button"][id="export"]').click() ws_frame.locator(".layui-layer-title", has_text="数据导出").wait_for( state="visible", timeout=15000 ) export_frame = ws_frame.frame_locator('iframe[name="myFrame"]') 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() confirm_link = export_frame.get_by_role("link", name="确定") try: confirm_link.wait_for(state="visible", timeout=6000) if export_frame.get_by_text("导出任务建立成功").is_visible(): print(" ✅ 已确认导出任务建立成功。") confirm_link.click() task_success = True break elif export_frame.get_by_text("请选择格式相应的导出字段").is_visible(): print(" ⚠️ 检测到字段未全选,重新点击全选...") confirm_link.click() page.wait_for_timeout(500) export_frame.locator(".allRight").click() page.wait_for_timeout(500) else: confirm_link.click() page.wait_for_timeout(1000) except Exception: page.wait_for_timeout(1000) if not task_success: raise RuntimeError( "连续 5 次尝试均未能建立实到数据离线任务。" ) ws_frame.locator(".layui-layer-close1").click() page.wait_for_timeout(500) export_times.append(datetime.now()) print(">> 任务提交完成,正在关闭【扫描记录查询】标签页...") page.locator(".tags-view-item", has_text="扫描记录查询").locator( ".el-icon-close" ).click() page.wait_for_timeout(500) # 防线:双重保护 if not export_times: print(">> ⚠️ 本次未产生任何离线下载任务,结束。") return # 6. 轮询并下载 _yunda_poll_and_download_tasks( page, export_times, target_task_title, download_dir, "韵达-实到货物数据.xlsx", ) except Exception as e: print(f"\n❌ 任务执行过程中发生异常: {e}") return False 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(">> 开始轮询离线文件队列(定时刷新直到任务齐全)...") 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 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}") print(">> 【导出服务】下载完成,正在关闭标签页...") try: page.locator(".tags-view-item", has_text="导出服务").locator( ".el-icon-close" ).click() print(" ✅ 【导出服务】标签页已关闭。") except Exception: pass 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 Exception: 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(" 临时文件已清理。")