fix: 元素抓取生成精确选择器,无属性元素用锚点祖先限定;抖音点赞找不到按钮不再双击屏幕
- 元素选择器生成:无 resource-id/text 的元素改用最近有唯一属性祖先限定范围, 并带同 class 兄弟序号消歧(如 //*[@resource-id=".."]/RelativeLayout[3]), 不再给裸 class(如 //android.widget.ImageView 会误匹配其他元素) - 仍无法限定的宽泛选择器标记 broad,前端元素列表显示"⚠宽泛"提示 - 抖音 like 动作:by_element 找不到点赞按钮改为跳过,不再双击屏幕中央(避免误触)
This commit is contained in:
+49
-38
@@ -112,12 +112,16 @@ def get_elements(serial):
|
|||||||
return False, f"抓取失败: {e}"
|
return False, f"抓取失败: {e}"
|
||||||
|
|
||||||
|
|
||||||
def _flatten(node, out, depth=0, path=""):
|
def _flatten(node, out, depth=0, path="", ctx=None, tag_index=0):
|
||||||
"""递归扁平化元素树,提取关键属性供前端选择。
|
"""递归扁平化元素树,提取关键属性供前端选择。
|
||||||
|
|
||||||
uiautodev dump_hierarchy 返回 Node 格式:
|
uiautodev dump_hierarchy 返回 Node 格式:
|
||||||
{key, name, bounds(归一化浮点), rect({x,y,width,height}), properties({所有属性}), children}
|
{key, name, bounds(归一化浮点), rect({x,y,width,height}), properties({所有属性}), children}
|
||||||
其中 properties 包含原始 XML 属性(resource-id/text/content-desc/class/bounds 字符串等)。
|
其中 properties 包含原始 XML 属性(resource-id/text/content-desc/class/bounds 字符串等)。
|
||||||
|
|
||||||
|
ctx: 最近一个有唯一属性(resource-id/text/content-desc)祖先的 xpath。
|
||||||
|
无唯一属性的元素用它限定范围生成精确选择器,避免裸 class(如 //ImageView)过宽误点。
|
||||||
|
tag_index: 本节点在父节点同 class 兄弟中的序号(1-based),用于消歧。
|
||||||
"""
|
"""
|
||||||
if not isinstance(node, dict):
|
if not isinstance(node, dict):
|
||||||
return
|
return
|
||||||
@@ -128,51 +132,58 @@ def _flatten(node, out, depth=0, path=""):
|
|||||||
if not bounds_str and node.get("rect"):
|
if not bounds_str and node.get("rect"):
|
||||||
r = node["rect"]
|
r = node["rect"]
|
||||||
bounds_str = f"[{r['x']},{r['y']}][{r['x']+r['width']},{r['y']+r['height']}]"
|
bounds_str = f"[{r['x']},{r['y']}][{r['x']+r['width']},{r['y']+r['height']}]"
|
||||||
# 关键属性(前端选择器用)
|
|
||||||
|
rid = props.get("resource-id", "")
|
||||||
|
text = props.get("text", "")
|
||||||
|
desc = props.get("content-desc", "")
|
||||||
|
cls = props.get("class", "")
|
||||||
|
tag = cls.split(".")[-1] if cls else "*"
|
||||||
|
|
||||||
|
# 推荐选择器:唯一属性优先;否则用最近锚点祖先限定范围(带同class序号消歧)
|
||||||
|
if rid:
|
||||||
|
suggested = {"type": "xpath", "value": f'//*[@resource-id="{rid}"]'}
|
||||||
|
child_ctx = suggested["value"]
|
||||||
|
elif text:
|
||||||
|
suggested = {"type": "xpath", "value": f'//*[@text="{text}"]'}
|
||||||
|
child_ctx = suggested["value"]
|
||||||
|
elif desc:
|
||||||
|
suggested = {"type": "xpath", "value": f'//*[@content-desc="{desc}"]'}
|
||||||
|
child_ctx = suggested["value"]
|
||||||
|
else:
|
||||||
|
if ctx:
|
||||||
|
seg = f"/{tag}" + (f"[{tag_index}]" if tag_index > 1 else "")
|
||||||
|
suggested = {"type": "xpath", "value": f"{ctx}{seg}"}
|
||||||
|
child_ctx = f"{ctx}{seg}"
|
||||||
|
else:
|
||||||
|
# 无唯一属性且无锚点祖先:裸 class 过宽,标记 broad 让前端提示
|
||||||
|
suggested = {"type": "xpath", "value": f"//{tag}", "broad": True}
|
||||||
|
child_ctx = None
|
||||||
|
|
||||||
item = {
|
item = {
|
||||||
"depth": depth,
|
"depth": depth,
|
||||||
"path": path,
|
"path": path,
|
||||||
"name": name,
|
"name": name,
|
||||||
"resource_id": props.get("resource-id", ""),
|
"resource_id": rid,
|
||||||
"text": props.get("text", ""),
|
"text": text,
|
||||||
"description": props.get("content-desc", ""),
|
"description": desc,
|
||||||
"class": props.get("class", ""),
|
"class": cls,
|
||||||
"package": props.get("package", ""),
|
"package": props.get("package", ""),
|
||||||
"clickable": props.get("clickable", ""),
|
"clickable": props.get("clickable", ""),
|
||||||
"bounds": bounds_str,
|
"bounds": bounds_str,
|
||||||
# 推荐选择器(优先级:resource-id > text > description > class)
|
# 推荐选择器(resource-id > text > description > 锚点祖先限定class)
|
||||||
"suggested": _suggest_selector(props),
|
"suggested": suggested,
|
||||||
}
|
}
|
||||||
out.append(item)
|
out.append(item)
|
||||||
# 递归子节点
|
# 递归子节点,同时计算每个子节点在父下同 class 兄弟中的序号
|
||||||
children = node.get("children") or []
|
children = node.get("children") or []
|
||||||
for i, child in enumerate(children):
|
for i, child in enumerate(children):
|
||||||
_flatten(child, out, depth + 1, f"{path}/{i}")
|
cprops = child.get("properties") or {}
|
||||||
|
ccls = cprops.get("class", "")
|
||||||
|
ctag = ccls.split(".")[-1] if ccls else "*"
|
||||||
def _suggest_selector(attrs):
|
same = 1
|
||||||
"""根据属性推荐最佳选择器(前端一键填入用)。
|
for prev in children[:i]:
|
||||||
|
pcls = (prev.get("properties") or {}).get("class", "")
|
||||||
优先返回 xpath(最稳定,uiautomator2 原生支持),
|
ptag = pcls.split(".")[-1] if pcls else "*"
|
||||||
其次 resourceId / text / description / class。
|
if ptag == ctag:
|
||||||
"""
|
same += 1
|
||||||
cls = attrs.get("class", "")
|
_flatten(child, out, depth + 1, f"{path}/{i}", child_ctx, same)
|
||||||
rid = attrs.get("resource-id", "")
|
|
||||||
text = attrs.get("text", "")
|
|
||||||
desc = attrs.get("content-desc", "")
|
|
||||||
# 优先生成 xpath:resource-id 最稳定
|
|
||||||
if rid:
|
|
||||||
# 简短类名(去掉包前缀)
|
|
||||||
short_cls = cls.split(".")[-1] if cls else "*"
|
|
||||||
return {"type": "xpath",
|
|
||||||
"value": f'//*[@resource-id="{rid}"]'}
|
|
||||||
if text:
|
|
||||||
return {"type": "xpath",
|
|
||||||
"value": f'//*[@text="{text}"]'}
|
|
||||||
if desc:
|
|
||||||
return {"type": "xpath",
|
|
||||||
"value": f'//*[@content-desc="{desc}"]'}
|
|
||||||
if cls:
|
|
||||||
return {"type": "xpath",
|
|
||||||
"value": f'//{cls}'}
|
|
||||||
return {"type": "xpath", "value": "//*"}
|
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
两种方式:
|
两种方式:
|
||||||
by_element — 用 xpath 找右侧红心按钮点击(相对定位,不依赖视频卡片序号)
|
by_element — 用 xpath 找右侧红心按钮点击(相对定位,不依赖视频卡片序号)
|
||||||
double_click — 双击屏幕中央(兜底,不依赖元素,抖音"双击点赞"手势)
|
double_click — 双击屏幕中央(需明确选择此方式,抖音"双击点赞"手势)
|
||||||
|
|
||||||
定位器随抖音版本变,by_element 找不到会自动回退双击,不会卡住。
|
by_element 找不到点赞按钮时**跳过本次,不点屏幕**(避免直播/无按钮页误触)。
|
||||||
要适配新版本抖音:用 weditor 重新抓当前界面,调整下面定位逻辑。
|
要适配新版本抖音:用 weditor 重新抓当前界面,调整下面定位逻辑。
|
||||||
"""
|
"""
|
||||||
from core.actions import BaseAction, register_action, should_trigger
|
from core.actions import BaseAction, register_action, should_trigger
|
||||||
@@ -33,7 +33,10 @@ class LikeAction(BaseAction):
|
|||||||
if self._like_by_element(d):
|
if self._like_by_element(d):
|
||||||
worker.set_action("点赞(红心)")
|
worker.set_action("点赞(红心)")
|
||||||
return True
|
return True
|
||||||
# 找不到红心,回退双击
|
# 找不到点赞按钮:跳过本次,不点屏幕,避免误触(如直播/无点赞按钮的页面)
|
||||||
|
worker.set_action("点赞(未找到,跳过)")
|
||||||
|
return False
|
||||||
|
# 仅在明确选择 double_click 方式时才双击屏幕
|
||||||
ok = self._double_click_center(d)
|
ok = self._double_click_center(d)
|
||||||
if ok:
|
if ok:
|
||||||
worker.set_action("点赞(双击)")
|
worker.set_action("点赞(双击)")
|
||||||
|
|||||||
@@ -1888,6 +1888,7 @@ var _stepEditor={
|
|||||||
'<div class="ep-info"><div class="ep-name">'+indent+esc(el.name||el.class||'')+'</div>'+
|
'<div class="ep-info"><div class="ep-name">'+indent+esc(el.name||el.class||'')+'</div>'+
|
||||||
'<div class="ep-attrs">'+attrs.join('')+'</div></div>'+
|
'<div class="ep-attrs">'+attrs.join('')+'</div></div>'+
|
||||||
'<span class="ep-sel">'+esc(el.suggested.type)+'</span>'+
|
'<span class="ep-sel">'+esc(el.suggested.type)+'</span>'+
|
||||||
|
(el.suggested&&el.suggested.broad?' <span class="label label-warning" title="无唯一属性,选择器可能误匹配其他元素,建议选带文字/id 的元素">⚠宽泛</span>':'')+
|
||||||
'</div>';
|
'</div>';
|
||||||
}).join('');
|
}).join('');
|
||||||
document.getElementById('el-list').innerHTML=html||'<div class="se-empty">无元素</div>';
|
document.getElementById('el-list').innerHTML=html||'<div class="se-empty">无元素</div>';
|
||||||
|
|||||||
Reference in New Issue
Block a user