ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

UI自动化中无id无text控件如何定位与点击?手把手实战指南

UI自动化中无id无text控件如何定位与点击?手把手实战指南 写自动化脚本最烦的不是逻辑写不出来而是目标控件就在你眼前但拿不到它的唯一标识。id是空的text是空的content-desc也是空的打开控件树一看整个节点光秃秃的就剩一个class和一个坐标框——这种场景我碰到过不下几十次每次都有人跑来问这玩意儿到底怎么才能点得动。这篇文章就专门聊这件事。如果你是刚接触UI自动化、或者写了段时间脚本但一直在靠写死坐标硬撑的朋友这篇内容会把没有id、没有text的控件如何定位、如何点击说清楚。我不光讲方案还会讲清楚每种方案用在什么场景、背后是什么原理、以及我自己踩过的坑。重点是看完之后你能直接拿去用而不是记住一堆用不上的API名词。1. 先摸清家底没有id和text时控件树上还剩哪些牌可打很多人一打开Appium Inspector或者uiautomator的控件树看到目标节点只有class和bounds就开始慌觉得这控件没法定位了。其实不是没法定位是你还没把手里的牌数清楚。一个Android原生控件节点上通常能看到这些字段resource-id也就是我们常说的idtext文本内容content-desc无障碍描述class控件类型packagebounds控件在屏幕上的坐标边界index同级兄弟节点中的序号以及一部分状态属性比如clickable、checked、selected、enabled关键点在于id和text只是定位属性里的两个选项不是全部。就算这俩字段都是空的只要class和bounds有值再加上它在整个控件树里的位置关系就足够我们把它精准勾出来了。甚至有些场景里你真正需要的不是这一层节点的属性而是它和旁边那些有属性的节点之间是什么关系——这个思路会在后面详细说。那为什么会有控件没有id也没有text我遇到的大致是这几类自定义View很多App为了视觉效果会用自定义控件重绘UI尤其是那些看起来像个按钮、其实内部完全自绘的元素。开发者没给它设idtext更是无从谈起。第三方SDK和广告组件开屏广告、弹窗、悬浮球这类东西很多是外部SDK渲染出来的资源id经常被混淆或者干脆不暴露。WebView或Flutter渲染的界面这类界面里很多元素并不是原生控件uiautomator拿到的只是一个Android WebView节点或者Flutter View节点内部的东西根本看不到。这种情况靠原生定位属性基本没戏得换思路。所以第一步永远是同一件事先把控件树完整dump出来把这个节点的所有属性都看一遍再顺便看看它的父节点、子节点、兄弟节点有哪些可用信息。只看一两项属性就下结论说定位不了太亏了。2. 相对定位才是主力从父节点和兄弟节点绕道确认目标一个没有id、没有text的控件通常不是孤立存在的。它旁边往往有带文本的label或者在它之上有一个带id的容器。这时候最靠谱的做法就是绕道——先定位到能定位的节点再通过节点关系找到目标。我在实践里最常用的两条路一是父节点或者兄弟节点借力二是写组合条件的XPath。2.1 先找能定位的父节点或兄弟节点举一个我实际处理过的例子。某个App的个人中心页用户头像右上角有个小圆点表示有未读消息。这个圆点纯粹是自定义绘制的id为空、text为空、content-desc为空class看起来是一堆view里面套着的小view。但它父节点恰好有个resource-idcom.xxx:id/user_info_layout。我在Appium里就直接用父节点往下找子节点# 先定位到可用的父节点 parent driver.find_element(By.ID, com.xxx:id/user_info_layout) # 通过XPath取父节点下方的第一个子节点 target parent.find_element(By.XPATH, ./*[1]) target.click()这里的./*[1]可能稍微有点挑但思路是对的父节点有id就用父节点兄弟节点有text就定位兄弟节点再往上或往下偏移一层。控件树只要层级结构稳定哪怕是绝对定位属性全空你也能靠位置关系把它找到。2.2 XPath组合条件把class、index、相对位置全部用上如果父节点和兄弟节点也不够用那就得写更长的XPath。这种写法的核心思路是单个属性不够定位时用多个属性组合限定出唯一目标。拿一个Android原生控件树举例目标是一个没有id和text的ImageView但它在某个Container下的第2个位置driver.find_element(By.XPATH, //*[resource-idcom.xxx:id/btn_container]/android.widget.ImageView[2])还有一种常见做法是直接按class和instance走driver.find_element(By.XPATH, //android.widget.ImageView[instance3])instance这属性其实就是节点在同class兄弟里的序号说白了就是旧版的index。用的时候要小心如果布局是动态变化的instance很容易因为前面多了一个同类节点而整体偏移。我在一个列表页就吃过这个亏——目标在第三个item里结果下拉刷新之后多了一条记录第四个位置变成了目标脚本直接就点错了。所以我的建议是能用静态文本或id锚定的部分尽量用静态锚点只有万不得已才用index或instance。比如上面那个例子更好的写法是先把整个列表容器定位出来再在容器内部去找那个没有id的节点而不是直接从全局树里数第几个。2.3 组合定位的优先级次序我自己在写脚本时心里是有一个优先级排列的遇到陌生控件也按这个顺序来resource-id—— 最稳定优先用。text或content-desc—— 只要固定就比坐标强。可定位的父节点 相对位置child、sibling—— 次优靠结构关系。混合XPathclass index 关系限定—— 能用但注意动态变化。bounds坐标 —— 放在最后必须动态获取。这个顺序的核心逻辑是离硬编码越远稳定性越高。一个控件如果今天在屏幕左边、明天因为不同分辨率跑到右边写死坐标就废了而结构关系不会变。3. 当原生属性彻底失灵图像识别与OCR的引入时机总有一些场景你会发现控件树里连目标节点的影子都找不到。最常见的就是WebView渲染的H5按钮、Flutter的自绘UI、以及游戏引擎渲染的界面。这些东西在uiautomator眼里就是一块光秃秃的画布你想从控件树里找窗体上的立即登录按钮是根本找不到的。两条路可以走模板匹配和OCR文字识别。3.1 模板匹配适合图标、无文字按钮模板匹配的原理不复杂你先截一张目标小图比如那个没有id的号按钮然后在当前屏幕截图上搜索与这个小图最相似的区域返回命中位置的坐标脚本去点那个坐标。在小项目里最省事的是直接用Airtest的Templatefrom airtest.core.api import * touch(Template(plus_button.png, threshold0.8))threshold是相似度阈值0.8是一个我自己常用的起步值。阈值设太高容易找不到设太低容易误点。实际用下来如果目标按钮有轻微阴影、背景色变化或不同分辨率的缩放建议先试0.7到0.8这个区间。原理层面如果不想上Airtest直接用OpenCV的matchTemplate也可以import cv2 screen cv2.imread(screen.png) template cv2.imread(button.png) result cv2.matchTemplate(screen, template, cv2.TM_CCOEFF_NORMED) min_val, max_val, min_loc, max_loc cv2.minMaxLoc(result)但这里有个坑OpenCV的模板匹配对缩放非常敏感。同一个小图换个分辨率的设备可能就差那么零点零几个相似度就找不到了。所以如果你要在多台不同分辨率的设备上跑建议对目标图做多尺度缩放后再匹配工程量大一点但稳定性会高很多。3.2 OCR适合有文字但在控件树里不可见的场景另一种情况是按钮上明明有字但控件树里看不到比如游戏里的文字按钮、Flash风格的界面、某些加密WebView。这时候就用OCR把整张屏幕截图的文字全部识别出来拿到立即领取这四个字的坐标然后点过去。我常用的是PaddleOCR中文识别效果比Tesseract默认模式好不少from paddleocr import PaddleOCR ocr PaddleOCR(use_angle_clsTrue, langch) result ocr.ocr(screen.png, clsTrue)识别结果里有文字内容和对应的四角坐标取中心点去点击就行。实际上很多自动化平台的按文字找控件功能底层就是集成OCR因为原生控件树里压根拿不到这些信息。3.3 图像方案的三个大坑图像识别这条路我是经历过社会毒打的几个大坑先给你排好背景变化会误识别如果按钮周围的背景是动态的比如是轮播图、会变色的广告位匹配相似度会剧烈波动今天0.95明天0.6。最好把目标小图裁剪得尽量贴合按钮本身别带太多背景。分辨率不同要重新截图Airtest虽然做了多分辨率处理但实际效果有限跨分辨率横跳时我建议重新截模板图。识别到不代表能点到有些按钮的响应区域并不等于视觉区域尤其是有动画缩放的控件视觉位置和命中区域可能差出几十像素。这种情况下我会用cv2把命中坐标先打点看一下确认颜色在那个区域是正常的再放脚本跑。图像识别本身不是银弹它只是原生定位体系之外的兜底方案。能用控件树解决的就不要上图像图像只用来解决树里根本没有这个节点的情形。4. 坐标方案的正确打开方式动态bounds而非裸写坐标说来也讽刺坐标是很多小白最先会的定位方式却在资深工程师这里被称为最后手段。倒不是坐标绝对不能用而是大家太容易把坐标给写死了写成一串奇怪的魔法数字——# 反面教材 driver.tap(540, 1200)这段代码的问题在于一旦设备分辨率变化、或者页面出现1px的偏移它就失灵。而且它没有任何容错能力你根本不知道这个点为什么在这下次改起来也无从下手。正确的坐标方案应该是让脚本自己从bounds里读出坐标而不是你替它把坐标算好写死。比如Android原生控件树里的节点会带一个bounds[120,500][240,600]这表示控件的左上角和右下角坐标。在Appium里可以直接这样动态拿中心点# 拿到这个没有id的节点的bounds属性 bounds_attr driver.find_element(By.XPATH, //android.widget.ImageView[3]).get_attribute(bounds) # 解析出中心点 import re nums list(map(int, re.findall(r\d, bounds_attr))) center_x (nums[0] nums[2]) // 2 center_y (nums[1] nums[3]) // 2 driver.tap(center_x, center_y)这么做的好处是即使按钮在不同页面里位置变了只要这个节点本身能被定位到哪怕靠class和index脚本每次都能取到它最新的坐标再点击而不是点一个曾经对过的死位置。还有一点容易被忽略点击前和点击后节点的bounds可能都会变。比如按钮点击后有按压动画按下瞬间坐标框会略有位移。如果你是对一个动画按钮做连续点击最好每次都重新获取一次bounds不要复用上一次的值尤其是那种点一下会展开新布局的折叠菜单。说到坐标方案还有一个绕不开的场景——自绘UI和游戏界面。在Unity、Cocos、自研引擎渲染的界面里控件树基本指望不上大部分脚本只能配合图像识别或者纯坐标计算去做。这种情况里坐标不是一个万不得已的方案而是唯一方案。但即便如此也强烈建议坐标为动态计算得来。举个例子右下角有个圆形按钮你可以通过屏幕宽高计算出它的圆心位置而不是写死一个坐标。这样换一台设备公式不变位置依然正确width, height driver.get_window_size() center_x int(width * 0.88) center_y int(height * 0.92) driver.tap(center_x, center_y)屏幕尺寸变了比例不变点依然能命中。这比写死(1090, 2200)要稳太多了。5. 一个真实排查案例开屏广告关闭按钮是怎么被点中的理论说了这么多我拿一个完整的案例给你顺一遍思路。之前帮朋友调一个App的自动签到脚本卡在启动后的开屏广告。广告右上角有个跳过文字按钮但在控件树里压根找不到任何文本节点整个界面只有一个大大的AdContainer容器id还是混淆过的乱码。5.1 初次尝试控件树路径直接失败我先后台dump了一下控件树adb shell uiautomator dump /sdcard/window_dump.xml adb pull /sdcard/window_dump.xml ./ui.xml打开一看广告区域是一个大的FrameLayout里面嵌套了几个空的View节点你要找的跳过按钮在树里根本不存在——大概率是平台SDK用自绘方式渲染的没接入原生控件体系。所以第一步就得出结论原生定位属性id/text/class组合这条路走不通就别硬磨了赶紧换赛道。5.2 第二步尝试OCR识别跳过文字我截了一张广告页的全屏图用PaddleOCR跑了一遍顺利识别出了跳过两个字和它的坐标。中心点大约在右上角。这里有个很关键的细节跑OCR之前先检查这次截图的尺寸和分辨率。因为OCR识别出来的是像素坐标如果你的脚本是在模拟器上跑但截图用的是设备真实分辨率要和Appium里的坐标映射保持一致不然点过去是偏的。确认坐标体系一致后我写了个临时脚本点过去结果出现了两个问题第一次点击跳过按钮弹出了一个小浮层问确定要跳过吗浮层上的确定按钮同样是自绘的没有id和text。这下可好一个广告里两个按钮都指向图像识别路线。5.3 最终方案OCR两次识别 动态坐标点击我干脆把流程改成先识别跳过文字→点击→等待0.5秒→再识别确定文字→点击。为了保证第二次识别不被干扰我对比了两次截图只保留弹窗出现后新增的OCR结果避免把底层广告页的跳过文字又检出一次。配合上前面说的动态坐标最终的代码长这样import time from paddleocr import PaddleOCR from appium import webdriver ocr PaddleOCR(use_angle_clsTrue, langch) def tap_text(target_text): driver.save_screenshot(current.png) result ocr.ocr(current.png, clsTrue) for line in result: text line[1][0] if target_text in text: box line[0] # 四个角坐标 center_x int((box[0][0] box[2][0]) / 2) center_y int((box[0][1] box[2][1]) / 2) driver.tap(center_x, center_y) return True return False tap_text(跳过) time.sleep(0.5) tap_text(确定)这套方案在本地跑了十几次基本都能稳定绕过广告。虽然每次要截两张图、跑两次OCR耗时比原生定位多出几百毫秒但胜在能跑通且不需要维护一堆坐标常量。这类业务脚本稳定性比速度重要得多。6. 常用框架下的写法差异Appium、Airtest、Windows UIA各有脾气同样是没有id、没有text的控件在不同自动化框架里解决手段的侧重点其实不太一样。我把平时用得多的三个环境分别说一下。6.1 Appium生态XPath和mobile find比较吃香Appium里除了常规的By.ID、By.XPATH还有一个好消息WebView的H5元素只要切对context也能用通常的selector定位。如果你发现原生的id和text找不到控件但你在Chromedriver的context里却能看到DOM节点那直接切context然后按CSS/class去定位就行。实际操作里不少找不到控件的问题最后其实是在context切换这一步绕坑的。6.2 Airtest/Poco层级关系就是命根子Airtest生态里Poco定位控件的语法和Appium很不一样。Poco不需要写一长串XPath它的核心是poco()函数 父子关系链from poco.drivers.android.uiautomation import AndroidUiautomationPoco poco AndroidUiautomationPoco(use_airtest_inputTrue) target poco(com.xxx:id/container).child(android.view.View)[1] target.click()如果没有id用text也很简单poco(text跳过).click()连text都没有的时候就用class索引。Poco还有一个不错的点是offspring接口可以从父节点一路往下找孙节点比一层层写child要省事。实际经验是Poco对第三方自定义控件的表现有时候比Appium稳因为它是基于引擎层的信息能拿到更多运行时状态。6.3 Windows桌面端UIA树里Name和AutomationId双双为空怎么办桌面端自动化尤其是WinForms、WPF、Electron应用会碰到另一种没有id没有text的情况UIA树里节点的Name是空的、AutomationId也是空的。这时候我喜欢用uiautomation库配合ControlType和BoundingRectangle来做定位import uiautomation as auto window auto.WindowControl(searchDepth1, ClassNamexxxMainWindow) target_btn window.ButtonControl(searchDepth5, foundIndex1) target_btn.GetBoundingRect() target_btn.Click()如果没有Name完全可以用foundIndex或者通过ControlType 父级关系去匹配。Windows端还有一个特点很多自绘控件的name是空的但控件之间在UIA树里的层级顺序非常稳定不太受屏幕分辨率影响所以写XPath或关系定位比坐标靠谱得多。一两句横向对比框架核心定位手段没有id和text时最值得先试的方案Appiumid / text / XPath父节点相对定位 动态boundsAirtest/Poco层级关系链class索引、图像、OCRWindows uiautomationName / AutomationIdControlType foundIndex 层级说实在的这几种框架我都跑过同样的业务脚本到最后你会发现定位方式再多思路是共通的。永远先看控件树里还有什么可用信息再考虑相对关系、动态坐标最后才轮到图像、OCR这种重型武器。在我自己做自动化项目时最深的体会是不要被没有id、没有text这几个字吓住。大多数控件并不是真的找不到只是它的信息不在你最习惯的那两个字段里。每次排查都从完整属性、父级关系、动态坐标这三层依次推进90%的问题其实在第二层就解决了。剩下的那10%就交给图像和OCR去兜底。还有个小经验是这类定位问题每多花半小时就意味着脚本在实际运行中可能多踩一个深层雷。所以遇到定位困难我给自己的要求是先用5分钟把控件树完整过一遍如果发现需要走图像或者坐标就提前把动态性考虑进去别留到跑批的时候才翻车。
返回列表