diff --git a/client/src/pdd_collect_service.py b/client/src/pdd_collect_service.py
index 6c391fa..b07dd0f 100644
--- a/client/src/pdd_collect_service.py
+++ b/client/src/pdd_collect_service.py
@@ -432,11 +432,42 @@ def _dimension_key(name: str, used: set[str]) -> str:
def _is_dimension_heading(label: str) -> bool:
compact = label.replace(" ", "")
+ # 新版页面会把可选数量写进标题,例如“颜色 (6)”或“颜色(6)”。
+ # 数量不是规格名称的一部分,只在判断标题类型时去掉,最终展示仍保留原文。
+ compact = re.sub(r"[((]\d+[))]$", "", compact)
return compact in _DIMENSION_NAMES or compact.endswith(
("分类", "规格", "尺寸", "尺码", "颜色", "型号", "款式", "容量", "类型", "版本", "口味")
)
+def _is_spec_panel_open(xml_data: str | bytes) -> bool:
+ """判断规格面板是否已经出现,不要求当前视口已露出规格选项。"""
+
+ root = _parse_xml(xml_data)
+ labels = _all_labels(root)
+ _raise_special_page(labels)
+ has_scrollable_region = any(
+ node.get("scrollable") == "true"
+ and _parse_bounds(node.get("bounds", "")) is not None
+ for node in root.iter("node")
+ )
+ if not has_scrollable_region:
+ return False
+
+ compact_labels = [label.replace(" ", "") for label in labels]
+ has_selection_summary = any(
+ label.startswith(("请选择", "已选"))
+ and any(word in label for word in ("颜色", "尺码", "规格", "款式"))
+ for label in compact_labels
+ )
+ has_submit_hint = any(
+ "提交订单" in label
+ and any(word in label for word in ("选择", "颜色", "尺码", "规格"))
+ for label in compact_labels
+ )
+ return has_selection_summary or has_submit_hint
+
+
def _top_level_clickable_options(
container: ET.Element,
parents: Mapping[ET.Element, ET.Element],
@@ -1027,7 +1058,7 @@ class PddCollectService:
if exc.code != "PDD_DATA_SPEC_INCOMPLETE":
raise
else:
- if snapshot.dimensions:
+ if snapshot.dimensions or _is_spec_panel_open(xml_data):
return snapshot
self._sleep(0.25)
raise PddCollectError(
diff --git a/client/test/fixtures/pdd_spec_panel_count_heading.xml b/client/test/fixtures/pdd_spec_panel_count_heading.xml
new file mode 100644
index 0000000..d24a939
--- /dev/null
+++ b/client/test/fixtures/pdd_spec_panel_count_heading.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/test/test_pdd_collect_service.py b/client/test/test_pdd_collect_service.py
index e20f167..c1a49e2 100644
--- a/client/test/test_pdd_collect_service.py
+++ b/client/test/test_pdd_collect_service.py
@@ -337,6 +337,9 @@ class PddCollectParserTest(unittest.TestCase):
def setUpClass(cls):
cls.home_xml = (FIXTURES / "pdd_goods_page.xml").read_text(encoding="utf-8")
cls.spec_xml = (FIXTURES / "pdd_spec_panel.xml").read_text(encoding="utf-8")
+ cls.count_heading_spec_xml = (
+ FIXTURES / "pdd_spec_panel_count_heading.xml"
+ ).read_text(encoding="utf-8")
def test_quantity_keeps_raw_value_and_approximate_flag(self):
result = parse_quantity("已拼1.2万+件")
@@ -385,6 +388,75 @@ class PddCollectParserTest(unittest.TestCase):
["M", "L"],
)
+ def test_parse_spec_panel_accepts_count_in_color_heading(self):
+ for heading in ("颜色 (6)", "颜色(6)"):
+ with self.subTest(heading=heading):
+ xml_data = self.count_heading_spec_xml.replace("颜色 (6)", heading)
+ result = parse_spec_panel(xml_data)
+ colors = next(
+ item for item in result.dimensions if item.key == "color"
+ )
+
+ self.assertEqual(colors.name, heading)
+ self.assertEqual(
+ [value.text for value in colors.values],
+ ["黑色", "红色", "蓝色", "紫色", "浅粉", "本色"],
+ )
+
+ def test_open_panel_without_visible_options_is_not_reported_as_timeout(self):
+ panel_xml = """
+
+
+
+
+
+ """
+ device = FakeCollectDevice(self.home_xml, panel_xml)
+ device.opened_url = "https://mobile.yangkeduo.com/goods.html?goods_id=123"
+ device.panel_open = True
+ clock = FakeClock()
+ service = PddCollectService(
+ PddDeviceService(lambda _serial: device),
+ "USB-001",
+ "client-001",
+ sleeper=clock.sleep,
+ monotonic=clock.monotonic,
+ spec_panel_timeout=1.0,
+ )
+
+ snapshot = service._wait_spec_panel(device)
+
+ self.assertEqual(snapshot.dimensions, ())
+ self.assertEqual(clock.sleeps, [])
+
+ def test_open_panel_with_incomplete_specs_reports_spec_incomplete(self):
+ panel_xml = """
+
+
+
+
+
+ """
+ device = FakeCollectDevice(self.home_xml, panel_xml)
+ service = PddCollectService(
+ PddDeviceService(lambda _serial: device),
+ "USB-001",
+ "client-001",
+ sleeper=lambda _seconds: None,
+ max_spec_swipes=0,
+ )
+
+ with self.assertRaises(PddCollectError) as raised:
+ service.collect(
+ FakeTask("https://mobile.yangkeduo.com/goods.html?goods_id=123")
+ )
+
+ self.assertEqual(raised.exception.code, "PDD_DATA_SPEC_INCOMPLETE")
+
def test_truncated_spec_name_prefers_full_content_description(self):
xml_data = self.spec_xml.replace(
'content-desc="红色" clickable="true"',