1.uiautomator2的dump_hierarchy()

uiautomator2 的 d.dump_hierarchy() 方法会返回一个 XML 格式的字符串,其中包含了当前屏幕上可见的 UI 元素层级结构。这个 XML 结构可以被解析,然后你可以使用 XPath 来定位特定的元素。

理解 dump_hierarchy() 返回的 XML

dump_hierarchy() 返回的 XML 结构类似于 HTML,每个 UI 元素都被表示为一个 XML 节点。每个节点包含一些属性,例如:

  • class: UI 元素的类名(例如:android.widget.TextViewandroid.widget.Button)。
  • text: UI 元素中显示的文本。
  • resource-id: UI 元素的资源 ID (如果有的话)。
  • content-desc: UI 元素的描述(常用于辅助功能)。
  • package: 元素的应用程序包名。
  • bounds: 元素在屏幕上的边界。
  • checkablecheckedclickableenabledfocusablefocusedlong-clickablescrollableselected: 表示元素状态的布尔值属性。

如何使用 XPath 匹配 dump_hierarchy() 中的元素

你可以使用 Python 的 lxml 库来解析 XML 并使用 XPath 查询。这里是一个示例:

import uiautomator2 as u2
from lxml import etree

# 连接设备
d = u2.connect()

# 获取 hierarchy
xml_string = d.dump_hierarchy()

# 解析 XML
root = etree.fromstring(xml_string.encode('utf-8'))

# 示例 XPath 查询

# 1. 根据 class 定位所有 TextView 元素
text_views = root.xpath("//node[@class='android.widget.TextView']")
for text_view in text_views:
    print(f"TextView Text: {text_view.get('text')}")

# 2. 根据 text 定位特定的 Button 元素
button = root.xpath("//node[@class='android.widget.Button' and @text='确定']")
if button:
    print(f"Button Text: {button[0].get('text')}")
else:
    print("Button not found")


# 3. 根据 resource-id 定位元素
edit_text = root.xpath("//node[@resource-id='com.example:id/edit_text']")
if edit_text:
    print(f"EditText Text: {edit_text[0].get('text')}")
else:
    print("EditText not found")

# 4. 定位带有特定 content-desc 的元素
image = root.xpath("//node[@class='android.widget.ImageView' and @content-desc='我的图片']")
if image:
    print("Image found!")
else:
    print("Image not found")

# 5. 查找可点击的 TextView
clickable_textviews = root.xpath("//node[@class='android.widget.TextView' and @clickable='true']")
for clickable_textview in clickable_textviews:
    print(f"Clickable TextView Text: {clickable_textview.get('text')}")

# 6.  查找祖先节点包含特定文本的按钮的子元素
button_child = root.xpath("//node[@class='android.widget.Button' and contains(@text, '登录')]/following::node[@class='android.widget.TextView']")
if button_child:
  for child in button_child:
     print(f"Child text: {child.get('text')}")


# 7. 查找指定包名下的元素
package_elements = root.xpath(f"//node[@package='com.example.myapp']")
for element in package_elements:
    print(f"Package Element: {element.get('class')}")

XPath 表达式解释

  • //node: 选择所有名为 node 的元素(uiautomator2 中所有 UI 元素都表示为 node)。
  • [@class='android.widget.TextView']: 选择 class 属性为 android.widget.TextView 的元素。
  • [@text='确定']: 选择 text 属性为 确定 的元素。
  • [@resource-id='com.example:id/edit_text']: 选择 resource-id 属性为 com.example:id/edit_text 的元素。
  • [@content-desc='我的图片']: 选择 content-desc 属性为 我的图片 的元素.
  • [@clickable='true']: 选择 clickable 属性为 true 的元素。
  • and: 表示逻辑与。
  • contains(@text, '登录'): 表示text属性包含 '登录'字符串
  • following::node: 表示当前节点的子元素中符合后面条件的元素
  • [@package='com.example.myapp']: 选择package属性为 com.example.myapp的元素。

注意事项

  • 安装 lxml: 你需要在你的 Python 环境中安装 lxml 库,可以使用 pip install lxml 命令进行安装。
  • 动态 UI: 如果你的 UI 经常变化,你可能需要定期重新获取 dump_hierarchy() 并重新解析。
  • 性能: 对于复杂的 UI 结构,XPath 查询可能会比较耗时,所以建议尽量使用更精确的选择器,避免使用过于宽泛的查询。
  • 元素属性: 仔细检查 dump_hierarchy() 输出的 XML,以确定你想要使用的属性来定位元素。
  • 避免频繁 d.dump_hierarchy() d.dump_hierarchy() 调用比较耗时,尽量在需要时调用,避免在循环中频繁使用。
  • 复杂的 XPath: XPath 支持更复杂的查询,例如使用 parentancestorchild 等轴,以及使用函数进行条件过滤。如果你需要更精细的定位,可以深入学习 XPath 的语法。
  • 可以使用 uiautomatorviewer 辅助: 如果你不太熟悉 XPath,可以先使用 Android SDK 自带的 uiautomatorviewer 工具来分析 UI 结构并生成 XPath 表达式。

总结

d.dump_hierarchy() 可以帮助你获取当前屏幕的 UI 结构,然后你可以使用 lxml 库和 XPath 表达式来精确定位 UI 元素,这为自动化测试提供了强大的能力。 请仔细阅读 XML 输出的内容,以便使用正确的属性和表达式来定位元素。 记住要定期获取最新的 hierarchy,以应对 UI 的动态变化。


详细解释如何结合 uiautomator2 和 XPath 进行 UI 元素定位并执行点击操作。

核心思路:

  1. 使用 dump_hierarchy() 获取 UI 结构: 首先,使用 d.dump_hierarchy() 获取当前屏幕的 UI 元素层级结构,并将其解析为 XML 对象。
  2. 使用 XPath 定位目标元素: 其次,使用 XPath 表达式精确地定位到你想要点击的元素。
  3. 获取元素的边界(bounds): 从定位到的元素 XML 节点中,获取其在屏幕上的边界 bounds 属性。
  4. 计算点击坐标: 根据元素的边界计算点击坐标,通常取边界矩形的中心点。
  5. 使用 d.click() 执行点击操作: 最后,使用 uiautomator2 的 d.click() 方法在计算出的坐标上执行点击操作。

代码示例:

import uiautomator2 as u2
from lxml import etree
import re

def click_element_by_xpath(d, xpath_expression):
    """
    使用 XPath 定位元素并执行点击操作.

    Args:
      d: uiautomator2 device 对象
      xpath_expression: 用于定位元素的 XPath 表达式
    """
    xml_string = d.dump_hierarchy()
    root = etree.fromstring(xml_string.encode('utf-8'))

    elements = root.xpath(xpath_expression)

    if not elements:
        print(f"元素未找到, XPath: {xpath_expression}")
        return False

    element = elements[0]
    bounds_str = element.get('bounds') # 获取边界属性,如 [0,0][100,200]
    print(f"Element bounds: {bounds_str}")

    # 使用正则表达式从 bounds 字符串中提取坐标
    match = re.match(r'\[(\d+),(\d+)\]\[(\d+),(\d+)\]', bounds_str)
    if not match:
        print(f"无法解析 bounds 属性: {bounds_str}")
        return False

    x1, y1, x2, y2 = map(int, match.groups())
    center_x = (x1 + x2) // 2  #计算中心点 x 坐标
    center_y = (y1 + y2) // 2 #计算中心点 y 坐标

    print(f"Clicking at coordinates: ({center_x}, {center_y})")
    d.click(center_x, center_y)
    return True



if __name__ == '__main__':
  d = u2.connect()

  # 示例:点击文本为“确定”的按钮
  xpath_button = "//node[@class='android.widget.Button' and @text='确定']"
  click_element_by_xpath(d, xpath_button)


  # 示例: 点击resource-id为指定 id 的按钮
  xpath_button_id = "//node[@class='android.widget.Button' and @resource-id='com.example:id/ok_button']"
  click_element_by_xpath(d, xpath_button_id)


  #示例: 点击包含特定文本的 TextView
  xpath_textview_contain_text = "//node[@class='android.widget.TextView' and contains(@text, '下一步')]"
  click_element_by_xpath(d, xpath_textview_contain_text)

代码详解:

  1. click_element_by_xpath(d, xpath_expression) 函数:
    • 接受 uiautomator2 的 device 对象 d 和 XPath 表达式 xpath_expression 作为参数。
    • 使用 d.dump_hierarchy() 获取并解析 XML。
    • 使用 root.xpath(xpath_expression) 定位元素。
    • 检查是否找到元素,如果没有找到,则打印错误消息并返回 False
    • 获取元素的 bounds 属性,使用正则表达式解析出边界坐标。
    • 计算出边界的中心点坐标。
    • 使用 d.click(center_x, center_y) 执行点击操作。
    • 如果点击成功则返回 True
  2. 示例代码:
    • 首先建立 u2 的连接 d = u2.connect()
    • 展示了使用多种 XPath 定位点击的示例
      • 点击文本为“确定”的按钮
      • 点击指定 resource-id 的按钮
      • 点击包含指定文本的 TextView

关键点:

  • XPath 的准确性: 使用准确的 XPath 表达式是定位目标元素的关键。你可以使用 uiautomatorviewer 工具来辅助生成 XPath。
  • 边界解析: 注意从元素的 bounds 属性中提取坐标,bounds 通常是 [x1,y1][x2,y2] 的字符串,需要使用正则表达式解析,其中(x1, y1) 是左上角坐标, (x2, y2) 是右下角坐标。
  • 点击位置: 这里使用的是元素中心点的坐标,可以根据需要调整点击的位置,例如使用元素左上角或右下角的坐标。
  • 错误处理: 代码中加入了简单的错误处理,可以根据具体需求完善错误处理机制。
  • 等待: 有时候,在点击元素后可能需要等待一段时间,可以使用 d.sleep() 或者 d.wait() 方法进行等待,确保 UI 变化完成。
  • 元素可见性: 确保元素在屏幕上可见,才可以执行点击操作。

总结:

这个示例代码提供了一个通用的函数 click_element_by_xpath,可以方便地使用 XPath 定位元素并执行点击操作。只需要提供正确的 XPath 表达式,就可以完成点击任务。你可以根据自己的需要调整 XPath 表达式,修改点击位置和错误处理等逻辑。 请注意,在实际使用中需要结合实际的UI结构调整相应的XPath表达式。

 

除了点击操作,uiautomator2 结合 XPath 还可以进行许多其他 UI 自动化操作。以下是一些常见操作的介绍,并提供相应的示例代码:

1. 输入文本 (send_keys)

  • 思路: 定位输入框元素,然后使用 send_keys() 方法输入文本。
def send_text_by_xpath(d, xpath_expression, text):
    """使用 XPath 定位输入框并输入文本"""
    xml_string = d.dump_hierarchy()
    root = etree.fromstring(xml_string.encode('utf-8'))

    elements = root.xpath(xpath_expression)
    if not elements:
        print(f"输入框未找到, XPath: {xpath_expression}")
        return False

    element = elements[0]
    bounds_str = element.get('bounds')
    match = re.match(r'\[(\d+),(\d+)\]\[(\d+),(\d+)\]', bounds_str)
    if not match:
        print(f"无法解析 bounds 属性: {bounds_str}")
        return False

    x1, y1, x2, y2 = map(int, match.groups())
    center_x = (x1 + x2) // 2
    center_y = (y1 + y2) // 2
    d.click(center_x, center_y)  # 先点击获取焦点
    d.send_keys(text)
    return True

if __name__ == '__main__':
    d = u2.connect()

    # 示例:向resource-id为指定 id 的输入框输入文本
    xpath_edit_text = "//node[@class='android.widget.EditText' and @resource-id='com.example:id/edit_text']"
    send_text_by_xpath(d, xpath_edit_text, "Hello World!")

2. 清除文本 (clear_text)

  • 思路: 定位输入框元素,然后使用 clear() 方法清除文本。

def clear_text_by_xpath(d, xpath_expression):
"""使用 XPath 定位输入框并清除文本"""
xml_string = d.dump_hierarchy()
root = etree.fromstring(xml_string.encode('utf-8'))

elements = root.xpath(xpath_expression)
if not elements:
    print(f"输入框未找到, XPath: {xpath_expression}")
    return False

element = elements[0]
bounds_str = element.get('bounds')
match = re.match(r'\[(\d+),(\d+)\]\[(\d+),(\d+)\]', bounds_str)
if not match:
    print(f"无法解析 bounds 属性: {bounds_str}")
    return False

x1, y1, x2, y2 = map(int, match.groups())
center_x = (x1 + x2) // 2
center_y = (y1 + y2) // 2
d.click(center_x, center_y)  # 先点击获取焦点
d.clear_text()
return True

if name == 'main':
d = u2.connect()

xpath_edit_text = "//node[@class='android.widget.EditText' and @resource-id='com.example:id/edit_text']"
clear_text_by_xpath(d, xpath_edit_text)

**3. 滑动 (swipe)**

*   **思路:** 获取起始和结束坐标,然后使用 `swipe()` 方法进行滑动。

```python
def swipe_screen(d, start_x, start_y, end_x, end_y, duration=0.5):
    """执行屏幕滑动操作"""
    d.swipe(start_x, start_y, end_x, end_y, duration)

if __name__ == '__main__':
    d = u2.connect()
    screen_width, screen_height = d.window_size()

    # 从屏幕右侧向左滑动
    swipe_screen(d, screen_width * 0.8, screen_height / 2, screen_width * 0.2, screen_height / 2)

4. 长按 (long_click)

  • 思路: 定位目标元素,然后使用 long_click() 方法进行长按。
def long_click_element_by_xpath(d, xpath_expression, duration=2):
    """使用 XPath 定位元素并执行长按操作"""
    xml_string = d.dump_hierarchy()
    root = etree.fromstring(xml_string.encode('utf-8'))

    elements = root.xpath(xpath_expression)
    if not elements:
      print(f"元素未找到, XPath: {xpath_expression}")
      return False
    element = elements[0]
    bounds_str = element.get('bounds')
    match = re.match(r'\[(\d+),(\d+)\]\[(\d+),(\d+)\]', bounds_str)
    if not match:
      print(f"无法解析 bounds 属性: {bounds_str}")
      return False

    x1, y1, x2, y2 = map(int, match.groups())
    center_x = (x1 + x2) // 2
    center_y = (y1 + y2) // 2
    d.long_click(center_x, center_y,duration)
    return True

if __name__ == '__main__':
    d = u2.connect()
    xpath_element = "//node[@class='android.widget.TextView' and @text='长按我']"
    long_click_element_by_xpath(d, xpath_element)

5. 获取元素属性 (get_text, get('attribute'))

  • 思路: 定位元素,然后使用 get('text') 获取文本,或使用 get('attribute') 获取其他属性的值。
def get_element_text_by_xpath(d, xpath_expression):
    """使用 XPath 定位元素并获取文本"""
    xml_string = d.dump_hierarchy()
    root = etree.fromstring(xml_string.encode('utf-8'))

    elements = root.xpath(xpath_expression)
    if not elements:
      print(f"元素未找到, XPath: {xpath_expression}")
      return None

    element = elements[0]
    text = element.get('text')
    return text


def get_element_attribute_by_xpath(d, xpath_expression, attribute):
  """使用 XPath 定位元素并获取属性值"""
  xml_string = d.dump_hierarchy()
  root = etree.fromstring(xml_string.encode('utf-8'))

  elements = root.xpath(xpath_expression)
  if not elements:
    print(f"元素未找到, XPath: {xpath_expression}")
    return None
  element = elements[0]
  attr_value = element.get(attribute)
  return attr_value


if __name__ == '__main__':
    d = u2.connect()
    xpath_text_view = "//node[@class='android.widget.TextView' and @text='示例文本']"
    text = get_element_text_by_xpath(d, xpath_text_view)
    print(f"TextView text: {text}")

    xpath_button = "//node[@class='android.widget.Button' and @text='按钮']"
    resource_id = get_element_attribute_by_xpath(d, xpath_button,'resource-id')
    print(f"Button resource-id: {resource_id}")

6. 判断元素是否存在 (xpath 查询结果判断)

  • 思路: 直接使用 XPath 查询元素,如果结果为空列表,则表示元素不存在。
def is_element_exist_by_xpath(d, xpath_expression):
    """使用 XPath 判断元素是否存在"""
    xml_string = d.dump_hierarchy()
    root = etree.fromstring(xml_string.encode('utf-8'))
    elements = root.xpath(xpath_expression)
    return bool(elements)


if __name__ == '__main__':
    d = u2.connect()
    xpath_exist = "//node[@class='android.widget.TextView' and @text='存在的文本']"
    xpath_not_exist = "//node[@class='android.widget.TextView' and @text='不存在的文本']"

    if is_element_exist_by_xpath(d, xpath_exist):
        print("元素存在")
    else:
        print("元素不存在")

    if is_element_exist_by_xpath(d, xpath_not_exist):
      print("元素存在")
    else:
      print("元素不存在")

7. 等待元素出现/消失 (wait)

  • 思路: 使用 d.wait.xpath() 方法等待元素出现或消失。
def wait_element_appear(d, xpath_expression, timeout=10):
    """等待元素出现"""
    return d.wait.xpath(xpath_expression, timeout=timeout)

def wait_element_disappear(d, xpath_expression, timeout=10):
    """等待元素消失"""
    return d.wait.xpath(xpath_expression, exists=False, timeout=timeout)


if __name__ == '__main__':
    d = u2.connect()
    xpath_element_to_appear = "//node[@class='android.widget.TextView' and @text='等待出现']"
    xpath_element_to_disappear = "//node[@class='android.widget.TextView' and @text='等待消失']"

    #假设初始状态 '等待出现' 的元素不存在, '等待消失' 的元素存在
    
    if wait_element_appear(d, xpath_element_to_appear,timeout=15): #这里可能需要手动点击让等待出现的元素出现
        print("元素出现")
    else:
        print("等待超时,元素没有出现")

    if wait_element_disappear(d, xpath_element_to_disappear,timeout=15):#这里可能需要手动点击让等待消失的元素消失
        print("元素消失")
    else:
        print("等待超时,元素没有消失")

注意事项:

  • 选择合适的定位方式: 在实际使用中,需要根据具体的 UI 结构和需要选择合适的定位方式,例如使用 resource-id 或 content-desc 可能会更加稳定可靠。
  • 错误处理: 请务必添加错误处理机制,例如元素未找到的处理,确保脚本的健壮性。
  • 结合实际情况: 在实际使用中,需要结合具体的 UI 界面进行分析,才能编写正确的 XPath 表达式。
  • 时间等待: 根据UI 变化情况, 添加合理的 sleep 和 wait 等待时间,确保脚本执行的稳定性。

这些示例代码涵盖了 UI 自动化中常见的操作,你可以根据自己的需求进行修改和扩展。 结合 uiautomator2 的其他 API 和 XPath 的强大功能,可以实现各种复杂的自动化测试任务。

 

2.Appium inspect

Appium inspect 连接后Copy XML Source to Clipboard,或者点击Download Source as .XML File下载下来读取,作为变量xml_string 

  • python实现
from lxml import etree

# 读取下载下来xml文件
f = open(r"C:\Users\yys53\Downloads\app-source-2025-03-30T05_41_16.992Z.xml", 'r', encoding='utf-8')
xml_string = f.read()
# 调用lxml库的etree.fromstring方法解析xml
xml_string = xml_string.replace("<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>", '')
root = etree.fromstring(xml_string.encode('utf-8'))

text_views = root.xpath('//*[@text="短剧历史"]/../../../preceding-sibling::*/android.widget.TextView')
print(text_views)
if text_views:
    print(text_views[0].get('text'))
  • nodejs实现
npm install xmldom xpath
const fs = require('fs');
const { DOMParser } = require('xmldom');
const xpath = require('xpath');

// 指定 XML 文件的路径
const xmlFilePath = 'C:\\Users\\yys53\\Downloads\\app-source-2025-03-30T05_41_16.992Z.xml';

// 读取 XML 文件内容
fs.readFile(xmlFilePath, 'utf-8', (err, xmlData) => {
  if (err) {
    console.error('读取 XML 文件时出错:', err);
    return;
  }

  // 解析 XML 数据
  const doc = new DOMParser().parseFromString(xmlData, 'text/xml');

  // 执行 XPath 查询
  const expression = '//*[@text="短剧历史"]/../../../preceding-sibling::*/android.widget.TextView';
  const nodes = xpath.select(expression, doc);

  // 输出查询结果
  if (nodes.length > 0) {
    nodes.forEach((node, index) => {
      console.log(`节点 ${index + 1}:`);
      console.log(`完整节点: ${node.toString()}`);
      console.log(`text 属性值: ${node.getAttribute('text')}`);
    });
  } else {
    console.log('未找到匹配的节点。');
  }
});