From b916d82058f7607e9934590cf966cd0fa5a8488e Mon Sep 17 00:00:00 2001 From: Misaka Date: Sat, 1 Aug 2026 17:33:20 +0800 Subject: [PATCH] =?UTF-8?q?fix(zto):=20=E4=BF=AE=E5=A4=8D=E5=8F=8C?= =?UTF-8?q?=E6=9C=88=E6=97=A5=E6=9C=9F=E6=8E=A7=E4=BB=B6=20real-today=20?= =?UTF-8?q?=E5=AE=9A=E4=BD=8D=E5=A4=B1=E8=B4=A5=EF=BC=88=E6=9C=88=E5=88=9D?= =?UTF-8?q?1=E5=8F=B7=E9=9A=90=E8=97=8F=20ghost=20cell=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中通 jQuery Date Range Picker 始终显示双月面板。月初时(如8月1日), 当天日期同时出现在 month1(隐藏 ghost cell)和 month2(可见 cell), 两者的 CSS class 都含 real-today,.first 取到隐藏元素导致超时。 改为直接从 Python datetime 计算目标时间戳,新增 _zto_find_visible_day 查找可见日期格(跳过 display:none 的 ghost cell),同步修复 _zto_flip_to_target_month 的可见性判断。 Co-Authored-By: Claude --- inbound_verify/sites/zto.py | 133 +++++++++++++++++++++--------------- 1 file changed, 79 insertions(+), 54 deletions(-) diff --git a/inbound_verify/sites/zto.py b/inbound_verify/sites/zto.py index 5858f39..b2104da 100644 --- a/inbound_verify/sites/zto.py +++ b/inbound_verify/sites/zto.py @@ -4,7 +4,7 @@ import os import re import time import yaml -from datetime import datetime +from datetime import datetime, timedelta import pandas as pd from inbound_verify.paths import DOWNLOAD_DIR, CONFIG_PATH @@ -93,20 +93,50 @@ def _dom_click(locator): ) +def _zto_compute_target_time(offset): + """直接从 Python datetime 计算目标日期的毫秒级时间戳(本地时区零点)。 + + 不再从 DOM 的 real-today 元素读取 time 属性,避免双月视图下 real-today + 同时出现在 month1(隐藏 ghost cell)和 month2(可见)导致 .first 取到隐藏元素。 + """ + target_date = datetime.now().date() - timedelta(days=offset) + target_dt = datetime(target_date.year, target_date.month, target_date.day) + return int(target_dt.timestamp() * 1000) + + +def _zto_find_visible_day(frame_locator, target_time): + """在双月日期控件中查找可见的日期格子。 + + jQuery Date Range Picker 双月视图下,同一天可能出现在两个面板中: + - month1(左面板)的溢出 ghost cell:display:none,不可见 + - month2(右面板)的正常 cell:可见 + + 同一日期在 DOM 中可能有毫秒级差异(零点 vs 23:59:59),遍历匹配并返回 + 第一个 visible 的;无可见匹配返回 None。 + """ + # 尝试两个时间变体:零点 和 23:59:59(部分 checked/selected 格用后者) + for time_variant in (target_time, target_time + 86399000): + sel = f"td div.day[time='{time_variant}']" + cells = frame_locator.locator(sel) + count = cells.count() + for i in range(count): + if cells.nth(i).is_visible(): + return cells.nth(i) + return None + + def _zto_flip_to_target_month(frame_locator, page, target_time, max_flips=12): """中通日历(jQuery-Date-Range-Picker 双月视图)跨月导航:目标日期不在当前视窗时, - 循环点 .prev 把目标月翻进 month1 视窗。offset 恒指向过去,故只往前翻;步长 1 月/次。 - 用 JS 派发点击(.evaluate("el=>el.click()"))避开 .date-range-length-tip 等 hover 遮挡。 + 循环点 .prev 把目标月翻进视窗。用 _zto_find_visible_day 判可见(跳过隐藏 ghost cell)。 返回 True 若目标格子最终可见。""" - sel = f"td div.day[time='{target_time}']" for _ in range(max_flips): - if frame_locator.locator(sel).first.is_visible(): + if _zto_find_visible_day(frame_locator, target_time) is not None: return True frame_locator.locator(".date-picker-wrapper .prev").first.evaluate( "el => el.click()" ) page.wait_for_timeout(450) - return frame_locator.locator(sel).first.is_visible() + return _zto_find_visible_day(frame_locator, target_time) is not None def zto_smart_menu_click(page, menu_path): @@ -170,32 +200,29 @@ def zto_expected_download_impl(page, force=False, date=None): 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") + # 直接从 Python datetime 计算目标时间戳,不再依赖 DOM real-today(双月视图 + # 下 real-today 可能同时出现在 month1 隐藏 ghost cell 和 month2 可见 cell, + # .first 会取到隐藏的那个导致 wait_for(visible) 超时)。 + target_time = _zto_compute_target_time(offset) + target_cell = _zto_find_visible_day(ewb_frame, target_time) - today_time_str = today_cell.get_attribute("time") - if today_time_str: - today_time = int(today_time_str) - target_time = today_time - offset * 86400000 - target_cell = ewb_frame.locator(f"td div.day[time='{target_time}']").first + if target_cell is None: + print(" ℹ️ 目标日期不在当前视窗,正在翻月导航 ...") + if not _zto_flip_to_target_month(ewb_frame, page, target_time): + raise RuntimeError( + f"翻月后仍无法定位目标日期格子(time={target_time})" + ) + target_cell = _zto_find_visible_day(ewb_frame, target_time) + if target_cell is None: + raise RuntimeError( + f"翻月后仍无法定位目标日期格子(time={target_time})" + ) - # 偏移日期跨月时目标格子不在当前双月视窗 → 向前翻月把它带进视窗,不再降级为当天 - if not target_cell.is_visible(): - print(" ℹ️ 偏移日期跨月,正在向前翻月导航到目标 ...") - if not _zto_flip_to_target_month(ewb_frame, page, target_time): - raise RuntimeError( - f"翻月后仍无法定位目标日期格子(time={target_time})" - ) - # 日期格子用 _dom_click 直接派发事件(同实到):.click() 会先 hover 格子,触发 - # “范围长度”提示气泡(.date-range-length-tip)盖住格子导致点击被遮挡超时, - # 跨月选中非今日格子时尤为明显。 - _dom_click(target_cell) - page.wait_for_timeout(300) - _dom_click(target_cell) - else: - today_cell.click() - page.wait_for_timeout(300) - today_cell.click() + # 日期格子用 _dom_click 直接派发事件:.click() 会先 hover 格子,触发 + # "范围长度"提示气泡(.date-range-length-tip)盖住格子导致点击被遮挡超时。 + _dom_click(target_cell) + page.wait_for_timeout(300) + _dom_click(target_cell) page.wait_for_timeout(500) @@ -422,31 +449,29 @@ def zto_actual_download_impl(page, date=None): 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") + # 直接从 Python datetime 计算目标时间戳,不再依赖 DOM real-today(双月视图 + # 下 real-today 可能同时出现在 month1 隐藏 ghost cell 和 month2 可见 cell, + # .first 会取到隐藏的那个导致 wait_for(visible) 超时)。 + target_time = _zto_compute_target_time(offset) + target_cell = _zto_find_visible_day(arr_frame, target_time) - today_time_str = today_cell.get_attribute("time") - if today_time_str: - today_time = int(today_time_str) - target_time = today_time - offset * 86400000 - target_cell = arr_frame.locator(f"td div.day[time='{target_time}']").first + if target_cell is None: + print(" ℹ️ 目标日期不在当前视窗,正在翻月导航 ...") + if not _zto_flip_to_target_month(arr_frame, page, target_time): + raise RuntimeError( + f"翻月后仍无法定位目标日期格子(time={target_time})" + ) + target_cell = _zto_find_visible_day(arr_frame, target_time) + if target_cell is None: + raise RuntimeError( + f"翻月后仍无法定位目标日期格子(time={target_time})" + ) - # 日期格子用 _dom_click 直接派发事件:Playwright 的 .click() 会先 hover 格子, - # 触发“范围长度”提示气泡(.date-range-length-tip)盖住格子,导致点击被判遮挡而超时。 - # 偏移日期跨月时目标格子不在当前双月视窗 → 向前翻月把它带进视窗,不再降级为当天。 - if not target_cell.is_visible(): - print(" ℹ️ 偏移日期跨月,正在向前翻月导航到目标 ...") - if not _zto_flip_to_target_month(arr_frame, page, target_time): - raise RuntimeError( - f"翻月后仍无法定位目标日期格子(time={target_time})" - ) - _dom_click(target_cell) - page.wait_for_timeout(300) - _dom_click(target_cell) - else: - _dom_click(today_cell) - page.wait_for_timeout(300) - _dom_click(today_cell) + # 日期格子用 _dom_click 直接派发事件:Playwright 的 .click() 会先 hover 格子, + # 触发"范围长度"提示气泡(.date-range-length-tip)盖住格子,导致点击被判遮挡而超时。 + _dom_click(target_cell) + page.wait_for_timeout(300) + _dom_click(target_cell) page.wait_for_timeout(500) @@ -695,7 +720,7 @@ def _zto_poll_and_download_tasks(page, export_times, download_dir, final_filenam ) # ==================================================================== - # 所有目标文件下载完成后,关闭“导出任务管理”标签页 + # 所有目标文件下载完成后,关闭"导出任务管理"标签页 # ==================================================================== print(">> 【导出任务管理】下载完成,正在关闭标签页...") try: