Centralize paths in paths.py and harden waybill-number handling

- Add paths.py: BASE_DIR/DOWNLOAD_DIR/CONFIG_PATH anchored on __file__
  so paths resolve regardless of the launch cwd
- Route main_router and all site modules through DOWNLOAD_DIR/CONFIG_PATH,
  replacing os.getcwd()-based download dirs and the "config.yaml" literal
- main_router compare engine: read Excel as str with keep_default_na,
  strip/normalize 运单号, drop blanks, and isin against a set so
  int/float-vs-str mismatches no longer produce false "undelivered"
- Shunxin: give downloaded files unique microsecond temp names and read
  Excel as str to preserve long-waybill precision
- Normalize bare except: to except Exception: across affected files

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Misaka
2026-06-20 22:28:30 +08:00
parent 5d13c13f40
commit ab3f25a10e
6 changed files with 79 additions and 39 deletions

View File

@@ -5,6 +5,8 @@ import yaml
import pandas as pd import pandas as pd
from playwright.sync_api import sync_playwright from playwright.sync_api import sync_playwright
from paths import DOWNLOAD_DIR, CONFIG_PATH
# 导入抽离出去的各个网点模块 # 导入抽离出去的各个网点模块
import site_shunxin import site_shunxin
import site_baishi import site_baishi
@@ -34,7 +36,7 @@ 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 = DOWNLOAD_DIR
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")
@@ -49,8 +51,9 @@ def task_process_undelivered_data(site_name="顺心"):
try: try:
print(">> 正在载入本地 Excel 文档...") print(">> 正在载入本地 Excel 文档...")
df_expected = pd.read_excel(expected_path) # 全部按字符串读取并保留空串:避免 18 位运单号被当成浮点数丢精度,也避免空单元格变成 NaN。
df_actual = pd.read_excel(actual_path) df_expected = pd.read_excel(expected_path, dtype=str, keep_default_na=False)
df_actual = pd.read_excel(actual_path, dtype=str, keep_default_na=False)
if "运单号" not in df_expected.columns or "运单号" not in df_actual.columns: if "运单号" not in df_expected.columns or "运单号" not in df_actual.columns:
print("❌ 核心资产校验失败:数据源中缺失【运单号】字段,请检查导出配置。") print("❌ 核心资产校验失败:数据源中缺失【运单号】字段,请检查导出配置。")
@@ -58,8 +61,16 @@ def task_process_undelivered_data(site_name="顺心"):
print(">> 正在启动多维数据集比对引擎...") print(">> 正在启动多维数据集比对引擎...")
# 统一运单号为去空白字符串,消除 int/float 与 str 混读导致 isin 永不命中的隐患
df_expected["运单号"] = df_expected["运单号"].astype(str).str.strip()
df_actual["运单号"] = df_actual["运单号"].astype(str).str.strip()
# 剔除空白运单号,避免空值被误判为“应到未到”
df_expected = df_expected[df_expected["运单号"] != ""]
actual_set = set(df_actual["运单号"]) - {""}
# Left Anti-Join在应到中找出不存在于实到里的运单号 # Left Anti-Join在应到中找出不存在于实到里的运单号
df_undelivered = df_expected[~df_expected["运单号"].isin(df_actual["运单号"])] df_undelivered = df_expected[~df_expected["运单号"].isin(actual_set)]
target_columns = ["班次号", "交接单号", "运单号"] target_columns = ["班次号", "交接单号", "运单号"]
available_columns = [ available_columns = [
@@ -86,8 +97,8 @@ def run_multi_site_daemon():
debug_mode = False debug_mode = False
debug_target = "" debug_target = ""
try: try:
if os.path.exists("config.yaml"): if os.path.exists(CONFIG_PATH):
with open("config.yaml", "r", encoding="utf-8") as f: with open(CONFIG_PATH, "r", encoding="utf-8") as f:
config = yaml.safe_load(f) config = yaml.safe_load(f)
debug_mode = config.get("debug", {}).get("enabled", False) debug_mode = config.get("debug", {}).get("enabled", False)
debug_target = config.get("debug", {}).get("target_site", "") debug_target = config.get("debug", {}).get("target_site", "")
@@ -191,7 +202,7 @@ def run_multi_site_daemon():
if read_btns.nth(i).is_visible(timeout=500): if read_btns.nth(i).is_visible(timeout=500):
read_btns.nth(i).click() read_btns.nth(i).click()
handled_any = True handled_any = True
except: except Exception:
pass pass
try: try:
if bs_page.locator("button:has-text('关 闭')").is_visible( if bs_page.locator("button:has-text('关 闭')").is_visible(
@@ -199,7 +210,7 @@ def run_multi_site_daemon():
): ):
bs_page.locator("button:has-text('关 闭')").click() bs_page.locator("button:has-text('关 闭')").click()
handled_any = True handled_any = True
except: except Exception:
pass pass
if not handled_any: if not handled_any:
break break

15
paths.py Normal file
View File

@@ -0,0 +1,15 @@
# paths.py
# 统一的路径锚点:所有路径都以本项目所在目录为基准,避免依赖运行时的工作目录(cwd)。
# 这样无论从哪个目录启动脚本IDE / 命令行 / 计划任务 / 双击),
# 下载目录与配置文件都能稳定定位,不会出现“文件落到别处”或“读不到密码”的隐蔽故障。
import os
# 项目根目录(以本文件所在位置为基准,与从哪个目录启动脚本无关)
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
# 统一的下载 / 输出目录
DOWNLOAD_DIR = os.path.join(BASE_DIR, "downloads")
# 统一的配置文件路径注意config.yaml 需与本项目脚本放在同一目录下)
CONFIG_PATH = os.path.join(BASE_DIR, "config.yaml")

View File

@@ -4,12 +4,14 @@ import os
import yaml import yaml
from playwright.sync_api import sync_playwright from playwright.sync_api import sync_playwright
from paths import DOWNLOAD_DIR, CONFIG_PATH
def baishi_download_undelivered_data(page): def baishi_download_undelivered_data(page):
"""百世:一键提取应到未到(当日未扫)数据""" """百世:一键提取应到未到(当日未扫)数据"""
print("\n▶ 开始执行【百世 - 一键提取应到未到数据】任务...") print("\n▶ 开始执行【百世 - 一键提取应到未到数据】任务...")
download_dir = os.path.join(os.getcwd(), "downloads") download_dir = DOWNLOAD_DIR
if not os.path.exists(download_dir): if not os.path.exists(download_dir):
os.makedirs(download_dir) os.makedirs(download_dir)
@@ -60,7 +62,7 @@ def baishi_download_undelivered_data(page):
print(">> 正在读取本地配置文件并进行授权验证...") print(">> 正在读取本地配置文件并进行授权验证...")
password = "" password = ""
try: try:
with open("config.yaml", "r", encoding="utf-8") as f: with open(CONFIG_PATH, "r", encoding="utf-8") as f:
config = yaml.safe_load(f) config = yaml.safe_load(f)
password = config.get("baishi", {}).get("password", "") password = config.get("baishi", {}).get("password", "")

View File

@@ -5,6 +5,8 @@ import re
from datetime import datetime from datetime import datetime
import pandas as pd import pandas as pd
from paths import DOWNLOAD_DIR
def _close_tab(page, tab_name): def _close_tab(page, tab_name):
"""关闭指定名称的标签页Ant Design Tabs """关闭指定名称的标签页Ant Design Tabs
@@ -34,7 +36,7 @@ def shunxin_expected_download(page):
print("\n▶ 开始执行【顺心 - 应到货物数据下载】任务...") print("\n▶ 开始执行【顺心 - 应到货物数据下载】任务...")
# 初始化并创建下载目录 # 初始化并创建下载目录
download_dir = os.path.join(os.getcwd(), "downloads") download_dir = DOWNLOAD_DIR
if not os.path.exists(download_dir): if not os.path.exists(download_dir):
os.makedirs(download_dir) os.makedirs(download_dir)
print(f">> 已创建专属下载文件夹: {download_dir}") print(f">> 已创建专属下载文件夹: {download_dir}")
@@ -189,10 +191,13 @@ def shunxin_expected_download(page):
).click() ).click()
download = download_info.value download = download_info.value
save_path = os.path.join(download_dir, download.suggested_filename) # 用带微秒的时间戳生成唯一临时文件名,避免同名任务相互覆盖导致丢数据
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) download.save_as(save_path)
downloaded_files.append(save_path) downloaded_files.append(save_path)
print(f" ⬇️ 文件已落盘: downloads/{download.suggested_filename}") print(f" ⬇️ 文件已落盘: downloads/{custom_filename}")
except Exception as e: except Exception as e:
print(f" ❌ 下载任务 [{time_str}] 失败: {e}") print(f" ❌ 下载任务 [{time_str}] 失败: {e}")
@@ -202,7 +207,7 @@ def shunxin_expected_download(page):
all_data_frames = [] all_data_frames = []
for file_path in downloaded_files: for file_path in downloaded_files:
try: try:
df = pd.read_excel(file_path) df = pd.read_excel(file_path, dtype=str)
if not df.empty: if not df.empty:
all_data_frames.append(df) all_data_frames.append(df)
except Exception as e: except Exception as e:
@@ -233,7 +238,7 @@ def shunxin_actual_download(page):
"""顺心:实到货物数据下载""" """顺心:实到货物数据下载"""
print("\n▶ 开始执行【顺心 - 实到货物数据下载】任务...") print("\n▶ 开始执行【顺心 - 实到货物数据下载】任务...")
download_dir = os.path.join(os.getcwd(), "downloads") download_dir = DOWNLOAD_DIR
if not os.path.exists(download_dir): if not os.path.exists(download_dir):
os.makedirs(download_dir) os.makedirs(download_dir)
@@ -358,10 +363,13 @@ def shunxin_actual_download(page):
).click() ).click()
download = download_info.value download = download_info.value
save_path = os.path.join(download_dir, download.suggested_filename) # 用带微秒的时间戳生成唯一临时文件名,避免同名任务相互覆盖导致丢数据
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) download.save_as(save_path)
downloaded_files.append(save_path) downloaded_files.append(save_path)
print(f" ⬇️ 文件已落盘: downloads/{download.suggested_filename}") print(f" ⬇️ 文件已落盘: downloads/{custom_filename}")
except Exception as e: except Exception as e:
print(f" ❌ 下载任务 [{time_str}] 失败: {e}") print(f" ❌ 下载任务 [{time_str}] 失败: {e}")
@@ -371,7 +379,7 @@ def shunxin_actual_download(page):
all_data_frames = [] all_data_frames = []
for file_path in downloaded_files: for file_path in downloaded_files:
try: try:
df = pd.read_excel(file_path) df = pd.read_excel(file_path, dtype=str)
if not df.empty: if not df.empty:
all_data_frames.append(df) all_data_frames.append(df)
except Exception as e: except Exception as e:

View File

@@ -6,6 +6,8 @@ import yaml
from datetime import datetime, timedelta from datetime import datetime, timedelta
import pandas as pd import pandas as pd
from paths import DOWNLOAD_DIR, CONFIG_PATH
def yunda_login(page): def yunda_login(page):
""" """
@@ -26,8 +28,8 @@ def yunda_login(page):
# 从配置读取凭证(默认空串;真实凭据仅存于被忽略的 config.yaml # 从配置读取凭证(默认空串;真实凭据仅存于被忽略的 config.yaml
username = "" username = ""
password = "" password = ""
if os.path.exists("config.yaml"): if os.path.exists(CONFIG_PATH):
with open("config.yaml", "r", encoding="utf-8") as f: with open(CONFIG_PATH, "r", encoding="utf-8") as f:
config = yaml.safe_load(f) or {} config = yaml.safe_load(f) or {}
yd_cfg = config.get("yunda", {}) yd_cfg = config.get("yunda", {})
username = str(yd_cfg.get("username", "")) username = str(yd_cfg.get("username", ""))
@@ -91,7 +93,7 @@ def yunda_expected_download(page):
print("\n▶ 开始执行【韵达 - 应到货物数据下载】任务...") print("\n▶ 开始执行【韵达 - 应到货物数据下载】任务...")
target_task_title = "进站主单表" target_task_title = "进站主单表"
download_dir = os.path.join(os.getcwd(), "downloads") download_dir = DOWNLOAD_DIR
if not os.path.exists(download_dir): if not os.path.exists(download_dir):
os.makedirs(download_dir) os.makedirs(download_dir)
@@ -116,8 +118,8 @@ def yunda_expected_download(page):
# 2. 从配置文件中解析并计算绝对日期跨度 # 2. 从配置文件中解析并计算绝对日期跨度
query_days = 1 query_days = 1
try: try:
if os.path.exists("config.yaml"): if os.path.exists(CONFIG_PATH):
with open("config.yaml", "r", encoding="utf-8") as f: with open(CONFIG_PATH, "r", encoding="utf-8") as f:
config = yaml.safe_load(f) or {} config = yaml.safe_load(f) or {}
query_days = int(config.get("yunda", {}).get("query_days", 1)) query_days = int(config.get("yunda", {}).get("query_days", 1))
except Exception as e: except Exception as e:
@@ -307,7 +309,7 @@ def yunda_actual_download(page):
print("\n▶ 开始执行【韵达 - 实到货物数据下载】任务...") print("\n▶ 开始执行【韵达 - 实到货物数据下载】任务...")
target_task_title = "扫描记录数据" target_task_title = "扫描记录数据"
download_dir = os.path.join(os.getcwd(), "downloads") download_dir = DOWNLOAD_DIR
if not os.path.exists(download_dir): if not os.path.exists(download_dir):
os.makedirs(download_dir) os.makedirs(download_dir)
@@ -331,8 +333,8 @@ def yunda_actual_download(page):
query_days = 1 query_days = 1
try: try:
if os.path.exists("config.yaml"): if os.path.exists(CONFIG_PATH):
with open("config.yaml", "r", encoding="utf-8") as f: with open(CONFIG_PATH, "r", encoding="utf-8") as f:
config = yaml.safe_load(f) or {} config = yaml.safe_load(f) or {}
query_days = int(config.get("yunda", {}).get("query_days", 1)) query_days = int(config.get("yunda", {}).get("query_days", 1))
except Exception: except Exception:
@@ -578,7 +580,7 @@ def _yunda_poll_and_download_tasks(
".el-icon-close" ".el-icon-close"
).click() ).click()
print(" ✅ 【导出服务】工作区已安全关闭。") print(" ✅ 【导出服务】工作区已安全关闭。")
except: except Exception:
pass pass
if downloaded_files: if downloaded_files:
@@ -589,7 +591,7 @@ def _yunda_poll_and_download_tasks(
df = pd.read_excel(file_path, dtype=str) df = pd.read_excel(file_path, dtype=str)
if not df.empty: if not df.empty:
all_dfs.append(df) all_dfs.append(df)
except: except Exception:
pass pass
if all_dfs: if all_dfs:

View File

@@ -6,6 +6,8 @@ import yaml
from datetime import datetime from datetime import datetime
import pandas as pd import pandas as pd
from paths import DOWNLOAD_DIR, CONFIG_PATH
def _wait_and_get_frame(page, text_indicator, timeout_ms=20000): def _wait_and_get_frame(page, text_indicator, timeout_ms=20000):
"""动态雷达探测器:全域扫描所有视窗""" """动态雷达探测器:全域扫描所有视窗"""
@@ -14,14 +16,14 @@ def _wait_and_get_frame(page, text_indicator, timeout_ms=20000):
try: try:
if page.get_by_text(text_indicator).count() > 0: if page.get_by_text(text_indicator).count() > 0:
return page return page
except: except Exception:
pass pass
for frame in page.frames: for frame in page.frames:
try: try:
if frame.get_by_text(text_indicator).count() > 0: if frame.get_by_text(text_indicator).count() > 0:
return frame return frame
except: except Exception:
pass pass
page.wait_for_timeout(300) page.wait_for_timeout(300)
@@ -49,7 +51,7 @@ def zto_expected_download(page):
print("\n▶ 开始执行【中通 - 应到货物数据下载】任务...") print("\n▶ 开始执行【中通 - 应到货物数据下载】任务...")
target_task_title = "进站交接单查询-运单信息" target_task_title = "进站交接单查询-运单信息"
download_dir = os.path.join(os.getcwd(), "downloads") download_dir = DOWNLOAD_DIR
if not os.path.exists(download_dir): if not os.path.exists(download_dir):
os.makedirs(download_dir) os.makedirs(download_dir)
@@ -67,8 +69,8 @@ def zto_expected_download(page):
# 读取 YAML 配置设定天数 # 读取 YAML 配置设定天数
query_days = 1 query_days = 1
try: try:
if os.path.exists("config.yaml"): if os.path.exists(CONFIG_PATH):
with open("config.yaml", "r", encoding="utf-8") as f: with open(CONFIG_PATH, "r", encoding="utf-8") as f:
config = yaml.safe_load(f) or {} config = yaml.safe_load(f) or {}
query_days = int(config.get("zto", {}).get("query_days", 1)) query_days = int(config.get("zto", {}).get("query_days", 1))
except Exception as e: except Exception as e:
@@ -146,7 +148,7 @@ def zto_expected_download(page):
page.locator(".mini-tab", has_text="进站交接单查询").locator( page.locator(".mini-tab", has_text="进站交接单查询").locator(
".mini-tab-close" ".mini-tab-close"
).click() ).click()
except: except Exception:
pass pass
return return
else: else:
@@ -155,7 +157,7 @@ def zto_expected_download(page):
page.locator(".mini-tab", has_text="进站交接单查询").locator( page.locator(".mini-tab", has_text="进站交接单查询").locator(
".mini-tab-close" ".mini-tab-close"
).click() ).click()
except: except Exception:
pass pass
return return
else: else:
@@ -257,7 +259,7 @@ def zto_actual_download(page):
print("\n▶ 开始执行【中通 - 实到货物数据下载】任务...") print("\n▶ 开始执行【中通 - 实到货物数据下载】任务...")
target_task_title = "到件扫描管理" target_task_title = "到件扫描管理"
download_dir = os.path.join(os.getcwd(), "downloads") download_dir = DOWNLOAD_DIR
if not os.path.exists(download_dir): if not os.path.exists(download_dir):
os.makedirs(download_dir) os.makedirs(download_dir)
@@ -275,8 +277,8 @@ def zto_actual_download(page):
# 2. 读取 YAML 并设定时间范围 # 2. 读取 YAML 并设定时间范围
query_days = 1 query_days = 1
try: try:
if os.path.exists("config.yaml"): if os.path.exists(CONFIG_PATH):
with open("config.yaml", "r", encoding="utf-8") as f: with open(CONFIG_PATH, "r", encoding="utf-8") as f:
config = yaml.safe_load(f) or {} config = yaml.safe_load(f) or {}
query_days = int(config.get("zto", {}).get("query_days", 1)) query_days = int(config.get("zto", {}).get("query_days", 1))
except Exception: except Exception:
@@ -344,7 +346,7 @@ def zto_actual_download(page):
page.locator(".mini-tab", has_text="到件扫描监控").locator( page.locator(".mini-tab", has_text="到件扫描监控").locator(
".mini-tab-close" ".mini-tab-close"
).click() ).click()
except: except Exception:
pass pass
return return