diff --git a/site_anneng.py b/site_anneng.py index 26e7d75..1eabfd4 100644 --- a/site_anneng.py +++ b/site_anneng.py @@ -783,6 +783,12 @@ def poll_and_download_tasks(export_cdp, export_times, download_dir): print(f" 已下载 [{idx}/{len(matched)}]: downloads/{save_name} ({fname})") time.sleep(0.3) + # 汇总校验:下载成功数必须等于本批提交的任务数,否则判失败 + if len(downloaded_files) < total_expected: + raise RuntimeError( + f"仅成功下载 {len(downloaded_files)}/{total_expected} 个任务,数据不完整" + ) + return downloaded_files @@ -821,6 +827,15 @@ def merge_and_cleanup(downloaded_files, download_dir): # ==================================================================== +def _remove_if_exists(path): + """删除文件(若存在):流程开头清理上次的最终文件,避免无数据/失败时残留旧数据。""" + try: + if os.path.exists(path): + os.remove(path) + except Exception: + pass + + def _load_query_days(): """从 config.yaml 读 anneng.query_days,默认 1。""" days = 1 @@ -846,6 +861,9 @@ def anneng_expected_download_impl(): download_dir = DOWNLOAD_DIR os.makedirs(download_dir, exist_ok=True) + # 清理上次的最终文件,避免本次无数据/失败时残留旧数据误导比对 + _remove_if_exists(os.path.join(download_dir, FINAL_FILENAME)) + query_days = _load_query_days() today = datetime.now() start = today - timedelta(days=(query_days - 1)) @@ -925,6 +943,7 @@ def anneng_expected_download_impl(): time.sleep(0.5) merge_and_cleanup(downloaded, download_dir) print("✅ 安能应到数据下载流程完成。") + return True except Exception as e: print(f"\n❌ 任务执行过程中发生异常: {e}") @@ -1147,6 +1166,9 @@ def anneng_actual_download_impl(): download_dir = DOWNLOAD_DIR os.makedirs(download_dir, exist_ok=True) + # 清理上次的最终文件,避免本次无数据/失败时残留旧数据误导比对 + _remove_if_exists(os.path.join(download_dir, ACTUAL_FINAL_FILENAME)) + days = _load_query_days() today = datetime.now() start = today - timedelta(days=days - 1) @@ -1183,8 +1205,9 @@ def anneng_actual_download_impl(): print(f" · 总共 {total} 条") if total == 0: print(">> ⚠ 无扫描数据,结束。") - _save_actual([], download_dir) - return + # 无数据:删除残留的旧最终文件(不落空文件),让比对层跳过本站 + _remove_if_exists(os.path.join(download_dir, ACTUAL_FINAL_FILENAME)) + return True # 5) 改每页 500 并重新查询 print(">> 设置每页 500 条 …") set_page_size_500(cdp) @@ -1203,6 +1226,8 @@ def anneng_actual_download_impl(): page_no += 1 print(f">> 共抽取 {len(all_rows)} 条扫描记录") + if total > 0 and not all_rows: + raise RuntimeError(f"扫描总数 {total} 但抽取到 0 条记录,翻页抽取异常") _save_actual(all_rows, download_dir) print("✅ 安能实到数据下载流程完成。") finally: @@ -1210,6 +1235,7 @@ def anneng_actual_download_impl(): # 7) 关闭扫描查询 tab(用完即关) print(">> 关闭【网点到件扫描查询(新)】tab …") close_tab_by_label(main_cdp, "网点到件扫描查询(新)") + return True except Exception as e: print(f"\n❌ 任务执行过程中发生异常: {e}") return False diff --git a/site_baishi.py b/site_baishi.py index 168472a..b444dfc 100644 --- a/site_baishi.py +++ b/site_baishi.py @@ -45,6 +45,15 @@ def baishi_reset(page): page.wait_for_timeout(1500) +def _remove_if_exists(path): + """删除文件(若存在):流程开头清理上次的最终文件,避免无数据/失败时残留旧数据。""" + try: + if os.path.exists(path): + os.remove(path) + except Exception: + pass + + def _close_tab(page, tab_name): """关闭指定名称的百世标签页。 @@ -83,6 +92,9 @@ def baishi_download_undelivered_data_impl(page): if not os.path.exists(download_dir): os.makedirs(download_dir) + # 清理上次的最终文件,避免本次无数据/失败时残留旧数据误导比对 + _remove_if_exists(os.path.join(download_dir, "百世-应到未到货物数据.xlsx")) + try: # 1. 导航与页面加载 print(">> 正在进入【扫描综合查询】界面...") @@ -111,7 +123,7 @@ def baishi_download_undelivered_data_impl(page): ) # 数据为空时,提前结束前也需清理环境 _close_tab(page, "扫描综合查询") - return + return True # 有未扫数据,继续点击操作 target_cell.locator("a").click() diff --git a/site_shunxin.py b/site_shunxin.py index e93dfc5..5def303 100644 --- a/site_shunxin.py +++ b/site_shunxin.py @@ -76,6 +76,15 @@ def _sanitize_for_filename(name): return re.sub(r'[\\/:*?"<>|]', "", str(name)).strip() +def _remove_if_exists(path): + """删除文件(若存在):清理上次的本账号中间文件/最终文件,避免残留旧数据。""" + try: + if os.path.exists(path): + os.remove(path) + except Exception: + pass + + def shunxin_belonging(page): """读取顺心当前账号的归属网点名(仅在首页可见,须在导航离开首页前调用)。 @@ -118,7 +127,8 @@ def shunxin_merge_final(kind, tags): print(f" ⚠️ 读取中间文件 {mid_name} 失败: {e}") if not frames: - print(f">> ⚠️ 所有归属地均无{kind}数据,未生成 {final_name}。") + print(f">> ⚠️ 所有归属地均无{kind}数据,删除残留的 {final_name}(不写空表)。") + _remove_if_exists(final_path) return combined = pd.concat(frames, ignore_index=True) @@ -182,6 +192,12 @@ def shunxin_expected_download_impl(page, out_tag=""): os.makedirs(download_dir) print(f">> 已创建下载目录: {download_dir}") + # 清理上次本账号的中间文件,避免本次无数据/失败时残留旧数据被 merge_final 误读 + _mid_suffix = f"-{out_tag}" if out_tag else "" + _remove_if_exists( + os.path.join(download_dir, f"顺心{_mid_suffix}-应到货物数据.xlsx") + ) + export_times = [] target_task_timestamps = [] @@ -420,6 +436,12 @@ def shunxin_expected_download_impl(page, out_tag=""): except Exception as e: print(f" ❌ 下载任务 [{time_str}] 失败: {e}") + # 汇总校验:下载成功数必须等于目标任务数,否则判失败 + if len(downloaded_files) < len(target_task_timestamps): + raise RuntimeError( + f"仅成功下载 {len(downloaded_files)}/{len(target_task_timestamps)} 个任务,数据不完整" + ) + # 9. 合并数据 if downloaded_files: print("\n>> 正在合并下载的数据...") @@ -499,6 +521,12 @@ def shunxin_actual_download_impl(page, out_tag=""): if not os.path.exists(download_dir): os.makedirs(download_dir) + # 清理上次本账号的中间文件,避免本次无数据/失败时残留旧数据被 merge_final 误读 + _mid_suffix = f"-{out_tag}" if out_tag else "" + _remove_if_exists( + os.path.join(download_dir, f"顺心{_mid_suffix}-实到货物数据.xlsx") + ) + export_times = [] target_task_timestamps = [] @@ -710,6 +738,12 @@ def shunxin_actual_download_impl(page, out_tag=""): except Exception as e: print(f" ❌ 下载任务 [{time_str}] 失败: {e}") + # 汇总校验:下载成功数必须等于目标任务数,否则判失败 + if len(downloaded_files) < len(target_task_timestamps): + raise RuntimeError( + f"仅成功下载 {len(downloaded_files)}/{len(target_task_timestamps)} 个任务,数据不完整" + ) + # 8. 合并数据 if downloaded_files: print("\n>> 正在合并下载的数据...") diff --git a/site_yunda.py b/site_yunda.py index 8186a1b..4e8f7ed 100644 --- a/site_yunda.py +++ b/site_yunda.py @@ -48,6 +48,15 @@ def yunda_reset(page): page.wait_for_timeout(1500) +def _remove_if_exists(path): + """删除文件(若存在):流程开头清理上次的最终文件,避免无数据/失败时残留旧数据。""" + try: + if os.path.exists(path): + os.remove(path) + except Exception: + pass + + def yunda_login(page): """韵达自动登录:未登录则填充表单并提交,已登录则跳过。""" print(">> 正在检查韵达登录状态...") @@ -136,6 +145,9 @@ def yunda_expected_download_impl(page): if not os.path.exists(download_dir): os.makedirs(download_dir) + # 清理上次的最终文件,避免本次无数据/失败时残留旧数据误导比对 + _remove_if_exists(os.path.join(download_dir, "韵达-应到货物数据.xlsx")) + export_times = [] try: @@ -361,6 +373,7 @@ def yunda_expected_download_impl(page): download_dir, "韵达-应到货物数据.xlsx", ) + return True except Exception as e: print(f"\n❌ 任务执行过程中发生异常: {e}") @@ -386,6 +399,9 @@ def yunda_actual_download_impl(page): if not os.path.exists(download_dir): os.makedirs(download_dir) + # 清理上次的最终文件,避免本次无数据/失败时残留旧数据误导比对 + _remove_if_exists(os.path.join(download_dir, "韵达-实到货物数据.xlsx")) + export_times = [] try: @@ -582,6 +598,7 @@ def yunda_actual_download_impl(page): download_dir, "韵达-实到货物数据.xlsx", ) + return True except Exception as e: print(f"\n❌ 任务执行过程中发生异常: {e}") @@ -686,13 +703,19 @@ def _yunda_poll_and_download_tasks(page, export_times, download_dir, final_filen except Exception as e: print(f" ❌ 下载失败: {e}") + # 汇总校验:下载成功数必须等于本批提交的任务数,否则判失败 + if len(downloaded_files) < total_expected: + raise RuntimeError( + f"仅成功下载 {len(downloaded_files)}/{total_expected} 个任务,数据不完整" + ) + print(">> 【导出服务】下载完成,正在关闭标签页...") try: page.locator(".tags-view-item", has_text="导出服务").locator( ".el-icon-close" ).click() print(" ✅ 【导出服务】标签页已关闭。") - except: + except Exception: pass if downloaded_files: @@ -703,7 +726,7 @@ def _yunda_poll_and_download_tasks(page, export_times, download_dir, final_filen df = pd.read_excel(file_path, dtype=str) if not df.empty: all_dfs.append(df) - except: + except Exception: pass if all_dfs: diff --git a/site_zto.py b/site_zto.py index a070132..d823a45 100644 --- a/site_zto.py +++ b/site_zto.py @@ -48,6 +48,15 @@ def zto_reset(page): page.wait_for_timeout(1500) +def _remove_if_exists(path): + """删除文件(若存在):流程开头清理上次的最终文件,避免无数据/失败时残留旧数据。""" + try: + if os.path.exists(path): + os.remove(path) + except Exception: + pass + + def _wait_and_get_frame(page, text_indicator, timeout_ms=20000): """在主页面与所有 iframe 中查找包含指定文本的窗口""" start_time = datetime.now() @@ -116,6 +125,9 @@ def zto_expected_download_impl(page): if not os.path.exists(download_dir): os.makedirs(download_dir) + # 清理上次的最终文件,避免本次无数据/失败时残留旧数据误导比对 + _remove_if_exists(os.path.join(download_dir, "中通-应到货物数据.xlsx")) + export_times = [] try: @@ -213,14 +225,14 @@ def zto_expected_download_impl(page): pass return else: - print(" ⚠️ 票数为 0 但未出现空记录提示,终止流程。") + print(" ⚠️ 票数为 0 但未出现空记录提示,页面状态异常,判失败。") try: page.locator(".mini-tab", has_text="进站交接单查询").locator( ".mini-tab-close" ).click() except Exception: pass - return + raise RuntimeError("票数为 0 但未出现空记录提示,页面状态异常") else: print(" >> 票数校验通过,等待主表格渲染数据行...") ewb_frame.locator( @@ -272,16 +284,22 @@ def zto_expected_download_impl(page): print(" ✅ 已确认【温馨提示】弹窗。") print(" >> 正在等待服务器建立后台离线任务...") + # 提示「生成离线导出任务成功」出现在导出列 iframe(/comm/download) 中; + # 提交完成后该 iframe 会被站点销毁,此时 wait_for 会抛 "Frame was detached" + # ——这恰恰说明提示已随 iframe 消失、任务已建立,属正常,不视为失败。 + ctx_tips = _wait_and_get_frame( + page, "生成离线导出任务成功", timeout_ms=10000 + ) 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(" ✅ 成功提示框已关闭。") + except Exception as e: + if "detached" in str(e).lower(): + print(" ℹ️ 提示框所在 iframe 已随提交关闭,任务已建立。") + else: + raise + print(" ✅ 成功提示框已消失。") export_times.append(datetime.now()) @@ -309,6 +327,7 @@ def zto_expected_download_impl(page): download_dir, "中通-应到货物数据.xlsx", ) + return True except Exception as e: print(f"\n❌ 任务执行过程中发生异常: {e}") @@ -334,6 +353,9 @@ def zto_actual_download_impl(page): if not os.path.exists(download_dir): os.makedirs(download_dir) + # 清理上次的最终文件,避免本次无数据/失败时残留旧数据误导比对 + _remove_if_exists(os.path.join(download_dir, "中通-实到货物数据.xlsx")) + export_times = [] try: @@ -444,16 +466,20 @@ def zto_actual_download_impl(page): print(" ✅ 已确认【温馨提示】弹窗。") print(" >> 正在等待服务器建立后台离线任务...") + # 提示「生成离线导出任务成功」出现在导出列 iframe(/comm/download) 中; + # 提交完成后该 iframe 会被站点销毁,此时 wait_for 会抛 "Frame was detached" + # ——这恰恰说明提示已随 iframe 消失、任务已建立,属正常,不视为失败。 + ctx_tips = _wait_and_get_frame(page, "生成离线导出任务成功", timeout_ms=10000) 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(" ✅ 成功提示框已关闭。") + except Exception as e: + if "detached" in str(e).lower(): + print(" ℹ️ 提示框所在 iframe 已随提交关闭,任务已建立。") + else: + raise + print(" ✅ 成功提示框已消失。") export_times.append(datetime.now()) @@ -477,6 +503,7 @@ def zto_actual_download_impl(page): download_dir, "中通-实到货物数据.xlsx", ) + return True except Exception as e: print(f"\n❌ 任务执行过程中发生异常: {e}") @@ -500,6 +527,8 @@ def _zto_poll_and_download_tasks(page, export_times, download_dir, final_filenam poll_deadline = ( time.monotonic() + 300 ) # 5 分钟上限:任务卡死/匹配不上时超时失败,交由上层重置重试 + last_total_found = -1 + stall_rounds = 0 # 连续无进展轮数:刷新后 total_found 不增长则累计,超阈值快速失败 while True: if time.monotonic() > poll_deadline: raise RuntimeError( @@ -543,14 +572,31 @@ def _zto_poll_and_download_tasks(page, export_times, download_dir, final_filenam ) if total_found < total_expected or len(processing_timestamps) > 0: + # 连续无进展即快速失败:避免页面被遮挡/任务卡住时干等到 5 分钟超时 + if total_found == last_total_found: + stall_rounds += 1 + else: + stall_rounds = 0 + last_total_found = total_found + if stall_rounds >= 4: + raise RuntimeError( + f"连续 {stall_rounds} 轮刷新无进展(仍 {total_found}/{total_expected})," + "疑似页面被遮挡或任务异常,触发重试" + ) print(" ⏳ 任务尚未齐全或仍在生成,点击查询刷新...") + # 查询按钮加短超时;失败则菜单刷新,两者都失败直接报错触发重试 try: taskdone_frame.locator( ".mini-button-text", has_text="查询" - ).first.click() + ).first.click(timeout=8000) except Exception as e: - print(f" ⚠️ 查询按钮不可用,改用菜单刷新: {e}") - page.locator("li.leaf span.menu-name", has_text="导出任务管理").click() + print(f" ⚠️ 查询按钮不可用({e}),改用菜单刷新...") + try: + page.locator( + "li.leaf span.menu-name", has_text="导出任务管理" + ).click(timeout=8000) + except Exception as e2: + raise RuntimeError(f"查询与菜单刷新均失败,疑似页面被遮挡: {e2}") page.wait_for_timeout(3000) else: @@ -598,6 +644,13 @@ def _zto_poll_and_download_tasks(page, export_times, download_dir, final_filenam except Exception as e: print(f" ❌ 下载任务 [{time_str}] 失败: {e}") + # 汇总校验:下载成功数必须等于本批提交的任务数,否则判失败 + # (防"部分/全失败却被判成功") + if len(downloaded_files) < total_expected: + raise RuntimeError( + f"仅成功下载 {len(downloaded_files)}/{total_expected} 个任务,数据不完整" + ) + # ==================================================================== # 所有目标文件下载完成后,关闭“导出任务管理”标签页 # ====================================================================