利用lxml 库来解析 dump_hierarchy的XML
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.TextView
,android.widget.Button
)。text
: UI 元素中显示的文本。resource-id
: UI 元素的资源 ID (如果有的话)。content-desc
: UI 元素的描述(常用于辅助功能)。package
: 元素的应用程序包名。bounds
: 元素在屏幕上的边界。checkable
,checked
,clickable
,enabled
,focusable
,focused
,long-clickable
,scrollable
,selected
: 表示元素状态的布尔值属性。
如何使用 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 支持更复杂的查询,例如使用
parent
、ancestor
、child
等轴,以及使用函数进行条件过滤。如果你需要更精细的定位,可以深入学习 XPath 的语法。 - 可以使用 uiautomatorviewer 辅助: 如果你不太熟悉 XPath,可以先使用 Android SDK 自带的
uiautomatorviewer
工具来分析 UI 结构并生成 XPath 表达式。
总结
d.dump_hierarchy()
可以帮助你获取当前屏幕的 UI 结构,然后你可以使用 lxml
库和 XPath 表达式来精确定位 UI 元素,这为自动化测试提供了强大的能力。 请仔细阅读 XML 输出的内容,以便使用正确的属性和表达式来定位元素。 记住要定期获取最新的 hierarchy,以应对 UI 的动态变化。
详细解释如何结合 uiautomator2
和 XPath 进行 UI 元素定位并执行点击操作。
核心思路:
- 使用
dump_hierarchy()
获取 UI 结构: 首先,使用d.dump_hierarchy()
获取当前屏幕的 UI 元素层级结构,并将其解析为 XML 对象。 - 使用 XPath 定位目标元素: 其次,使用 XPath 表达式精确地定位到你想要点击的元素。
- 获取元素的边界(bounds): 从定位到的元素 XML 节点中,获取其在屏幕上的边界
bounds
属性。 - 计算点击坐标: 根据元素的边界计算点击坐标,通常取边界矩形的中心点。
- 使用
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)
代码详解:
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
。
- 接受
- 示例代码:
- 首先建立 u2 的连接
d = u2.connect()
- 展示了使用多种 XPath 定位点击的示例
- 点击文本为“确定”的按钮
- 点击指定 resource-id 的按钮
- 点击包含指定文本的 TextView
- 首先建立 u2 的连接
关键点:
- 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 的强大功能,可以实现各种复杂的自动化测试任务。
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('未找到匹配的节点。');
}
});
本文作者: 永生
本文链接: https://yys.zone/detail/?id=371
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!
发表评论
评论列表 (0 条评论)
暂无评论,快来抢沙发吧!