fix: 兼容带数量的规格面板标题 (#137)
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<hierarchy>
|
||||
<node class="android.widget.FrameLayout" bounds="[0,0][1080,2400]"
|
||||
visible-to-user="true" enabled="true">
|
||||
<node class="android.widget.TextView" text="券后 ¥0.03 起"
|
||||
bounds="[300,800][700,880]" visible-to-user="true" enabled="true" />
|
||||
<node class="android.widget.TextView" text="请选择: 颜色 尺码"
|
||||
bounds="[36,920][600,1000]" visible-to-user="true" enabled="true" />
|
||||
<node class="androidx.recyclerview.widget.RecyclerView"
|
||||
scrollable="true" bounds="[0,1077][1080,2079]"
|
||||
visible-to-user="true" enabled="true">
|
||||
<node class="android.widget.TextView" text="颜色 (6)"
|
||||
bounds="[36,1102][197,1163]" visible-to-user="true" enabled="true" />
|
||||
<node class="android.view.ViewGroup" content-desc="黑色" clickable="true"
|
||||
bounds="[36,1188][352,1591]" visible-to-user="true" enabled="true" />
|
||||
<node class="android.view.ViewGroup" content-desc="红色" clickable="true"
|
||||
bounds="[382,1188][698,1591]" visible-to-user="true" enabled="true" />
|
||||
<node class="android.view.ViewGroup" content-desc="蓝色" clickable="true"
|
||||
bounds="[728,1188][1044,1591]" visible-to-user="true" enabled="true" />
|
||||
<node class="android.view.ViewGroup" content-desc="紫色" clickable="true"
|
||||
bounds="[36,1621][352,2024]" visible-to-user="true" enabled="true" />
|
||||
<node class="android.view.ViewGroup" content-desc="浅粉" clickable="true"
|
||||
bounds="[382,1621][698,2024]" visible-to-user="true" enabled="true" />
|
||||
<node class="android.view.ViewGroup" content-desc="本色" clickable="true"
|
||||
bounds="[728,1621][1044,2024]" visible-to-user="true" enabled="true" />
|
||||
<node class="android.widget.TextView" text="尺码"
|
||||
bounds="[36,2074][126,2079]" visible-to-user="true" enabled="true" />
|
||||
</node>
|
||||
<node class="android.widget.TextView" text="选择颜色及尺码后,提交订单"
|
||||
bounds="[300,2200][1000,2300]" visible-to-user="true" enabled="true" />
|
||||
</node>
|
||||
</hierarchy>
|
||||
@@ -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 = """<hierarchy>
|
||||
<node class="android.widget.FrameLayout" bounds="[0,0][1080,2400]">
|
||||
<node text="请选择:颜色 尺码" bounds="[36,900][600,980]" />
|
||||
<node class="androidx.recyclerview.widget.RecyclerView"
|
||||
scrollable="true" bounds="[0,1077][1080,2079]" />
|
||||
<node text="选择颜色及尺码后,提交订单"
|
||||
bounds="[300,2200][1000,2300]" />
|
||||
</node>
|
||||
</hierarchy>"""
|
||||
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 = """<hierarchy>
|
||||
<node class="android.widget.FrameLayout" bounds="[0,0][1080,2400]">
|
||||
<node text="请选择:颜色 尺码" bounds="[36,900][600,980]" />
|
||||
<node class="androidx.recyclerview.widget.RecyclerView"
|
||||
scrollable="true" bounds="[0,1077][1080,2079]" />
|
||||
<node text="选择颜色及尺码后,提交订单"
|
||||
bounds="[300,2200][1000,2300]" />
|
||||
</node>
|
||||
</hierarchy>"""
|
||||
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"',
|
||||
|
||||
Reference in New Issue
Block a user