ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

uiautomator2 XPath 扩展深入实战:简写定位语法、选择器 API 与源码级实现细节

2026/9/25 2:58:27 拓冰建站 浏览量
uiautomator2 XPath 扩展深入实战:简写定位语法、选择器 API 与源码级实现细节 测试移动开发GUI 自动化【免费下载链接】uiautomator2Android Uiautomator2 Python Wrapper项目地址https://gitcode.com/gh_mirrors/ui/uiautomator2点击查看免费下载本文基于 uiautomator2 仓库根目录的中文文档 XPATH_CN.md 展开系统讲解其 XPath 扩展的工作原理、自定义简写定位语法、XPathSelector/XMLElement/PageSource三层 API 的完整用法与scroll_to滑动查找机制并结合 uiautomator2/xpath.py 源码与 tests/test_xpath.py、mobile_tests/test_xpath.py 测试用例帮助你在 Android UI 自动化中写出更简洁、更可靠的元素定位脚本并理解每个 API 背后的真实实现。工作原理dump_hierarchy lxmlXPath 扩展的工作链路只有两步通过 uiautomator2 库的dump_hierarchy接口获取当前界面 XML包含界面上所有元素的 resource-id、text、content-desc、bounds 等属性的hierarchy文档使用lxml库解析该 XML执行 XPath 查询找到匹配元素再根据元素bounds计算出坐标最终通过click指令完成操作。从源码看这条链路落在 uiautomator2/xpath.py 的PageSource类上get_page_source()对d.dump_hierarchy()的返回值做PageSource.parse包装首次访问root属性时才真正用etree.fromstring解析并把每个node标签改写为class属性的值即控件类名如android.widget.TextView这样后续才能写出//android.widget.TextView这样的查询# uiautomator2/xpath.py (PageSource.root) functools.cached_property def root(self): _root etree.fromstring(str2bytes(self._xml_content)) for node in _root.xpath(//node): node.tag safe_xmlstr(node.attrib.pop(class, )) or node return _root需要注意的限制lxml只支持 XPath 1.0 语法文档原作者也明确说明尚未研究 XPath 2.0 的接入方式。另外查询时通过namespaces{re: http://exslt.org/regular-expressions}注册了 EXSLT 正则命名空间见 xpath.py这正是下文^正则简写语法能生效的原因。弹窗监控原理hierarchy 中包含了界面上所有元素信息包括弹窗按钮。假设界面上存在跳过、知道了两个弹窗按钮而真正要点的按钮是播放监控流程为获取当前界面 XML通过dump_hierarchy检查跳过、知道了是否存在存在就点击然后回到第 1 步重新抓取界面检查播放是否存在存在就点击并结束未找到则回到第 1 步循环执行直到查找次数超标。需要注意当前源码中d.xpath.when(...)、d.xpath.run_watchers()、d.xpath.watch_background()、d.xpath.watch_stop()、d.xpath.sleep_watch(seconds)等方法均已标记为deprecated官方建议改用独立的d.watcherAPI见 xpath.py 中各方法上的deprecated装饰器原因均为 use d.watcher.xxx instead。真机测试 mobile_tests/test_xpath.py 演示了新版写法dev.watcher.when(App).click() dev.watcher.start(interval1.0) # 触发回调后 watcher 每 interval 秒检查一次安装pip3 install -U uiautomator2XPath 扩展是 uiautomator2 的核心内置模块随主包安装即可使用无需额外插件。简单用法最小示例下面这个完整可运行的例子演示了启动应用、按 text 点击、resource-id 定位、多条件组合、父元素与子元素定位import uiautomator2 as u2 def main(): d u2.connect() d.app_start(com.netease.cloudmusic, stopTrue) d.xpath(//*[text私人FM]).click() # # 高级用法(元素定位) # # 开头等价于 d.xpath(//*[resource-idpersonal-fm]) d.xpath(personal-fm) # 多个条件定位类似于 AND当前推荐用 运算符见下文 d.xpath(//android.widget.Button).xpath(//*[text私人FM]) d.xpath(//*[text私人FM]).parent() # 定位到父元素 d.xpath(//*[text私人FM]).parent(android:list) # 定位到符合条件的父元素 # 包含 child 的时候不建议再使用多条件 xpath容易搞混 d.xpath(android:id/list).child(/android.widget.TextView).click() # 等价于 # d.xpath(//*[resource-idandroid:id/list]/android.widget.TextView).click()后续示例为方便均省略import与main默认存在设备对象d。简写 XPath 语法规则为了脚本写得更快uiautomator2 自定义了一套简写规则。这些规则并非魔术而是d.xpath(...)内部统一调用 strict_xpath() 把简写转换回标准 XPath 字符串转换结果还会经etree.XPath语法校验非法表达式抛出XPathError。简写含义等价标准 XPath//...开头原生 XPath原样透传不转换smartisanos:id/right_container按 resource-id 定位//*[resource-idsmartisanos:id/right_container]^.*道了正则匹配灵感Python re//*[re:match(text(), ^.*道了)]知道%前缀匹配灵感SQL LIKE//*[starts-with(text(), 知道)]%知道后缀匹配//*[ends-with(text(), 知道)]%知道%包含匹配//*[contains(text(), 知道)]搜索无特殊前缀同时匹配 text 与 description 字段//*[text搜索 or content-desc搜索 or resource-id搜索]结合 strict_xpath() 源码 有两个值得注意的实现细节^正则实际同时作用于三个字段。源码生成的是re:match(text, ...) or re:match(content-desc, ...) or re:match(resource-id, ...)比文档中给出的只匹配 text范围更广同理%x%、x%、%x也同时匹配text与content-desc两个字段。后缀匹配%x不是用ends-with()。因为 XPath 1.0 没有ends-with函数源码用substring(text, string-length(text) - n 1)截取末尾 n 个字符再做等值比较实现。单元测试 tests/test_xpath.py 验证了转换结果def test_strict_xpath(): for (input, expect) in [ (n1, //*[resource-idn1]), (//TextView, //TextView), (//TextView[textn1], //TextView[textn1]), ((//TextView)[2], (//TextView)[2]), (//TextView/, //TextView), # 末尾 / 会被 rstrip 掉 ]: assert strict_xpath(input) expectXPath 类型与路径拼接简写字符串首先会被包装成 XPath 类继承自str构造时即完成strict_xpath转换。它的joinpath()方法用于把子路径拼接在父路径之后xp XPath(//TextView) xp.joinpath(/n1) # - //TextView/n1XPathSelector.child()正是基于joinpath实现的见 xpath.py且对已设置了/|运算符的选择器会抛出XPathError——这就是文档提醒使用 child 时不要再套多条件 xpath的原因。XPathSelector等待、点击与遍历d.xpath(...)返回XPathSelector对象XPathEntry.call它是整个扩展的使用入口sl d.xpath(com.example:id/home_searchedit) # sl 为 XPathSelector 对象 # 点击 sl.click() sl.click(timeout10) # 指定超时超时未找到抛出 XPathElementNotFoundError sl.click_exists() # 存在即点击返回是否点击成功 (bool) sl.click_exists(timeout10) # 等待最多 10 秒 sl.match() # 不匹配返回 None否则返回 XMLElement # 等待元素出现返回 XMLElement未找到返回 None el sl.wait() el sl.wait(timeout15) # 等待 15 秒 # 等待元素消失 sl.wait_gone() sl.wait_gone(timeout15) # 与 wait 类似区别是未找到直接抛出 XPathElementNotFoundError 异常 el sl.get() el sl.get(timeout15) # 修改全局默认等待时间 d.xpath.global_set(timeout, 15) d.xpath.implicitly_wait(15) # 与上一行等价源码中已标记 deprecated print(sl.exists) # 返回是否存在 (bool) sl.get_last_match() # 获取上次匹配的 XMLElement sl.get_text() # 获取组件文本内部先 get() 再取 .text sl.set_text() # 清空输入框 sl.set_text(hello world) # 输入 hello world几个行为细节由源码直接给出轮询间隔 0.2 秒。wait() 在一个while循环中每 0.2s 重新dump_hierarchy查询一次直到命中或超过 deadlinewait_gone()逻辑相反命中不存在即返回True。默认超时不是 10 秒。文档注释写默认等待时间是 10s但当前仓库中该默认值来自settingswait_timeout: 20.0且 XPathSelector._global_timeout 的回退值也是 20.0 秒。因此按当前版本实测不传timeout时默认最多等待 20 秒global_set(timeout, 15)本质上就是改写d.wait_timeout。set_text的实现是先点击聚焦再 send_keys见 xpath.py所以输入框必须先可点击。get()找不到时抛异常异常类型是 XPathElementNotFoundError继承自NormalError→RPCError。单测 tests/test_xpath.py 用 mock 的 hierarchy 验证了这些行为例如click会精确点击元素中心点bounds [0,0][1080,100]的元素点击坐标为(540, 50)click_exists在元素不存在时不产生任何点击调用get超时抛XPathElementNotFoundErrordef test_xpath_click(): x(n1).click() assert mock.click.call_args[0] (540, 50) assert x(n1).click_exists() True assert x(n3).click_exists(timeout.1) False # 不存在则不点击 def test_xpath_wait_and_wait_gone(): assert x(n1).wait() is True assert x(n3).wait(timeout.1) is False assert x(n3).wait_gone(timeout.1) is True遍历所有匹配元素for el in d.xpath(//android.widget.EditText).all(): print(rect:, el.rect) # tuple: (left_x, top_y, width, height) print(center:, el.center()) el.click() print(el.elem) # lxml 解析出来的 Node print(el.text)all()xpath.py每次调用都会重新抓取一次最新界面除非显式传入PageSource返回XMLElement列表并把每个元素的_parent回填为XPathEntry保证后续el.click()等动作能拿到设备对象。高级查找语法与|Added in version 3.1选择器支持用 Python 运算符组合条件内部实现为对两组匹配结果做集合交并运算Operator.AND→set Operator.OR→set |# 查找 textNFC AND idandroid:id/item (d.xpath(NFC) d.xpath(android:id/item)).get() # 查找 textNFC OR idandroid:id/item (d.xpath(NFC) | d.xpath(App) | d.xpath(Content)).get() # 复杂组合也支持 ((d.xpath(NFC) | d.xpath(android:id/item)) d.xpath(//android.widget.TextView)).get()链式方法sl.xpath(...)同样等价于源码标注 Deprecated, using a b instead见 xpath.py传 list 时会用functools.reduce依次合并。XMLElement几何属性与手势操作XPathSelector.get()返回的对象称为XMLElement封装了单个 UI 节点el d.xpath(com.example:id/home_searchedit).get() lx, ly, width, height el.rect # 左上角坐标 宽高 lx, ly, rx, ry el.bounds # 左上角与右下角坐标 x, y el.center() # 元素中心点 x, y el.offset(0.5, 0.5) # 等价于 center()可取任意相对位置 el.click() # 发送点击实际是 d.click(x, y) print(el.text) # 文本内容 print(el.attrib) # 节点全部属性, dict # 控件截图原理整张截图后按 bounds crop el.screenshot() # 控件内滑动 el.swipe(right) # left / right / up / down el.swipe(right, scale0.9) # scale 表示滑动距离占控件宽度(水平)/高度(垂直)的比例几何属性的实现细节bounds是从节点bounds[0,0][1080,100]属性用正则提取四个整数得到cached_property同一元素只解析一次rect由bounds换算为(x, y, w, h)xpath.py。offset(px, py)按宽高百分比从左上角偏移取点xpath.pycenter()即offset(0.5, 0.5)。screenshot()调用d.screenshot()整屏截图后im.crop(self.bounds)xpath.py。el.info会把 XML 属性整理成更接近 dump 输出的 dict布尔属性转成bool、属性名转驼峰convert_to_camel_case、并补充childCount、className、bounds、packageName、contentDescription、resourceName等字段info 实现输出示例{index: 0, text: , resourceId: com.example:id/home_searchedit, checkable: True, checked: True, clickable: True, enabled: True, focusable: False, focused: False, scrollable: False, longClickable: False, password: False, selected: False, visibleToUser: True, childCount: 0, className: android.widget.Switch, bounds: {left: 882, top: 279, right: 1026, bottom: 423}, packageName: com.android.settings, contentDescription: , resourceName: android:id/switch_widget}单测 tests/test_xpath.py 验证了坐标换算、截图裁剪尺寸、click/long_click落到中心点、get_xpath(strip_indexTrue)生成/hierarchy/FrameLayout/TextView等路径。swipe 的 scale 语义注意与旧文档注释的差异el.swipe(direction, scale)委托给 swipe_in_bounds()以控件中心为轴心从距离对边width * scale处滑动到另一侧即滑动距离 scale × 控件宽度水平方向或高度垂直方向滑动中心点与控件中心点一致。这里要特别指出一个源码与旧文档注释的差异当前源码中swipe的scale参数默认值是 0.6XMLElement.swipe 与 utils.swipe_in_bounds 一致且断言0 scale 1.0文档早期注释里写的scale 默认 0.9是过时说明以源码为准。parent向上定位d.xpath(//*[text私人FM]).parent() # 直接父节点 d.xpath(//*[text私人FM]).parent(android:list) # 向上找第一个符合条件的祖先实现见 XMLElement.parent不带参数时直接取 lxml 父节点带 XPath 条件时把从当前节点到根的整条祖先链收集起来再在全树中执行条件查询取祖先链 ∩ 匹配集中离自己最近的那个节点。真机测试 mobile_tests/test_xpath.py 验证了d.xpath(App).parent(android:id/list).info[resourceId] android:id/list。滑动到指定位置scroll_toscroll_to属于较新的功能文档原文提醒可能不这么完善比如不能检测是否滑动到底部了——源码中同样留有FIXME: 还差一个检测是否到底的功能的注释xpath.py。先看方向枚举 Directionclass Direction(str, enum.Enum): LEFT, RIGHT, UP, DOWN left, right, up, down FORWARD up # 垂直向前 内容向上滚手指向上滑 BACKWARD down HORIZ_FORWARD left # 水平向前 HORIZ_BACKWARD right设备级与元素级两种用法from uiautomator2 import connect_usb, Direction d connect_usb() d.scroll_to(下单) # 默认向下FORWARD查找 d.scroll_to(下单, Direction.FORWARD) d.scroll_to(下单, Direction.HORIZ_FORWARD, max_swipes5) # 在指定元素内部滑动 d.xpath(com.taobao.taobao:id/dx_root).scroll(Direction.HORIZ_FORWARD) d.xpath(com.taobao.taobao:id/dx_root).scroll_to(下单, Direction.HORIZ_FORWARD)两层实现对照元素级XMLElement.scroll_to循环最多max_swipes默认 10次先查目标是否存在不存在则调用self.scroll(direction)滑动一旦scroll()返回False没有新元素出现可推断已经到底则提前break找到目标返回XMLElement否则返回None。设备级XPathEntry.scroll_to把FORWARD/BACKWARD/HORIZ_*映射为UP/DOWN/LEFT/RIGHT手势方向在max_swipes次循环内每次先target.exists检查找不到就swipe_ext(direction, 0.5)滑半屏找到后还会再滑0.1比例的小位移防止目标元素停留在屏幕边缘然后返回get_last_match()未找到时源码实际返回False返回类型注解为XMLElement | None以运行时行为为准。scroll()与swipe()的关键区别在于返回值语义scroll(direction)返回bool表示滑动后是否还有新子元素出现XMLElement.scroll 的做法是滑动前后各查一次//*用集合差集再配合get_xpath()前缀限定到当前元素子树内。真机测试 mobile_tests/test_xpath.py 验证了元素级scroll_todef test_xpath_scroll_to(dev: u2.Device): d dev d.xpath(Graphics).click() d.xpath(android:id/list).scroll_to(Pictures) assert d.xpath(Pictures).exists综合示例import uiautomator2 as u2 from uiautomator2 import Direction def main(): d u2.connect() d.app_start(com.netease.cloudmusic, stopTrue) # steps d.xpath(//*[text私人FM]/../android.widget.ImageView).click() d.xpath(下一首).click() # 监控弹窗注意以下 watch 系列方法在当前源码中已标记 deprecated # 新代码建议使用 d.watcher 系列 API参见上文弹窗监控原理 d.xpath.sleep_watch(2) d.xpath(转到上一层级).click() d.xpath(转到上一层级).click(watchFalse) # 点击且不触发 watch d.xpath(转到上一层级).click(timeout5.0) # 最多等待 5s d.xpath.watch_background() # 开启后台监控默认每 4s 检查一次 d.xpath.watch_background(interval2.0) # 每 2s 检查一次 d.xpath.watch_stop() # 停止监控 for el in d.xpath(//android.widget.EditText).all(): print(rect:, el.rect) # tuple: (left_x, top_y, width, height) print(bounds:, el.bounds) # tuple: (left, top, right, bottom) print(center:, el.center()) el.click() print(el.elem) # lxml Node # 控件滑动 el d.xpath(com.taobao.taobao:id/fl_banner_container).get() el.swipe(Direction.HORIZ_FORWARD) # 从右滑到左 el.swipe(Direction.LEFT) # 同上 el.swipe(Direction.FORWARD) # 从下滑到上 el.swipe(Direction.UP) el.swipe(right, scale0.9) # 滑动距离为控件宽度的 90% el.swipe(up, scale0.5) # 滑动距离为控件高度的 50% # scroll 返回 bool是否还有新元素出现 el.scroll(Direction.FORWARD) el.scroll(Direction.BACKWARD) el.scroll(Direction.HORIZ_FORWARD) el.scroll(Direction.HORIZ_BACKWARD) if el.scroll(forward): print(还可以继续滚动)PageSource 对象Added in version 3.1这是 XPath 扩展最底层的对象——高级用法但最初级几乎所有函数最终都依赖它。PageSource由d.dump_hierarchy()的返回字符串初始化核心方法是find_elements。source d.xpath.get_page_source() # find_elements 是核心方法 elements source.find_elements(//android.widget.TextView) # List[XMLElement] for el in elements: print(el.text) # 获取坐标后点击 x, y elements[0].center() d.click(x, y) # 多种条件的查询写法 es1 source.find_elements(//android.widget.TextView) es2 source.find_elements(XPath(android:id/content).joinpath(//*)) # 寻找是 TextView 但不属于 idandroid:id/content 下的节点 els set(es1) - set(es2) # 寻找是 TextView 且属于 idandroid:id/content 下的节点 els set(es1) set(es2)从源码看PageSourcefind_elements直接在 lxml 树上执行带re命名空间的 XPath 查询并把每个命中节点包成XMLElementXMLElement实现了__hash__/__eq__xpath.py所以set集合运算可以直接比较两个查询结果。d.xpath(..., source)还支持传入固定的 XML 字符串/PageSource做离线查询XPathEntry.call适合调试时复用一份 dump 结果。特殊说明与高级 XPath 用法类名中的非法字符有时className中包含$#等 XML 非法字符uiautomator2 会统一替换成.见 safe_xmlstr()用re.sub([$#], ., s)并把连续点号折叠。因此查询自定义 View 时若类名含这些字符要记得写替换后的类名。lxml 仅支持 XPath 1.0写表达式时不要用ends-with()、subsequence()等 XPath 2.0 函数。常用高级写法清单# 所有元素 //* # resource-id 包含 login 字符 //*[contains(resource-id, login)] # 按钮包含账号或帐号 //android.widget.Button[contains(text, 账号) or contains(text, 帐号)] # 所有 ImageView 中的第二个 (//android.widget.ImageView)[2] # 所有 ImageView 中的最后一个 (//android.widget.ImageView)[last()] # className 包含 ImageView //*[contains(name(), ImageView)]简写语法也支持这些组合例如 mobile_tests/test_xpath.py 中%前缀/后缀简写的真机验证assert dev.xpath(Accessibility).wait() assert dev.xpath(%ccessibility).wait() # 后缀匹配 assert dev.xpath(Accessibilit%).wait() # 前缀匹配此外社区有大量成熟的 XPath 学习资料与在线测试工具W3School XPath 教程、XPath Quicksheet、各类 xpath-tester 网站可供检索使用用来验证自己写的表达式仓库中配套的示例脚本可参考 examples/com.netease.cloudmusic/main.py与上文云音乐示例同一场景。小结文档与当前版本的差异速查事项文档描述当前仓库源码事实默认等待超时默认等待时间是 10ssettings默认wait_timeout 20.0settings.py_global_timeout回退值亦为 20.0swipe默认 scale注释写 0.9源码默认scale0.6xpath.py^正则简写匹配字段仅 text实际匹配text、content-desc、resource-id三字段xpath.pywhen/watch_background/sleep_watch文档推荐用法均标记deprecated改用d.watcherAPIxpath.pysl.xpath(...)链式 AND文档示例功能保留但标注 Deprecated推荐a bxpath.py掌握以上内容后你可以用一行简写完成绝大多数日常定位id、%text%、^regex用wait/get/click_exists组合出健壮的等待-操作逻辑用/|组合精确条件用scroll_to在长列表中找到目标元素并在遇到行为疑点时直接对照 uiautomator2/xpath.py 的源码与 tests/test_xpath.py 的单测确认预期行为。赞分享测试移动开发GUI 自动化【免费下载链接】uiautomator2Android Uiautomator2 Python Wrapper项目地址https://gitcode.com/gh_mirrors/ui/uiautomator2点击查看免费下载相关推荐uiautomator2 XPath 扩展实战元素定位、等待点击、滚动与弹窗监控uiautomator2 XPath 扩展实战元素定位、等待点击、滚动与弹窗监控 uiautomator2 的 XPath 扩展允许你用一行 d.xpath测试移动开发GUI 自动化深入理解openatx/uiautomator2中的XPath扩展功能深入理解openatx/uiautomator2中的XPath扩展功能 前言 在移动应用自动化测试领域元素定位是最基础也是最重要的环节之一。openatx/u测试移动开发GUI 自动化E5-small-v2-openmind实战案例如何将文本嵌入集成到你的Python项目中E5 small v2 openmind实战案例如何将文本嵌入集成到你的Python项目中 E5 small v2 openmind是一款高效的文本嵌入模型上一篇为什么越来越多的企业选择ERPNext开源ERP系统从零到一完整入门指南下一篇Video2X用AI魔法让老旧视频重获新生创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考