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"',