成败信号统一化(顺心/中通/韵达/安能/百世): - 各站 impl 开头清理上次的最终文件,避免无数据/失败时残留旧数据误导比对 - 轮询下载段加汇总校验:下载数 < 预期则 raise,堵住"零/部分下载被判成功" - 正常完成统一显式 return True(原中通/韵达/安能靠 None 隐式成功) - 无数据统一不落文件(安能实到不再落空 xlsx);韵达裸 except 改 except Exception - 中通"票数0无空提示"分支改判失败 中通 bug 修复(site_zto.py): - poll 加 stall 快速失败(连续4轮无进展即 raise)+ 刷新短超时 + 双失败 raise, 避免页面被遮挡时干等 5 分钟才触发重试 - "生成离线导出任务成功"提示所在 iframe 提交后被销毁,wait_for 抛 "Frame was detached" 属正常(提示已随 iframe 消失=任务已建立),改判成功 Co-Authored-By: Claude <noreply@anthropic.com>
193 lines
7.4 KiB
Python
193 lines
7.4 KiB
Python
# site_baishi.py
|
||
|
||
import os
|
||
import yaml
|
||
from playwright.sync_api import sync_playwright
|
||
|
||
from paths import DOWNLOAD_DIR, CONFIG_PATH
|
||
|
||
|
||
def with_retry(site_name, label, flow, reset, max_attempts=3):
|
||
"""异常兜底:flow 失败 → 重置回初始态 → 重试,最多 max_attempts 次(含首次)。
|
||
|
||
每次失败都重置(含最终放弃那一次):既是重试前的清场,也保证最终放弃时环境干净。
|
||
flow 为零参可调用;返回 False 视为失败,其余视为成功。
|
||
"""
|
||
for attempt in range(1, max_attempts + 1):
|
||
try:
|
||
ret = flow()
|
||
if ret is False:
|
||
raise RuntimeError("流程返回失败状态")
|
||
if attempt > 1:
|
||
print(f">> 【{site_name}-{label}】第 {attempt} 次尝试成功 ✅")
|
||
return
|
||
except Exception as e:
|
||
print(f"⚠️ 【{site_name}-{label}】第 {attempt}/{max_attempts} 次失败: {e}")
|
||
print(f" → 重置【{site_name}】到初始态,清理环境 ...")
|
||
try:
|
||
reset()
|
||
except Exception as re:
|
||
print(f" ⚠️ 重置异常: {re}")
|
||
if attempt < max_attempts:
|
||
continue
|
||
print(
|
||
f"❌ 【{site_name}-{label}】已达最大尝试次数 {max_attempts},放弃(环境已清理)。"
|
||
)
|
||
|
||
|
||
# 站点首页 URL:异常兜底重置用,也供 main_router 的 SITES_CONFIG 引用(单一来源)
|
||
HOME_URL = "https://v5.800best.com"
|
||
|
||
|
||
def baishi_reset(page):
|
||
"""异常兜底:重置百世到初始态(跳首页 URL,丢弃当前页面状态,登录态保留)。"""
|
||
page.goto(HOME_URL)
|
||
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):
|
||
"""关闭指定名称的百世标签页。
|
||
|
||
通过 li > span 中的文本定位标签,并点击其内部 title 为 "关闭标签页" 的图标。
|
||
"""
|
||
try:
|
||
tab = page.locator("li").filter(has=page.locator("span", has_text=tab_name))
|
||
if tab.count() == 0:
|
||
print(f" ℹ️ 未找到标签页【{tab_name}】(可能尚未打开或已关闭),跳过。")
|
||
return
|
||
|
||
# 点击百世特有的关闭按钮
|
||
tab.first.locator("i[title='关闭标签页']").click()
|
||
print(f" 🗙 已关闭标签页【{tab_name}】")
|
||
page.wait_for_timeout(300)
|
||
except Exception as e:
|
||
print(f" ⚠️ 关闭标签页【{tab_name}】时出错: {e}")
|
||
|
||
|
||
def baishi_download_undelivered_data(page):
|
||
"""百世:一键提取应到未到(当日未扫)数据(内部含异常兜底重试,路由层无感)。"""
|
||
|
||
return with_retry(
|
||
"百世",
|
||
"应到未到",
|
||
lambda: baishi_download_undelivered_data_impl(page),
|
||
lambda: baishi_reset(page),
|
||
)
|
||
|
||
|
||
def baishi_download_undelivered_data_impl(page):
|
||
"""百世:一键提取应到未到(当日未扫)数据(单次执行,无重试;供自动化测试用)。"""
|
||
print("\n▶ 开始执行【百世 - 一键提取应到未到数据】任务...")
|
||
|
||
download_dir = DOWNLOAD_DIR
|
||
if not os.path.exists(download_dir):
|
||
os.makedirs(download_dir)
|
||
|
||
# 清理上次的最终文件,避免本次无数据/失败时残留旧数据误导比对
|
||
_remove_if_exists(os.path.join(download_dir, "百世-应到未到货物数据.xlsx"))
|
||
|
||
try:
|
||
# 1. 导航与页面加载
|
||
print(">> 正在进入【扫描综合查询】界面...")
|
||
# 打开基础服务菜单面板
|
||
page.locator("div.nav-level1", has_text="基础服务").click()
|
||
|
||
# 限制在菜单面板(nav-level2-wrapper)内查找,避免命中右侧同名标签页
|
||
page.locator(".nav-level2-wrapper").locator(
|
||
"a", has_text="扫描综合查询"
|
||
).click()
|
||
|
||
# 验证表格主界面加载完毕
|
||
page.get_by_role("tab", name="实时扫描率").wait_for(state="visible")
|
||
print("✅ 扫描综合查询界面已加载")
|
||
|
||
# 2. 精准定位并点击【到/接件扫描 -> 当日 -> 未扫】的数字控件
|
||
print(">> 正在解析表格,提取当日到件未扫明细...")
|
||
# 定位第一行数据的第 13 列(索引 12)
|
||
target_cell = page.locator(".ant-table-tbody > tr").first.locator("td").nth(12)
|
||
|
||
# 特殊情况处理:检查未扫数量是否为0
|
||
cell_text = target_cell.inner_text().strip()
|
||
if cell_text == "0" or cell_text == "":
|
||
print(
|
||
f" ℹ️ 注意:当日未扫数量为【{cell_text}】,无数据需要提取,任务结束。"
|
||
)
|
||
# 数据为空时,提前结束前也需清理环境
|
||
_close_tab(page, "扫描综合查询")
|
||
return True
|
||
|
||
# 有未扫数据,继续点击操作
|
||
target_cell.locator("a").click()
|
||
|
||
# 等待下方弹出的明细表格区域加载完毕
|
||
detail_section = page.locator(".m-query-all-scanRateDetail")
|
||
detail_section.wait_for(state="visible")
|
||
page.wait_for_timeout(1000)
|
||
|
||
# 3. 触发导出设置模态框
|
||
print(">> 正在打开导出配置面板...")
|
||
# 使用明细区域内的导出按钮,避免误点主表导出
|
||
detail_section.locator(".export-wrap a[title='导出']").click()
|
||
|
||
# 验证模态框弹出
|
||
page.locator(".ant-modal-title", has_text="导出设置").wait_for(state="visible")
|
||
|
||
# 4. 加载 YAML 配置并输入密码
|
||
print(">> 正在读取配置并填写密码...")
|
||
password = ""
|
||
try:
|
||
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
|
||
config = yaml.safe_load(f)
|
||
password = config.get("baishi", {}).get("password", "")
|
||
|
||
if not password:
|
||
print(
|
||
" ⚠️ 警告:在 config.yaml 中未找到百世的登录密码,可能导致导出失败。"
|
||
)
|
||
except Exception as e:
|
||
print(f" ⚠️ 读取 config.yaml 失败,请确保文件存在且格式正确: {e}")
|
||
return False
|
||
|
||
page.get_by_placeholder("请输入登录密码").fill(password)
|
||
|
||
# 5. 执行最终下载
|
||
print(">> 正在下载...")
|
||
with page.expect_download() as download_info:
|
||
# 点击模态框底部的“导 出”按钮
|
||
page.locator(".ant-modal-footer").get_by_role(
|
||
"button", name="导 出"
|
||
).click()
|
||
|
||
download = download_info.value
|
||
save_path = os.path.join(download_dir, "百世-应到未到货物数据.xlsx")
|
||
|
||
# 如果之前已经有同名文件,覆盖保存
|
||
if os.path.exists(save_path):
|
||
os.remove(save_path)
|
||
|
||
download.save_as(save_path)
|
||
print(f"====================================================")
|
||
print(f" 提取成功。")
|
||
print(f" 已下载: {save_path}")
|
||
print(f"====================================================")
|
||
|
||
# 6. 环境清理
|
||
print(">> 任务完成,正在清理环境...")
|
||
_close_tab(page, "扫描综合查询")
|
||
|
||
print("\n【百世 - 应到未到数据提取】流程结束。")
|
||
return True
|
||
|
||
except Exception as e:
|
||
print(f"\n❌ 任务执行过程中发生异常: {e}")
|
||
return False
|