feat: 滚动采集评价数量和店铺名 (#40)

This commit is contained in:
chengma
2026-08-08 10:30:04 +08:00
parent 2f83be5e40
commit 9dadac2bff
4 changed files with 257 additions and 11 deletions
+135 -2
View File
@@ -60,6 +60,25 @@ class FakeCollectDevice:
self.swipe_panel_states.append(self.panel_open)
class GoodsMetadataScrollDevice(FakeCollectDevice):
"""模拟评价和店铺分别出现在商品页后续屏幕。"""
def __init__(self, pages, spec_xml):
super().__init__(pages[0], spec_xml)
self.pages = pages
self.page_index = 0
def dump_hierarchy(self):
if self.panel_open:
return self.spec_xml
return self.pages[self.page_index]
def swipe(self, *args, **kwargs):
super().swipe(*args, **kwargs)
if not self.panel_open:
self.page_index = min(self.page_index + 1, len(self.pages) - 1)
class LoadingDevice(FakeCollectDevice):
def dump_hierarchy(self):
return '<hierarchy><node text="正在加载" /></hierarchy>'
@@ -251,6 +270,25 @@ class PddCollectParserTest(unittest.TestCase):
self.assertTrue(result.sales.approximate)
self.assertEqual(result.reviews.value, 2356)
def test_parse_review_title_and_shop_name_next_to_enter_shop(self):
xml_data = """<hierarchy>
<node class="android.widget.FrameLayout" bounds="[0,0][1080,2000]">
<node class="android.widget.TextView" text="商品评价(191)"
bounds="[30,700][300,760]" />
<node class="android.widget.TextView" text="承诺假一赔十"
bounds="[30,1050][300,1110]" />
<node class="android.widget.TextView" text="小熊服饰店"
bounds="[300,1200][650,1260]" />
<node class="android.widget.TextView" text="进店"
bounds="[850,1200][1030,1260]" />
</node>
</hierarchy>"""
result = parse_goods_page(xml_data)
self.assertEqual(result.reviews.value, 191)
self.assertEqual(result.shop_name, "小熊服饰店")
def test_parse_spec_panel_uses_generic_dimensions_and_cents(self):
result = parse_spec_panel(self.spec_xml)
self.assertEqual(result.price_cent, 1000)
@@ -328,6 +366,70 @@ class PddCollectParserTest(unittest.TestCase):
self.assertEqual(data["source"]["device_address"], "USB-001")
self.assertIsNone(data["purchase"])
def test_collect_scrolls_before_spec_panel_and_accumulates_metadata(self):
initial_page = self.home_xml.replace(
'<node class="android.widget.TextView" text="2356条评价" '
'bounds="[330,950][600,1010]" visible-to-user="true" '
'enabled="true" />',
"",
).replace(
'<node class="android.widget.TextView" text="店铺:测试服饰旗舰店" '
'bounds="[30,1050][600,1110]" visible-to-user="true" '
'enabled="true" />',
"",
)
buy_button = """
<node class="android.view.ViewGroup" content-desc="¥10.00立即购买"
clickable="true" bounds="[500,1700][1080,1950]"
visible-to-user="true" enabled="true">
<node class="android.widget.TextView" text="¥10.00"
bounds="[650,1730][850,1810]"
visible-to-user="true" enabled="true" />
<node class="android.widget.TextView" text="立即购买"
bounds="[650,1820][900,1900]"
visible-to-user="true" enabled="true" />
</node>
"""
review_page = f"""<hierarchy>
<node class="android.widget.FrameLayout" bounds="[0,0][1080,2000]">
<node class="android.widget.TextView" text="商品评价(191)"
bounds="[30,700][300,760]" />
{buy_button}
</node>
</hierarchy>"""
shop_page = f"""<hierarchy>
<node class="android.widget.FrameLayout" bounds="[0,0][1080,2000]">
<node class="android.widget.TextView" text="小熊服饰店"
bounds="[300,1200][650,1260]" />
<node class="android.widget.TextView" text="进店"
bounds="[850,1200][1030,1260]" />
{buy_button}
</node>
</hierarchy>"""
device = GoodsMetadataScrollDevice(
[initial_page, review_page, shop_page],
keep_only_one_sku(self.spec_xml),
)
service = PddCollectService(
PddDeviceService(lambda _serial: device),
"USB-001",
"client-001",
sleeper=lambda _seconds: None,
max_spec_swipes=0,
)
result = service.collect(
FakeTask("https://mobile.yangkeduo.com/goods.html?goods_id=123")
)
self.assertEqual(result.title, "测试纯棉短袖商品")
self.assertEqual(result.sales.value, 12000)
self.assertEqual(result.reviews.value, 191)
self.assertEqual(result.shop_name, "小熊服饰店")
self.assertEqual(device.swipe_panel_states[:2], [False, False])
self.assertEqual(device.page_index, 2)
self.assertTrue(device.clicks)
def test_pdd_hierarchy_is_ready_even_when_focused_package_is_settings(self):
pdd_home_xml = self.home_xml.replace(
"<node ", '<node package="com.xunmeng.pinduoduo" '
@@ -593,7 +695,7 @@ class PddCollectParserTest(unittest.TestCase):
)
self.assertIsNone(result.shop_name)
def test_missing_reviews_does_not_scroll_before_opening_spec_panel(self):
def test_missing_reviews_stops_when_goods_page_no_longer_changes(self):
home_without_reviews = self.home_xml.replace(
'<node class="android.widget.TextView" text="2356条评价" '
'bounds="[330,950][600,1010]" visible-to-user="true" '
@@ -617,7 +719,38 @@ class PddCollectParserTest(unittest.TestCase):
self.assertIsNone(result.reviews.raw)
self.assertTrue(device.clicks)
self.assertTrue(all(device.swipe_panel_states))
self.assertEqual(device.swipe_panel_states[:2], [False, False])
def test_cancel_during_goods_page_scroll_stops_before_spec_panel(self):
incomplete_home = self.home_xml.replace(
'<node class="android.widget.TextView" text="2356条评价" '
'bounds="[330,950][600,1010]" visible-to-user="true" '
'enabled="true" />',
"",
).replace(
'<node class="android.widget.TextView" text="店铺:测试服饰旗舰店" '
'bounds="[30,1050][600,1110]" visible-to-user="true" '
'enabled="true" />',
"",
)
cancelled = {"value": False}
device = FakeCollectDevice(incomplete_home, self.spec_xml)
service = PddCollectService(
PddDeviceService(lambda _serial: device),
"USB-001",
"client-001",
sleeper=lambda _seconds: cancelled.update(value=True),
cancelled=lambda: cancelled["value"],
)
with self.assertRaises(PddCollectError) as raised:
service.collect(
FakeTask("https://mobile.yangkeduo.com/goods.html?goods_id=123")
)
self.assertEqual(raised.exception.code, "PDD_CANCELLED")
self.assertEqual(device.swipe_panel_states, [False])
self.assertEqual(device.clicks, [])
def test_spec_panel_timeout_has_stable_code_and_xml_evidence(self):
with tempfile.TemporaryDirectory() as directory: