Refactor monolithic script into multi-site module architecture

- Introduce main_router.py as multi-site controller with interactive menu
- Extract 顺心 site logic into site_shunxin.py (expected & actual download)
- Add site_baishi.py stub for upcoming 百世 integration
- Add global 应到未到 data reconciliation via Left Anti-Join on 运单号

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Misaka
2026-06-15 22:02:51 +08:00
parent b0c82bc23d
commit 411a2487fc
3 changed files with 563 additions and 0 deletions

173
main_router.py Normal file
View File

@@ -0,0 +1,173 @@
# main_router.py
import os
import pandas as pd
from playwright.sync_api import sync_playwright
# 导入抽离出去的各个网点模块
import site_shunxin
import site_baishi
# 定义网点及对应的初始登录 URL
SITES_CONFIG = {
"顺心": "https://sxne.sxjdfreight.com",
"百世": "https://v5.800best.com",
}
def task_process_undelivered_data():
"""全局模块:应到未到异常件比对引擎"""
print("\n▶ 开始执行【应到未到数据处理】任务...")
download_dir = os.path.join(os.getcwd(), "downloads")
expected_path = os.path.join(download_dir, "应到货物数据.xlsx")
actual_path = os.path.join(download_dir, "实到货物数据.xlsx")
output_path = os.path.join(download_dir, "应到未到货物数据.xlsx")
if not os.path.exists(expected_path):
print(f"❌ 错误:找不到【应到货物数据】主文档:{expected_path}")
return
if not os.path.exists(actual_path):
print(f"❌ 错误:找不到【实到货物数据】主文档:{actual_path}")
return
try:
print(">> 正在载入本地 Excel 文档...")
df_expected = pd.read_excel(expected_path)
df_actual = pd.read_excel(actual_path)
if "运单号" not in df_expected.columns or "运单号" not in df_actual.columns:
print("❌ 核心资产校验失败:数据源中缺失【运单号】字段,请检查导出配置。")
return
print(">> 正在启动多维数据集比对引擎...")
# Left Anti-Join在应到中找出不存在于实到里的运单号
df_undelivered = df_expected[~df_expected["运单号"].isin(df_actual["运单号"])]
target_columns = ["班次号", "交接单号", "运单号"]
available_columns = [
col for col in target_columns if col in df_undelivered.columns
]
df_output = df_undelivered[available_columns]
print(f">> 筛选完毕!共捕获到异常【应到未到】货物数据: {len(df_output)} 条。")
df_output.to_excel(output_path, index=False)
print(f"====================================================")
print(f" 🎉 异常比对流完成!独立数据已安全输出。")
print(f" 📁 成果归档路径: {output_path}")
print(f"====================================================")
except Exception as e:
print(f"❌ 数据处理引擎在执行连接和输出时发生致命异常: {e}")
def run_multi_site_daemon():
"""多网点自动化主控引擎"""
with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
# 取消禁用视口大小限制,防止有些系统自适应出问题
context = browser.new_context(viewport={"width": 1920, "height": 1080})
# 用于存储网点名称与 Page 对象的映射字典
pages_map = {}
print("\n====================================================")
print("【初始化阶段】正在构建多网点运行环境...")
print("====================================================")
# 遍历配置,依次创建独立的标签页
for site_name, url in SITES_CONFIG.items():
print(f">> 正在打开【{site_name}】页面: {url}")
page = context.new_page()
page.goto(url)
pages_map[site_name] = page
page.wait_for_timeout(1000)
print("\n====================================================")
print("⚠️ 【等待人工介入】")
print("请在弹出的浏览器中,依次切换标签页,人工完成所有网点的登录!")
print("====================================================")
# 阻塞程序,等待用户登录完毕
input(">> 登录全部完成后,请在此处按下【回车键】正式接管中控台...")
print("\n====================================================")
print("【系统接管】正在执行各网点就绪前初始化动作...")
print("====================================================")
# 处理【顺心】网点的登录后就绪判定与弹窗清理
try:
sx_page = pages_map["顺心"]
sx_page.bring_to_front()
print(">> 正在处理【顺心】网点初始状态...")
# 使用较长超时时间确认登录状态(避免回车按得太早没加载完)
sx_page.wait_for_selector('h1:has-text("盟商门户网")', timeout=30000)
print(" 🎉 登录成功!系统已接管顺心浏览器。")
sx_page.wait_for_timeout(1000)
# 跳过初始一些弹窗干扰
sx_page.locator("a").nth(4).click()
sx_page.wait_for_timeout(1000)
sx_page.get_by_role("button", name="Close").click()
sx_page.wait_for_timeout(1000)
sx_page.get_by_role("button", name="不再询问").click()
sx_page.wait_for_timeout(1000)
print(" ✅ 【顺心】弹窗清理完毕,状态就绪!")
except Exception as e:
print(f" ⚠️ 【顺心】初始化或弹窗清理异常 (如无弹窗可忽略): {e}")
# 未来如果要加【百世】的弹窗清理,可以直接在这里依葫芦画瓢加上
# try:
# bs_page = pages_map["百世"]
# bs_page.bring_to_front()
# ...
while True:
print("\n==============================")
print(" 物流数据多端提取总枢纽 ")
print("==============================")
print("1. [顺心] - 执行【应到货物数据下载】")
print("2. [顺心] - 执行【实到货物数据下载】")
print("3. [百世] - 执行【应到货物数据下载】")
print("4. [百世] - 执行【实到货物数据下载】")
print("5. [全局] - 执行【应到未到数据清洗比对】")
print("0. 退出系统")
print("==============================")
choice = input("请输入任务编号并回车: ")
try:
if choice == "1":
pages_map["顺心"].bring_to_front() # 切换到对应标签页
site_shunxin.shunxin_expected_download(pages_map["顺心"])
elif choice == "2":
pages_map["顺心"].bring_to_front()
site_shunxin.shunxin_actual_download(pages_map["顺心"])
elif choice == "3":
pages_map["百世"].bring_to_front()
site_baishi.baishi_expected_download(pages_map["百世"])
elif choice == "4":
pages_map["百世"].bring_to_front()
site_baishi.baishi_actual_download(pages_map["百世"])
elif choice == "5":
task_process_undelivered_data()
elif choice == "0":
print("\n准备退出程序,释放浏览器资源...")
break
else:
print("\n⚠️ 无效输入,请重新选一下。")
except Exception as e:
print(f"❌ 调度执行异常: {e}")
# 退出循环后关闭浏览器
browser.close()
print("系统已安全关闭。")
if __name__ == "__main__":
run_multi_site_daemon()

15
site_baishi.py Normal file
View File

@@ -0,0 +1,15 @@
# site_baishi.py
def baishi_expected_download(page):
"""百世:应到货物数据下载"""
print("\n▶ 开始执行【百世 - 应到货物数据下载】任务...")
print("🚧 逻辑开发中 (pass)...")
pass
def baishi_actual_download(page):
"""百世:实到货物数据下载"""
print("\n▶ 开始执行【百世 - 实到货物数据下载】任务...")
print("🚧 逻辑开发中 (pass)...")
pass

375
site_shunxin.py Normal file
View File

@@ -0,0 +1,375 @@
# site_shunxin.py
import os
import re
from datetime import datetime
import pandas as pd
def shunxin_expected_download(page):
"""顺心:应到货物数据下载"""
print("\n▶ 开始执行【顺心 - 应到货物数据下载】任务...")
# 初始化并创建下载目录
download_dir = os.path.join(os.getcwd(), "downloads")
if not os.path.exists(download_dir):
os.makedirs(download_dir)
print(f">> 已创建专属下载文件夹: {download_dir}")
export_times = []
target_task_timestamps = []
try:
# 1. 导航与页面加载判断
print(">> 正在进入【车辆点到】界面...")
page.locator("span.ant-pro-menu-item-title:has-text('派件管理')").click()
page.locator("div.ant-pro-menu-item:has-text('车辆点到')").click()
page.get_by_role("button", name="点到").wait_for(state="visible")
page.get_by_role("button", name="打印交接单").wait_for(state="visible")
page.get_by_role("button", name="强卸").wait_for(state="visible")
print("✅ 车辆点到界面加载完毕")
# 2. 筛选条件1天、状态=已发 -> 已到、查询
print(">> 正在设置筛选条件: 选择【1天】...")
page.get_by_role("radio", name="1天").check()
print(">> 正在展开【状态】下拉菜单...")
# 尊重改动:匹配包含 已发 或 已到 的下拉选项
page.locator(
".ant-select-selection-item", has_text=re.compile(r"已发|已到")
).click()
print(">> 正在选择状态为【已到】...")
# 尊重改动:精确定位选项
page.locator(".ant-select-item-option", has_text="已到").click()
print(">> 正在点击【查询】按钮...")
# 尊重改动:使用带 search 标识的 exact role 定位
page.get_by_role("button", name="search 查询").click()
page.wait_for_timeout(2000)
# 3. 获取所有【运单列表】按钮并循环处理
waybill_btns = page.get_by_role("button", name="运单列表")
count = waybill_btns.count()
print(f">> 共发现 {count} 个班次需要导出。")
for i in range(count):
print(f" ⏳ 正在处理第 {i+1}/{count} 个班次...")
waybill_btns.nth(i).click()
page.locator("label[title='运单查询']").wait_for(state="visible")
# 4. 执行导出流程
# 尊重改动:精准的 export role 定位
page.get_by_role("button", name="export 导出").click()
page.locator(
"span.ant-transfer-list-header-title:has-text('待选导出列')"
).wait_for(state="visible")
page.locator(".ant-transfer-list").first.locator(
".ant-transfer-list-header label"
).click()
# 尊重改动:限定在"导出"区域内的向右按钮
page.get_by_label("导出").get_by_role("button", name="right").click()
# 尊重改动:精准的 export 数据按钮
page.get_by_role("button", name="export 导出数据").click()
export_times.append(datetime.now())
page.locator("text=任务添加成功!").wait_for(state="visible")
page.get_by_role("button", name="知道了").click()
page.get_by_role("tab", name="车辆点到").click()
page.wait_for_timeout(500)
if count > 0:
print("✅ 所有班次的导出任务已成功提交!")
else:
print("⚠️ 未发现任何运单列表,直接跳转至下载环节。")
# 6. 前往数据导出页面去下载
print(">> 正在前往【数据导出】界面...")
page.locator("a[href='/dataExport']").click()
# 尊重改动:使用 columnheader 确保表格表头真正加载完毕
page.get_by_role("columnheader", name="任务标题").wait_for(state="visible")
page.wait_for_timeout(2000)
# 尊重改动:进入页面后主动点击一次查询刷新状态
page.get_by_role("button", name="search 查询").click()
page.wait_for_timeout(2000)
# 7. 轮询任务状态
print(">> 列表中已渲染,开始匹配并检查后端处理状态...")
while True:
rows = page.locator(".ant-table-tbody > tr.ant-table-row")
row_count = rows.count()
pending_tasks = 0
current_ready_timestamps = []
for i in range(row_count):
tds = rows.nth(i).locator("td")
if tds.count() < 9:
continue
submit_time_str = tds.nth(2).inner_text().strip()
title_str = tds.nth(5).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()) <= 60
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)
# 尊重改动:使用更精准的 search 查询按钮定位
page.get_by_role("button", name="search 查询").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 = page.locator(".ant-table-tbody > tr.ant-table-row").filter(
has=page.locator(f"td:nth-child(3):has-text('{time_str}')")
)
print(f" 🎯 触发下载 -> 任务 [{time_str}] ...")
with page.expect_download() as download_info:
target_row.locator("td").nth(8).locator(
"button", has_text=re.compile(r"\s*载")
).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}")
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:
df = pd.read_excel(file_path)
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 shunxin_actual_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 = []
target_task_timestamps = []
try:
# 1. 导航与页面加载
print(">> 正在进入【卸车扫描记录】界面...")
page.locator("span.ant-pro-menu-item-title:has-text('派件管理')").click()
page.locator("div.ant-pro-menu-item:has-text('卸车扫描记录')").click()
page.get_by_role("radio", name="1天").wait_for(state="visible")
print("✅ 卸车扫描记录界面加载完毕")
# 2. 筛选条件1天、查询
print(">> 正在设置筛选条件: 选择【1天】...")
page.get_by_role("radio", name="1天").check()
print(">> 正在点击【查询】按钮...")
# 尊重改动:精确到 search 查询
page.get_by_role("button", name="search 查询").click()
page.wait_for_timeout(2000)
# 3. 直接发起全局导出
print(">> 正在发起导出请求...")
# 尊重改动:精确定位
page.get_by_role("button", name="export 导出").click()
page.locator(
"span.ant-transfer-list-header-title:has-text('待选导出列')"
).wait_for(state="visible")
page.locator(".ant-transfer-list").first.locator(
".ant-transfer-list-header label"
).click()
# 尊重改动:定位 right 按钮
page.get_by_label("导出").get_by_role("button", name="right").click()
# 尊重改动:精准的 export 导出数据按钮
page.get_by_role("button", name="export 导出数据").click()
export_times.append(datetime.now())
page.locator("text=任务添加成功!").wait_for(state="visible")
page.get_by_role("button", name="知道了").click()
print("✅ 卸车扫描记录导出任务已成功提交!")
# 4. 前往数据导出页面去下载
print(">> 正在前往【数据导出】界面...")
page.locator("a[href='/dataExport']").click()
# 尊重改动:使用 columnheader 确保表格加载
page.get_by_role("columnheader", name="任务标题").wait_for(state="visible")
page.wait_for_timeout(2000)
# 尊重改动:主动点击查询刷新
page.get_by_role("button", name="search 查询").click()
page.wait_for_timeout(2000)
# 5. 轮询任务状态
print(">> 列表中已渲染,开始匹配并检查后端处理状态...")
while True:
rows = page.locator(".ant-table-tbody > tr.ant-table-row")
row_count = rows.count()
pending_tasks = 0
current_ready_timestamps = []
for i in range(row_count):
tds = rows.nth(i).locator("td")
if tds.count() < 9:
continue
submit_time_str = tds.nth(2).inner_text().strip()
title_str = tds.nth(5).inner_text().strip()
status_str = tds.nth(6).inner_text().strip()
if title_str.startswith("卸车扫描记录"):
try:
row_time = datetime.strptime(
submit_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_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.get_by_role("button", name="search 查询").click()
page.wait_for_timeout(2000)
else:
target_task_timestamps = current_ready_timestamps
if len(target_task_timestamps) > 0:
print(">> ✅ 目标任务已就绪!开始下载...")
break
# 6. 下载逻辑
downloaded_files = []
for time_str in target_task_timestamps:
try:
target_row = page.locator(".ant-table-tbody > tr.ant-table-row").filter(
has=page.locator(f"td:nth-child(3):has-text('{time_str}')")
)
print(f" 🎯 触发下载 -> 任务 [{time_str}] ...")
with page.expect_download() as download_info:
target_row.locator("td").nth(8).locator(
"button", has_text=re.compile(r"\s*载")
).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}")
except Exception as e:
print(f" ❌ 下载任务 [{time_str}] 失败: {e}")
# 7. 合并数据
if downloaded_files:
print("\n>> 🧪 正在开始执行数据归档整理...")
all_data_frames = []
for file_path in downloaded_files:
try:
df = pd.read_excel(file_path)
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}")