Files
InboundVerify/site_zto.py
Misaka 66bd8af421 阶段1:服务化骨架(FastAPI + Playwright worker + 任务队列)
- 新增 runtime.py:抽离共享核心 launch_and_prepare / dispatch_task / run_heartbeat /
  probe_* / launch_anneng / RuntimeContext,常量 SITES_CONFIG 等;main_router 复用
- 新增 server.py:FastAPI(主线程)+ Playwright worker(独立线程)+ 任务队列;
  API:POST/GET /tasks、GET /status、GET /data/{file}(防路径穿越)
- state_store:加 task_history 表 + create/update/get/list 接口
- 5 站点 with_retry 改为返回 True/False,供 dispatch_task 判成败
- main_router:重写为复用 runtime 的交互模式(行为不变)
- requirements:加 fastapi、uvicorn
- CLAUDE.md:补充运行模式与共享核心架构说明

线程模型:主线程 FastAPI 不碰 Playwright,worker 线程独占 page,经 Queue + SQLite 通信。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-17 00:04:39 +08:00

693 lines
28 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# site_zto.py
import os
import re
import time
import yaml
from datetime import datetime
import pandas as pd
from paths import DOWNLOAD_DIR, CONFIG_PATH
def with_retry(site_name, label, flow, reset, max_attempts=3):
"""异常兜底flow 失败 → 重置回初始态 → 重试,最多 max_attempts 次(含首次)。
每次失败都重置(含最终放弃那一次):既是重试前的清场,也保证最终放弃时环境干净。
flow 为零参可调用;返回 False 视为失败,其余视为成功。
返回 True=最终成功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 True
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},放弃(环境已清理)。"
)
return False
# 站点首页 URL异常兜底重置用也供 main_router 的 SITES_CONFIG 引用(单一来源)
HOME_URL = "https://ws.zto56.com/"
def zto_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 _wait_and_get_frame(page, text_indicator, timeout_ms=20000):
"""在主页面与所有 iframe 中查找包含指定文本的窗口"""
start_time = datetime.now()
while (datetime.now() - start_time).total_seconds() * 1000 < timeout_ms:
try:
if page.get_by_text(text_indicator).count() > 0:
return page
except Exception:
pass
for frame in page.frames:
try:
if frame.get_by_text(text_indicator).count() > 0:
return frame
except Exception:
pass
page.wait_for_timeout(300)
raise TimeoutError(f"超时:未找到包含 [{text_indicator}] 的窗口。")
def _dom_click(locator):
"""直接在元素上派发 mousedown+mouseup+click 事件,不走 Playwright 的坐标命中测试,
避免被遮挡元素(如日期控件的 hover 提示气泡)拦截。事件直接发到目标元素并冒泡,
兼容绑定 mousedown 或 click 的控件。"""
locator.evaluate(
"el => { const o = { bubbles: true, cancelable: true, view: window, button: 0 };"
" el.dispatchEvent(new MouseEvent('mousedown', o));"
" el.dispatchEvent(new MouseEvent('mouseup', o));"
" el.dispatchEvent(new MouseEvent('click', o)); }"
)
def zto_smart_menu_click(page, menu_path):
"""中通菜单导航"""
print(f">> 正在导航: {' -> '.join(menu_path)}")
for i in range(len(menu_path)):
current_menu = menu_path[i]
if i < len(menu_path) - 1:
next_menu = menu_path[i + 1]
next_locator = page.locator("span.menu-name", has_text=next_menu).first
if not next_locator.is_visible():
page.locator("span.menu-name", has_text=current_menu).first.click()
page.wait_for_timeout(800)
else:
page.locator("span.menu-name", has_text=current_menu).first.click()
page.wait_for_timeout(1000)
def zto_expected_download(page):
"""中通:应到货物数据下载(内部含异常兜底重试,路由层无感)。"""
return with_retry(
"中通",
"应到",
lambda: zto_expected_download_impl(page),
lambda: zto_reset(page),
)
def zto_expected_download_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"))
export_times = []
try:
# 1. 菜单导航
zto_smart_menu_click(page, ["运营管理", "进站管理", "进站交接单查询"])
ewb_frame = page.frame_locator('iframe[src*="inEwbsListNoSearch"]')
print(">> 正在检测表格统计面板 (#inEwbCount)...")
ewb_frame.locator("#inEwbCount").wait_for(state="attached", timeout=15000)
# 读取 YAML 配置设定天数
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("zto", {}).get("query_days", 1))
except Exception as e:
print(f" ⚠️ 读取 config.yaml 失败,默认查询 1 天: {e}")
print(f">> 正在设定查询时间范围为近【{query_days}】天...")
ewb_frame.locator("#beginDate").click()
page.wait_for_timeout(500)
today_cell = ewb_frame.locator("td div.day.real-today").first
today_cell.wait_for(state="visible")
today_time_str = today_cell.get_attribute("time")
if today_time_str:
today_time = int(today_time_str)
start_time = today_time - (query_days - 1) * 86400000
start_cell = ewb_frame.locator(f"td div.day[time='{start_time}']").first
if start_cell.is_visible():
start_cell.click()
page.wait_for_timeout(300)
today_cell.click()
else:
print(" ⚠️ 设定的天数过大,不在当前日历视窗内,自动降级为查询当天。")
today_cell.click()
page.wait_for_timeout(300)
today_cell.click()
else:
today_cell.click()
page.wait_for_timeout(300)
today_cell.click()
page.wait_for_timeout(500)
# 3. 触发查询并判断数据状态
print(">> 正在点击【查询】按钮并等待数据响应...")
old_count = ewb_frame.locator("#inEwbCount").inner_text().strip()
ewb_frame.locator("#searchbtn").click()
page.wait_for_timeout(1000)
loading_mask = ewb_frame.locator(".mini-mask-loading", has_text="加载中")
if loading_mask.is_visible():
print(" ⏳ 检测到数据加载遮罩层,等待系统渲染...")
loading_mask.wait_for(state="hidden", timeout=30000)
page.wait_for_timeout(1000)
# 轮询判定统计面板更新
wait_cycles = 0
while wait_cycles < 20:
current_count = ewb_frame.locator("#inEwbCount").inner_text().strip()
if current_count != old_count:
break
page.wait_for_timeout(500)
wait_cycles += 1
ticket_count_str = ewb_frame.locator("#inEwbCount").inner_text().strip()
print(f" ✅ 数据已加载,进站实际票数: [{ticket_count_str}]")
# 数据分流
if (
ticket_count_str == "0"
or not ticket_count_str.isdigit()
or int(ticket_count_str) == 0
):
print(" >> 票数为 0正在确认是否为空数据...")
empty_flag = ewb_frame.locator("#datagrid1").get_by_text(
"没有搜索到符合条件的数据记录"
)
if empty_flag.is_visible():
print(" ⚠️ 确认为空数据,正在关闭当前标签页...")
# 即使无数据也关闭已打开的标签页
try:
page.locator(".mini-tab", has_text="进站交接单查询").locator(
".mini-tab-close"
).click()
except Exception:
pass
return
else:
print(" ⚠️ 票数为 0 但未出现空记录提示,页面状态异常,判失败。")
try:
page.locator(".mini-tab", has_text="进站交接单查询").locator(
".mini-tab-close"
).click()
except Exception:
pass
raise RuntimeError("票数为 0 但未出现空记录提示,页面状态异常")
else:
print(" >> 票数校验通过,等待主表格渲染数据行...")
ewb_frame.locator(
"#datagrid1 .mini-grid-rows-view .mini-grid-row"
).first.wait_for(state="visible", timeout=15000)
# 4. 提取主表记录并循环双击
main_rows = ewb_frame.locator("#datagrid1 .mini-grid-rows-view .mini-grid-row")
count = main_rows.count()
print(f">> 共发现 {count} 个交接单需要导出。")
for i in range(count):
print(f" ⏳ 正在处理第 {i+1}/{count} 个交接单...")
row = ewb_frame.locator(
"#datagrid1 .mini-grid-rows-view .mini-grid-row"
).nth(i)
raw_text = row.locator("td").nth(3).inner_text()
match = re.search(r"\d{18}", raw_text)
handover_no = match.group(0) if match else raw_text.strip()
print(f" -> 当前交接单号:{handover_no}")
row.dblclick()
ewb_frame.locator("#datagrid2").get_by_text("运单号").wait_for(
state="visible"
)
ewb_frame.locator(
"#datagrid2 .mini-grid-row", has_text=handover_no
).first.wait_for(state="visible")
# 5. 执行导出流程
ewb_frame.locator("#exportExcel").click()
page.locator(".mini-panel-title", has_text="导出选择列").wait_for(
state="visible"
)
export_frame = page.frame_locator('iframe[src*="download"]')
export_frame.locator(".mini-button-text", has_text=">>").click()
page.wait_for_timeout(300)
export_frame.locator(".mini-button-text", has_text="确定").click()
print(" >> 正在查找【温馨提示】弹窗...")
ctx_alert = _wait_and_get_frame(page, "温馨提示")
ctx_alert.locator(
".mini-messagebox-buttons .mini-button-text", has_text="确定"
).click()
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.locator(".mini-tips-success").wait_for(
state="hidden", timeout=15000
)
except Exception as e:
if "detached" in str(e).lower():
print(" 提示框所在 iframe 已随提交关闭,任务已建立。")
else:
raise
print(" ✅ 成功提示框已消失。")
export_times.append(datetime.now())
print(" >> 切换回【交接单信息】标签页...")
ewb_frame.locator("#ewbsListNo").click()
page.wait_for_timeout(1000)
# ====================================================================
# 完成所有交接单导出提交后,关闭当前标签页
# ====================================================================
print(">> 【进站交接单查询】已完成,正在关闭标签页...")
try:
page.locator(".mini-tab", has_text="进站交接单查询").locator(
".mini-tab-close"
).click()
page.wait_for_timeout(500)
print(" ✅ 【进站交接单查询】标签页已关闭。")
except Exception as e:
print(f" ⚠️ 关闭【进站交接单查询】标签页时出错: {e}")
# 交由统一的轮询下载流程处理
_zto_poll_and_download_tasks(
page,
export_times,
download_dir,
"中通-应到货物数据.xlsx",
)
return True
except Exception as e:
print(f"\n❌ 任务执行过程中发生异常: {e}")
return False
def zto_actual_download(page):
"""中通:实到货物数据下载(内部含异常兜底重试,路由层无感)。"""
return with_retry(
"中通",
"实到",
lambda: zto_actual_download_impl(page),
lambda: zto_reset(page),
)
def zto_actual_download_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"))
export_times = []
try:
# 1. 菜单导航
zto_smart_menu_click(page, ["运营管理", "扫描操作与监控", "到件扫描监控"])
arr_frame = page.frame_locator('iframe[src*="ArriveScan"]')
print(">> 正在检测主页面 (#daterange)...")
arr_frame.locator("#daterange").wait_for(state="attached", timeout=15000)
# 2. 读取 YAML 并设定时间范围
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("zto", {}).get("query_days", 1))
except Exception:
pass
print(f">> 正在设定查询时间范围为近【{query_days}】天...")
arr_frame.locator("#daterange").click()
page.wait_for_timeout(500)
today_cell = arr_frame.locator("td div.day.real-today").first
today_cell.wait_for(state="visible")
today_time_str = today_cell.get_attribute("time")
if today_time_str:
today_time = int(today_time_str)
start_time = today_time - (query_days - 1) * 86400000
start_cell = arr_frame.locator(f"td div.day[time='{start_time}']").first
# 日期格子用 _dom_click 直接派发事件Playwright 的 .click() 会先 hover 格子,
# 触发“范围长度”提示气泡(.date-range-length-tip)盖住格子,导致点击被判遮挡而超时。
if start_cell.is_visible():
_dom_click(start_cell)
page.wait_for_timeout(300)
_dom_click(today_cell)
else:
_dom_click(today_cell)
page.wait_for_timeout(300)
_dom_click(today_cell)
else:
_dom_click(today_cell)
page.wait_for_timeout(300)
_dom_click(today_cell)
page.wait_for_timeout(500)
# 3. 设定单号类型
print(">> 正在设定单号类型为【子单】...")
arr_frame.locator('[id="bandEwbType$text"]').click()
page.wait_for_timeout(500)
arr_frame.locator(".mini-tree-nodeshow").filter(
has_text=re.compile(r"^子单$")
).locator(".mini-tree-checkbox").click()
page.wait_for_timeout(300)
# 4. 触发查询并判断数据状态
print(">> 正在点击【查询】按钮并等待数据响应...")
arr_frame.locator("#searchbtn").click()
page.wait_for_timeout(1000)
loading_mask = arr_frame.locator(".mini-mask-loading", has_text="加载中")
if loading_mask.is_visible():
print(" ⏳ 检测到数据加载遮罩层,等待系统渲染...")
loading_mask.wait_for(state="hidden", timeout=30000)
page.wait_for_timeout(1000)
empty_flag = arr_frame.locator("#datagrid1").get_by_text(
"没有搜索到符合条件的数据记录"
)
if empty_flag.is_visible():
print(" ⚠️ 当前查询范围内没有数据,终止并关闭标签页。")
try:
page.locator(".mini-tab", has_text="到件扫描监控").locator(
".mini-tab-close"
).click()
except Exception:
pass
return
arr_frame.locator("#page1").wait_for(state="visible", timeout=15000)
print(" ✅ 数据已加载。(底部分页控件已就绪)")
# 5. 执行导出流程
arr_frame.locator("#exportExcel").click()
page.locator(".mini-panel-title", has_text="导出选择列").wait_for(
state="visible"
)
export_frame = page.frame_locator('iframe[src*="download"]')
export_frame.locator(".mini-button-text", has_text=">>").click()
page.wait_for_timeout(300)
export_frame.locator(".mini-button-text", has_text="确定").click()
print(" >> 正在查找【温馨提示】弹窗...")
ctx_alert = _wait_and_get_frame(page, "温馨提示")
ctx_alert.locator(
".mini-messagebox-buttons .mini-button-text", has_text="确定"
).click()
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.locator(".mini-tips-success").wait_for(
state="hidden", timeout=15000
)
except Exception as e:
if "detached" in str(e).lower():
print(" 提示框所在 iframe 已随提交关闭,任务已建立。")
else:
raise
print(" ✅ 成功提示框已消失。")
export_times.append(datetime.now())
# ====================================================================
# 完成实到数据导出提交后,关闭当前标签页
# ====================================================================
print(">> 【到件扫描监控】已完成,正在关闭标签页...")
try:
page.locator(".mini-tab", has_text="到件扫描监控").locator(
".mini-tab-close"
).click()
page.wait_for_timeout(500)
print(" ✅ 【到件扫描监控】标签页已关闭。")
except Exception as e:
print(f" ⚠️ 关闭【到件扫描监控】标签页时出错: {e}")
# 交由统一的轮询下载流程处理
_zto_poll_and_download_tasks(
page,
export_times,
download_dir,
"中通-实到货物数据.xlsx",
)
return True
except Exception as e:
print(f"\n❌ 任务执行过程中发生异常: {e}")
return False
def _zto_poll_and_download_tasks(page, export_times, download_dir, final_filename):
"""中通离线任务的轮询与下载"""
print("\n>> 正在前往【导出任务管理】界面...")
zto_smart_menu_click(page, ["系统配置", "导出任务管理"])
taskdone_frame = page.frame_locator('iframe[src*="taskdone"]')
taskdone_frame.locator("#taskdoneDatagrid").get_by_text("任务标题").wait_for(
state="visible"
)
page.wait_for_timeout(1000)
print(">> 列表已加载,开始匹配并检查任务状态...")
total_expected = len(export_times)
poll_deadline = (
time.monotonic() + 300
) # 5 分钟上限:任务卡死/匹配不上时超时失败,交由上层重置重试
last_total_found = -1
stall_rounds = 0 # 连续无进展轮数:刷新后 total_found 不增长则累计,超阈值快速失败
while True:
if time.monotonic() > poll_deadline:
raise RuntimeError(
"轮询导出任务超时5 分钟未全部完成/匹配),疑似任务卡死"
)
task_rows = taskdone_frame.locator(
"#taskdoneDatagrid .mini-grid-rows-view .mini-grid-row"
)
row_count = task_rows.count()
ready_timestamps = set()
processing_timestamps = set()
# 单账号无并发:仅按提交时间容差(40s)认领本批任务,不再校验任务标题
# (站点可能调整标题名,写死标题会导致匹配失败、任务一直查不到)。
for i in range(row_count):
tds = task_rows.nth(i).locator("td")
if tds.count() < 10:
continue
submit_time_str = tds.nth(4).inner_text().strip()
status_str = tds.nth(6).inner_text().strip()
try:
row_time = datetime.strptime(submit_time_str, "%Y-%m-%d %H:%M:%S")
matched = any(
abs((row_time - et).total_seconds()) <= 40 for et in export_times
)
if matched:
if status_str == "成功执行":
ready_timestamps.add(submit_time_str)
else:
processing_timestamps.add(submit_time_str)
except Exception:
pass
total_found = len(ready_timestamps) + len(processing_timestamps)
print(
f" 📊 状态统计:期望 [{total_expected}],已入表 [{total_found}] (就绪 [{len(ready_timestamps)}],处理中 [{len(processing_timestamps)}])"
)
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(timeout=8000)
except Exception as e:
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:
target_task_timestamps = list(ready_timestamps)
print(">> 所有目标任务已生成,开始下载...")
break
# 8. 下载逻辑
downloaded_files = []
for time_str in target_task_timestamps:
try:
target_row = (
taskdone_frame.locator(
"#taskdoneDatagrid .mini-grid-rows-view .mini-grid-row"
)
.filter(
has=taskdone_frame.locator(
f"td:nth-child(5):has-text('{time_str}')"
)
)
.first
)
print(f" 开始下载任务 [{time_str}] ...")
checkbox = target_row.locator(".mini-grid-checkbox")
if checkbox.is_visible():
checkbox.click()
print(" -> 已勾选当前记录的 Checkbox")
page.wait_for_timeout(500)
with page.expect_download() as download_info:
target_row.locator("td").nth(10).locator(".ui-btn-download").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}")
if checkbox.is_visible():
checkbox.click()
print(" -> 已取消勾选,继续下一条")
page.wait_for_timeout(500)
except Exception as e:
print(f" ❌ 下载任务 [{time_str}] 失败: {e}")
# 汇总校验:下载成功数必须等于本批提交的任务数,否则判失败
# (防"部分/全失败却被判成功"
if len(downloaded_files) < total_expected:
raise RuntimeError(
f"仅成功下载 {len(downloaded_files)}/{total_expected} 个任务,数据不完整"
)
# ====================================================================
# 所有目标文件下载完成后,关闭“导出任务管理”标签页
# ====================================================================
print(">> 【导出任务管理】下载完成,正在关闭标签页...")
try:
page.locator(".mini-tab", has_text="导出任务管理").locator(
".mini-tab-close"
).click()
page.wait_for_timeout(500)
print(" ✅ 【导出任务管理】标签页已关闭。")
except Exception as e:
print(f" ⚠️ 关闭【导出任务管理】标签页时出错: {e}")
# 9. 合并数据
if downloaded_files:
print("\n>> 正在合并下载的数据...")
all_data_frames = []
for file_path in downloaded_files:
try:
df = pd.read_excel(file_path, dtype=str)
if not df.empty:
all_data_frames.append(df)
except Exception:
pass
if all_data_frames:
combined_df = pd.concat(all_data_frames, ignore_index=True)
final_output_path = os.path.join(download_dir, final_filename)
combined_df.to_excel(final_output_path, index=False)
print(f"====================================================")
print(f" 合并完成。")
print(f" 📁 输出路径: {final_output_path}")
print(f"====================================================")
for file_path in downloaded_files:
os.remove(file_path)
print(" 临时文件已清理。")