perf: 优化颜色采集等待时间 (#44)

This commit is contained in:
chengma
2026-08-09 10:10:15 +08:00
parent 6d40c1e986
commit ed72e62eda
2 changed files with 238 additions and 29 deletions
+105 -29
View File
@@ -7,6 +7,7 @@
from __future__ import annotations
import hashlib
import math
import re
import time
import xml.etree.ElementTree as ET
@@ -638,6 +639,10 @@ class PddCollectService:
max_goods_page_swipes: int = 12,
max_spec_swipes: int = 12,
max_sku_count: int = 200,
color_poll_interval: float = 0.1,
color_selection_timeout: float = 0.6,
color_price_timeout: float = 1.2,
horizontal_swipe_settle_interval: float = 0.1,
artifact_directory: Optional[Path] = None,
) -> None:
self._device_service = device_service
@@ -654,6 +659,21 @@ class PddCollectService:
self._max_goods_page_swipes = max_goods_page_swipes
self._max_spec_swipes = max_spec_swipes
self._max_sku_count = max_sku_count
timing_values = {
"颜色轮询间隔": color_poll_interval,
"颜色选中超时": color_selection_timeout,
"颜色价格超时": color_price_timeout,
"水平滑动稳定间隔": horizontal_swipe_settle_interval,
}
for name, value in timing_values.items():
if value <= 0:
raise ValueError(f"{name}必须大于 0 秒")
self._color_poll_interval = color_poll_interval
self._color_selection_timeout = color_selection_timeout
self._color_price_timeout = color_price_timeout
self._horizontal_swipe_settle_interval = (
horizontal_swipe_settle_interval
)
self._artifact_directory = artifact_directory
self._last_goods_xml: Optional[str] = None
self._goods_screens_checked = 0
@@ -953,13 +973,15 @@ class PddCollectService:
current_row_colors: list[DimensionValue] = []
stable_edge_reads = 0
previous_signature: Optional[tuple[tuple[str, Bounds], ...]] = None
pending_xml: Optional[str] = None
for swipe_count in range(self._max_spec_swipes + 1):
self._check_cancelled()
# 每次只处理一个节点;点击可能让列表自动移动,下一项必须
# 从最新 XML 重新计算,不能继续使用点击前的旧坐标。
while True:
xml_data = self._dump_hierarchy(device)
xml_data = pending_xml or self._dump_hierarchy(device)
pending_xml = None
rows = self._visible_color_rows(xml_data)
if row_index >= len(rows):
raise PddCollectError(
@@ -998,7 +1020,7 @@ class PddCollectService:
if stable_edge_reads >= 2 or swipe_count >= self._max_spec_swipes:
break
root = _parse_xml(self._dump_hierarchy(device))
root = _parse_xml(xml_data)
region = self._horizontal_region(root)
if region is None:
break
@@ -1008,7 +1030,9 @@ class PddCollectService:
horizontal=True,
reverse=not move_right,
)
self._sleep(0.35)
pending_xml = self._wait_for_horizontal_change(
device, signature
)
# 操作采用蛇形以减少无效滑动;输出仍恢复成页面自然的
# “每行从左到右”顺序,方便 Admin 下拉框稳定展示。
@@ -1030,13 +1054,10 @@ class PddCollectService:
previous_signature: Optional[tuple[tuple[str, Bounds], ...]] = None
stable_edge_reads = 0
xml_data = self._dump_hierarchy(device)
for _ in range(self._max_spec_swipes):
self._check_cancelled()
xml_data = self._dump_hierarchy(device)
rows = self._visible_color_rows(xml_data)
signature = tuple(
(item.text, item.bounds) for row in rows for item in row
)
signature = self._color_view_signature(xml_data)
if signature == previous_signature:
stable_edge_reads += 1
else:
@@ -1052,7 +1073,7 @@ class PddCollectService:
self._swipe_region(
device, region, horizontal=True, reverse=True
)
self._sleep(0.35)
xml_data = self._wait_for_horizontal_change(device, signature)
def _click_and_sample_color(
self, device: Any, target: str
@@ -1080,11 +1101,20 @@ class PddCollectService:
(bounds[1] + bounds[3]) // 2,
)
selection_deadline = self._monotonic() + self._color_selection_timeout
selection_sleep_limit = math.ceil(
self._color_selection_timeout / self._color_poll_interval
)
selection_sleeps = 0
price_deadline: Optional[float] = None
price_sleep_limit = math.ceil(
self._color_price_timeout / self._color_poll_interval
)
price_sleeps = 0
previous_price: Optional[tuple[int, Optional[str], Optional[int]]] = None
stable_price_reads = 0
for _ in range(8):
while True:
self._check_cancelled()
self._sleep(0.2)
latest_xml = self._dump_hierarchy(device)
latest_root = _parse_xml(latest_xml)
snapshot = parse_spec_panel(latest_xml)
@@ -1097,28 +1127,74 @@ class PddCollectService:
# 只能以完整可见可点击节点未报错的点击作为降级证据。
selection_confirmed = selected or not selection_exposed
if snapshot.price_cent is None or not selection_confirmed:
now = self._monotonic()
if not selection_confirmed:
stable_price_reads = 0
previous_price = None
continue
current_price = (
snapshot.price_cent,
snapshot.raw_price,
snapshot.list_price_cent,
)
if current_price == previous_price:
stable_price_reads += 1
else:
stable_price_reads = 1
previous_price = current_price
if stable_price_reads >= 2:
return ColorPriceSample(
snapshot.price_cent,
snapshot.raw_price,
snapshot.list_price_cent,
selection_timed_out = (
now >= selection_deadline
or selection_sleeps >= selection_sleep_limit
)
if selection_timed_out:
return ColorPriceSample(None, None, None)
else:
if price_deadline is None:
price_deadline = now + self._color_price_timeout
if snapshot.price_cent is None:
stable_price_reads = 0
previous_price = None
else:
current_price = (
snapshot.price_cent,
snapshot.raw_price,
snapshot.list_price_cent,
)
if current_price == previous_price:
stable_price_reads += 1
else:
stable_price_reads = 1
previous_price = current_price
if stable_price_reads >= 2:
return ColorPriceSample(
snapshot.price_cent,
snapshot.raw_price,
snapshot.list_price_cent,
)
if now >= price_deadline or price_sleeps >= price_sleep_limit:
return ColorPriceSample(None, None, None)
return ColorPriceSample(None, None, None)
self._sleep(self._color_poll_interval)
if selection_confirmed:
price_sleeps += 1
else:
selection_sleeps += 1
def _color_view_signature(
self, xml_data: str | bytes
) -> tuple[tuple[str, Bounds], ...]:
"""返回颜色视口签名,用于判断水平滑动是否已经更新页面。"""
return tuple(
(item.text, item.bounds)
for row in self._visible_color_rows(xml_data)
for item in row
)
def _wait_for_horizontal_change(
self,
device: Any,
previous_signature: tuple[tuple[str, Bounds], ...],
) -> str:
"""水平滑动后短暂等待;首次未变化时只补等一次。"""
latest_xml = ""
for _ in range(2):
self._check_cancelled()
self._sleep(self._horizontal_swipe_settle_interval)
latest_xml = self._dump_hierarchy(device)
if self._color_view_signature(latest_xml) != previous_signature:
break
return latest_xml
def _collect_size_dimension(self, device: Any) -> Optional[SpecDimension]:
"""颜色采价完成后只滚动并收集尺码文字,不点击尺码。"""
+133
View File
@@ -239,6 +239,58 @@ class AllMissingColorPriceDevice(IgnoredColorClickDevice):
return super()._spec_xml().replace('selected="true"', 'selected="false"')
class NoColorPriceDevice(SnakeColorDevice):
"""颜色可以选中,但价格节点一直没有出现。"""
def _spec_xml(self):
price = self.prices[self.selected_color] / 100
return super()._spec_xml().replace(
f'text="¥{price:.2f}"', 'text="价格加载中"'
)
class DelayedHorizontalSwipeDevice(SnakeColorDevice):
"""水平滑动后第一次读取仍返回旧视口,第二次才更新。"""
def __init__(self, home_xml):
super().__init__(home_xml)
self.pending_page = None
self.old_page_reads = 0
def dump_hierarchy(self):
if self.panel_open and self.pending_page is not None:
if self.old_page_reads > 0:
self.old_page_reads -= 1
else:
self.page = self.pending_page
self.pending_page = None
return super().dump_hierarchy()
def swipe(self, x1, y1, x2, y2, duration=0.35):
self.swipes.append(((x1, y1, x2, y2), {"duration": duration}))
self.swipe_panel_states.append(self.panel_open)
if abs(x2 - x1) > abs(y2 - y1):
self.pending_page = 1 if x1 > x2 else 0
self.old_page_reads = 1
else:
self.sizes_visible = True
class FakeClock:
"""测试用时钟:sleep 只推进虚拟时间,不真的等待。"""
def __init__(self):
self.now = 0.0
self.sleeps = []
def monotonic(self):
return self.now
def sleep(self, seconds):
self.sleeps.append(seconds)
self.now += seconds
def keep_only_one_sku(xml_data: str) -> str:
"""从脱敏固件中删除蓝色和 L,只保留一个组合。"""
@@ -569,6 +621,87 @@ class PddCollectParserTest(unittest.TestCase):
self.assertIsNone(sample.price_cent)
def test_color_price_is_checked_immediately_then_stabilized(self):
device = SnakeColorDevice(self.home_xml)
device.panel_open = True
device.page = 0
clock = FakeClock()
service = PddCollectService(
PddDeviceService(lambda _serial: device),
"USB-001",
"client-001",
sleeper=clock.sleep,
monotonic=clock.monotonic,
)
sample = service._click_and_sample_color(device, "A色")
self.assertEqual(sample.price_cent, 1000)
self.assertEqual(clock.sleeps, [0.1])
self.assertEqual(device.clicked_colors, ["A色"])
def test_ignored_color_click_stops_at_selection_timeout(self):
device = IgnoredColorClickDevice(self.home_xml)
device.panel_open = True
device.page = 0
clock = FakeClock()
service = PddCollectService(
PddDeviceService(lambda _serial: device),
"USB-001",
"client-001",
sleeper=clock.sleep,
monotonic=clock.monotonic,
color_poll_interval=0.1,
color_selection_timeout=0.3,
)
sample = service._click_and_sample_color(device, "B色")
self.assertIsNone(sample.price_cent)
self.assertLessEqual(clock.now, 0.31)
self.assertEqual(len(device.clicks), 1)
def test_selected_color_without_price_stops_at_price_timeout(self):
device = NoColorPriceDevice(self.home_xml)
device.panel_open = True
device.page = 0
clock = FakeClock()
service = PddCollectService(
PddDeviceService(lambda _serial: device),
"USB-001",
"client-001",
sleeper=clock.sleep,
monotonic=clock.monotonic,
color_poll_interval=0.1,
color_price_timeout=0.3,
)
sample = service._click_and_sample_color(device, "A色")
self.assertIsNone(sample.price_cent)
self.assertLessEqual(clock.now, 0.31)
self.assertEqual(device.clicked_colors, ["A色"])
def test_horizontal_swipe_waits_once_more_when_view_is_unchanged(self):
device = DelayedHorizontalSwipeDevice(self.home_xml)
device.panel_open = True
device.page = 0
clock = FakeClock()
service = PddCollectService(
PddDeviceService(lambda _serial: device),
"USB-001",
"client-001",
sleeper=clock.sleep,
monotonic=clock.monotonic,
)
previous = service._color_view_signature(device.dump_hierarchy())
device.swipe(800, 800, 200, 800, duration=0.35)
latest_xml = service._wait_for_horizontal_change(device, previous)
self.assertNotEqual(service._color_view_signature(latest_xml), previous)
self.assertEqual(clock.sleeps, [0.1, 0.1])
def test_all_missing_color_prices_still_collects_sizes(self):
device = AllMissingColorPriceDevice(self.home_xml)
service = PddCollectService(