fix: 识别带价格的纵向套餐续页 (#168)
This commit is contained in:
@@ -1740,9 +1740,12 @@ class PddCollectService:
|
|||||||
size_name = dimension.name
|
size_name = dimension.name
|
||||||
size_found = True
|
size_found = True
|
||||||
for value in dimension.values:
|
for value in dimension.values:
|
||||||
if self._is_second_dimension_action(value.text):
|
option_text = self._second_dimension_option_text(value.text)
|
||||||
|
if option_text is None:
|
||||||
continue
|
continue
|
||||||
sizes[value.text] = sizes.get(value.text, False) or value.available
|
sizes[option_text] = (
|
||||||
|
sizes.get(option_text, False) or value.available
|
||||||
|
)
|
||||||
|
|
||||||
if size_found:
|
if size_found:
|
||||||
break
|
break
|
||||||
@@ -1827,9 +1830,10 @@ class PddCollectService:
|
|||||||
)
|
)
|
||||||
size_name = size_dimension.name
|
size_name = size_dimension.name
|
||||||
for value in size_dimension.values:
|
for value in size_dimension.values:
|
||||||
if self._is_second_dimension_action(value.text):
|
option_text = self._second_dimension_option_text(value.text)
|
||||||
|
if option_text is None:
|
||||||
continue
|
continue
|
||||||
sizes[value.text] = sizes.get(value.text, False) or value.available
|
sizes[option_text] = sizes.get(option_text, False) or value.available
|
||||||
|
|
||||||
signature = self._second_dimension_signature(xml_data, size_dimension)
|
signature = self._second_dimension_signature(xml_data, size_dimension)
|
||||||
stable_edge_reads = (
|
stable_edge_reads = (
|
||||||
@@ -1928,14 +1932,24 @@ class PddCollectService:
|
|||||||
"""返回已确认的第二规格签名,不要求当前屏仍显示规格标题。"""
|
"""返回已确认的第二规格签名,不要求当前屏仍显示规格标题。"""
|
||||||
|
|
||||||
root = _parse_xml(xml_data)
|
root = _parse_xml(xml_data)
|
||||||
|
targets = {
|
||||||
|
normalized
|
||||||
|
for value in dimension.values
|
||||||
|
if (normalized := self._second_dimension_option_text(value.text))
|
||||||
|
is not None
|
||||||
|
}
|
||||||
result = []
|
result = []
|
||||||
for value in dimension.values:
|
for node in root.iter("node"):
|
||||||
node = self._find_option_node(root, value.text)
|
if node.get("clickable") != "true":
|
||||||
if node is None:
|
continue
|
||||||
|
text = self._second_dimension_option_text(
|
||||||
|
_preferred_or_descendant_label(node).strip()
|
||||||
|
)
|
||||||
|
if text not in targets:
|
||||||
continue
|
continue
|
||||||
bounds = _parse_bounds(node.get("bounds", ""))
|
bounds = _parse_bounds(node.get("bounds", ""))
|
||||||
if bounds is not None:
|
if bounds is not None:
|
||||||
result.append((value.text, bounds))
|
result.append((text, bounds))
|
||||||
return tuple(result)
|
return tuple(result)
|
||||||
|
|
||||||
def _build_second_dimension_context(
|
def _build_second_dimension_context(
|
||||||
@@ -2009,13 +2023,11 @@ class PddCollectService:
|
|||||||
continue
|
continue
|
||||||
text = _preferred_or_descendant_label(node).strip()
|
text = _preferred_or_descendant_label(node).strip()
|
||||||
bounds = _parse_bounds(node.get("bounds", ""))
|
bounds = _parse_bounds(node.get("bounds", ""))
|
||||||
|
option_text = self._second_dimension_option_text(text)
|
||||||
if (
|
if (
|
||||||
not text
|
option_text is None
|
||||||
or text in seen
|
or option_text in seen
|
||||||
or bounds is None
|
or bounds is None
|
||||||
or self._is_second_dimension_action(text)
|
|
||||||
or bounds[2] - bounds[0]
|
|
||||||
>= (container_bounds[2] - container_bounds[0]) * 0.85
|
|
||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
if text.endswith(("…", "...")):
|
if text.endswith(("…", "...")):
|
||||||
@@ -2023,8 +2035,8 @@ class PddCollectService:
|
|||||||
"PDD_DATA_SKU_NAME_TRUNCATED",
|
"PDD_DATA_SKU_NAME_TRUNCATED",
|
||||||
f"规格名称被截断,无法安全采集:{text}",
|
f"规格名称被截断,无法安全采集:{text}",
|
||||||
)
|
)
|
||||||
seen.add(text)
|
seen.add(option_text)
|
||||||
values.append(DimensionValue(text, _is_available(node)))
|
values.append(DimensionValue(option_text, _is_available(node)))
|
||||||
if not values:
|
if not values:
|
||||||
return None
|
return None
|
||||||
return SpecDimension("size", context.name, tuple(values))
|
return SpecDimension("size", context.name, tuple(values))
|
||||||
@@ -2078,13 +2090,11 @@ class PddCollectService:
|
|||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _is_second_dimension_action(text: str) -> bool:
|
def _second_dimension_option_text(text: str) -> Optional[str]:
|
||||||
"""排除规格面板中容易和选项结构相同的操作按钮。"""
|
"""返回去掉尾部价格的规格文字;操作项或纯价格返回 ``None``。"""
|
||||||
|
|
||||||
compact = text.replace(" ", "")
|
compact = text.replace(" ", "")
|
||||||
if "¥" in compact or "¥" in compact:
|
if not compact or compact.startswith(("已选", "请选择")) or any(
|
||||||
return True
|
|
||||||
return compact.startswith(("已选", "请选择")) or any(
|
|
||||||
marker in compact
|
marker in compact
|
||||||
for marker in (
|
for marker in (
|
||||||
"增加数量",
|
"增加数量",
|
||||||
@@ -2097,7 +2107,19 @@ class PddCollectService:
|
|||||||
"确认购买",
|
"确认购买",
|
||||||
"关闭",
|
"关闭",
|
||||||
)
|
)
|
||||||
)
|
):
|
||||||
|
return None
|
||||||
|
if re.fullmatch(
|
||||||
|
r"(?:券后|到手价|拼单价|单买价|价格)?[¥¥]\d+(?:\.\d{1,2})?",
|
||||||
|
compact,
|
||||||
|
):
|
||||||
|
return None
|
||||||
|
normalized = re.sub(
|
||||||
|
r"\s*[¥¥]\s*\d+(?:\.\d{1,2})?\s*$",
|
||||||
|
"",
|
||||||
|
text,
|
||||||
|
).strip()
|
||||||
|
return normalized or None
|
||||||
|
|
||||||
def _second_dimension_region(
|
def _second_dimension_region(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -512,7 +512,7 @@ class HorizontalPackageDevice:
|
|||||||
class VerticalLongPackageDevice:
|
class VerticalLongPackageDevice:
|
||||||
"""模拟纵向长套餐:滑到第二屏后“套餐”标题不再出现在控件树。"""
|
"""模拟纵向长套餐:滑到第二屏后“套餐”标题不再出现在控件树。"""
|
||||||
|
|
||||||
packages = tuple(f"套餐{i}" for i in range(1, 11))
|
packages = tuple(f"套餐{i}" for i in range(1, 17))
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.page = 0
|
self.page = 0
|
||||||
@@ -528,14 +528,15 @@ class VerticalLongPackageDevice:
|
|||||||
<node text="款式" bounds="[36,720][180,780]" />
|
<node text="款式" bounds="[36,720][180,780]" />
|
||||||
<node text="口罩款" clickable="true" selected="true"
|
<node text="口罩款" clickable="true" selected="true"
|
||||||
bounds="[36,800][300,900]" />
|
bounds="[36,800][300,900]" />
|
||||||
<node text="套餐(10)" bounds="[36,980][240,1040]" />
|
<node text="套餐(16)" bounds="[36,980][240,1040]" />
|
||||||
"""
|
"""
|
||||||
options = []
|
options = []
|
||||||
for index, text in enumerate(visible):
|
for index, text in enumerate(visible):
|
||||||
top = 1080 + index * 210
|
top = 1080 + index * 210
|
||||||
|
right = 500 if self.page == 0 else 1015
|
||||||
options.append(
|
options.append(
|
||||||
f'<node text="{text}" clickable="true" '
|
f'<node text="{text} ¥5.69" clickable="true" '
|
||||||
f'bounds="[36,{top}][500,{top + 150}]" />'
|
f'bounds="[36,{top}][{right},{top + 150}]" />'
|
||||||
)
|
)
|
||||||
return f"""<hierarchy>
|
return f"""<hierarchy>
|
||||||
<node bounds="[0,0][1080,2340]">
|
<node bounds="[0,0][1080,2340]">
|
||||||
@@ -544,6 +545,8 @@ class VerticalLongPackageDevice:
|
|||||||
<node text="已选:套餐1" bounds="[36,660][500,710]" />
|
<node text="已选:套餐1" bounds="[36,660][500,710]" />
|
||||||
{headings}
|
{headings}
|
||||||
{''.join(options)}
|
{''.join(options)}
|
||||||
|
<node text="券后 ¥5.69" clickable="true"
|
||||||
|
bounds="[36,1880][300,1940]" />
|
||||||
<node text="增加数量" clickable="true"
|
<node text="增加数量" clickable="true"
|
||||||
bounds="[36,1950][220,2050]" />
|
bounds="[36,1950][220,2050]" />
|
||||||
<node text="提交订单" clickable="true"
|
<node text="提交订单" clickable="true"
|
||||||
@@ -557,7 +560,7 @@ class VerticalLongPackageDevice:
|
|||||||
if abs(y2 - y1) <= abs(x2 - x1):
|
if abs(y2 - y1) <= abs(x2 - x1):
|
||||||
return
|
return
|
||||||
if y1 > y2:
|
if y1 > y2:
|
||||||
self.page = min(self.page + 1, 2)
|
self.page = min(self.page + 1, 4)
|
||||||
else:
|
else:
|
||||||
self.page = max(self.page - 1, 0)
|
self.page = max(self.page - 1, 0)
|
||||||
|
|
||||||
@@ -1323,13 +1326,15 @@ class PddCollectParserTest(unittest.TestCase):
|
|||||||
result = service._collect_size_dimension(device)
|
result = service._collect_size_dimension(device)
|
||||||
|
|
||||||
self.assertIsNotNone(result)
|
self.assertIsNotNone(result)
|
||||||
self.assertEqual(result.name, "套餐(10)")
|
self.assertEqual(result.name, "套餐(16)")
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
[value.text for value in result.values],
|
[value.text for value in result.values],
|
||||||
list(device.packages),
|
list(device.packages),
|
||||||
)
|
)
|
||||||
self.assertNotIn("增加数量", [value.text for value in result.values])
|
self.assertNotIn("增加数量", [value.text for value in result.values])
|
||||||
self.assertNotIn("提交订单", [value.text for value in result.values])
|
self.assertNotIn("提交订单", [value.text for value in result.values])
|
||||||
|
self.assertNotIn("券后", [value.text for value in result.values])
|
||||||
|
self.assertTrue(all("¥" not in value.text for value in result.values))
|
||||||
self.assertEqual(device.clicks, [])
|
self.assertEqual(device.clicks, [])
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
all(abs(y2 - y1) > abs(x2 - x1) for x1, y1, x2, y2, _ in device.swipes)
|
all(abs(y2 - y1) > abs(x2 - x1) for x1, y1, x2, y2, _ in device.swipes)
|
||||||
|
|||||||
Reference in New Issue
Block a user