fix: 兼容非滚动PDD规格面板 (#154)

This commit is contained in:
chengma
2026-08-11 15:01:53 +08:00
parent 08565d976e
commit fee8210ebc
3 changed files with 177 additions and 3 deletions
+79 -3
View File
@@ -435,17 +435,80 @@ def _is_dimension_heading(label: str) -> bool:
# 新版页面会把可选数量写进标题,例如“颜色 (6)”或“颜色(6)”。
# 数量不是规格名称的一部分,只在判断标题类型时去掉,最终展示仍保留原文。
compact = re.sub(r"[((]\d+[))]$", "", compact)
# “确认款式”是新版规格弹层的标题,不是一个可选择的“款式”维度。
if compact in ("确认款式", "確認款式"):
return False
return compact in _DIMENSION_NAMES or compact.endswith(
("分类", "规格", "尺寸", "尺码", "颜色", "型号", "款式", "容量", "类型", "版本", "口味")
)
def _find_non_scrollable_spec_panel(
root: ET.Element,
parents: Mapping[ET.Element, ET.Element],
) -> Optional[ET.Element]:
"""用多项强证据定位不暴露 scrollable 属性的自绘规格面板。"""
headings: list[ET.Element] = []
summaries: list[ET.Element] = []
panel_cues: list[ET.Element] = []
confirms: list[ET.Element] = []
for node in root.iter("node"):
label = _preferred_node_label(node).strip()
compact = label.replace(" ", "")
if not label or _parse_bounds(node.get("bounds", "")) is None:
continue
if _is_dimension_heading(label):
headings.append(node)
if compact.startswith(("已选", "已選", "请选择", "請選擇")):
summaries.append(node)
if compact in ("确认款式", "確認款式", "关闭", "關閉"):
panel_cues.append(node)
if compact in ("确定", "確定") and node.get("clickable") == "true":
confirms.append(node)
if not headings or not summaries or not panel_cues or not confirms:
return None
required = [*headings, summaries[0], panel_cues[0], confirms[0]]
common = set([required[0], *_ancestors(required[0], parents)])
for node in required[1:]:
common.intersection_update([node, *_ancestors(node, parents)])
all_bounds = [
bounds
for node in root.iter("node")
if (bounds := _parse_bounds(node.get("bounds", ""))) is not None
]
screen_area = 0
if all_bounds:
screen_area = max(item[2] for item in all_bounds) * max(
item[3] for item in all_bounds
)
candidates: list[tuple[int, ET.Element]] = []
for node in common:
bounds = _parse_bounds(node.get("bounds", ""))
if bounds is None or node.get("visible-to-user", "true") != "true":
continue
area = (bounds[2] - bounds[0]) * (bounds[3] - bounds[1])
if screen_area and area >= screen_area * 0.95:
continue
candidates.append((area, node))
if not candidates:
return None
return min(candidates, key=lambda item: item[0])[1]
def _is_spec_panel_open(xml_data: str | bytes) -> bool:
"""判断规格面板是否已经出现,不要求当前视口已露出规格选项。"""
root = _parse_xml(xml_data)
labels = _all_labels(root)
_raise_special_page(labels)
parents = {child: parent for parent in root.iter() for child in parent}
if _find_non_scrollable_spec_panel(root, parents) is not None:
return True
has_scrollable_region = any(
node.get("scrollable") == "true"
and _parse_bounds(node.get("bounds", "")) is not None
@@ -536,11 +599,23 @@ def parse_spec_panel(xml_data: str | bytes) -> SpecSnapshot:
for node in root.iter("node")
if node.get("scrollable") == "true" and _parse_bounds(node.get("bounds", ""))
]
if not scrollables:
non_scrollable_outer = _find_non_scrollable_spec_panel(root, parents)
panel_scrollables = []
if non_scrollable_outer is not None:
panel_scrollables = [
node
for node in scrollables
if node is non_scrollable_outer
or non_scrollable_outer in _ancestors(node, parents)
]
outer_candidates = panel_scrollables or (
[non_scrollable_outer] if non_scrollable_outer is not None else scrollables
)
if not outer_candidates:
raise PddCollectError("PDD_DATA_SPEC_INCOMPLETE", "规格面板没有可识别的规格区域")
outer = max(
scrollables,
outer_candidates,
key=lambda node: (
(_parse_bounds(node.get("bounds", "")) or (0, 0, 0, 0))[2]
- (_parse_bounds(node.get("bounds", "")) or (0, 0, 0, 0))[0]
@@ -616,8 +691,9 @@ def parse_spec_panel(xml_data: str | bytes) -> SpecSnapshot:
dimensions.append(SpecDimension(key, name, tuple(values)))
selected_text = next((label for label in labels if label.startswith("已选")), None)
price_boundary = heading_nodes[0][0] if heading_nodes else outer_bounds[1]
price_cent, raw_price, list_price_cent = _price_from_nodes(
root.iter("node"), outer_bounds[1]
root.iter("node"), price_boundary
)
return SpecSnapshot(
tuple(dimensions), selected_text, price_cent, raw_price, list_price_cent