用这个插件前,要先了解一些XPath知识。 好在网上这方便的资料很多。下面列举一些
代码并没有完全测试完,可能还有bug,欢迎跟我反馈。
- 通过uiautomator2库的
dump_hierarchy
接口,获取到当前的UI界面(一个很丰富的XML)。 - 然后使用
lxml
库解析,寻找匹配的xpath,然后使用click指令完后操作
目前发现lxml只支持XPath1.0, 有了解的可以告诉我下怎么支持XPath2.0
弹窗监控原理
通过hierarchy可以知道界面上的所有元素信息(包括弹窗和要点击的按钮)。
假设有 跳过
, 知道了
这两个弹窗按钮。需要点击的按钮名是 播放
- 获取到当前界面的XML(通过dump_hierarchy函数)
- 检查有没有
跳过
,知道了
这两个按钮,如果有就点击,然后回到第一步 - 检查有没有
播放
按钮, 有就点击,结束。没有找到在回到第一步,一直执行到查找次数超标。
pip3 install -U uiautomator2
目前该插件已经内置到uiautomator2中了,所以不需要plugin注册了。
简单用法
看下面的这个简单的例子了解下如何使用
import uiautomator2 as u2
def main():
d = u2.connect()
d.app_start("com.netease.cloudmusic", stop=True)
d.xpath('//*[@text="私人FM"]').click()
下面的代码为了方便就不写
import
和main
了,默认存在d
这个变量
XPathSelector
的操作
sl = d.xpath("@com.example:id/home_searchedit") # sl为XPathSelector对象
# 等到对应的元素出现,返回XMLElement
# 默认的等待时间是10s
el = sl.wait()
el = sl.wait(timeout=15) # 等待15s, 没有找到会返回None
# 等待元素消失
sl.wait_gone()
sl.wait_gone(timeout=15)
# 跟wait用法类似,区别是如果没找到直接抛出 XPathElementNotFoundError 异常
el = sl.get()
el = sl.get(timeout=15)
# 修改默认的等待时间为15s
d.xpath.global_set("timeout", 15)
d.xpath.implicitly_wait(15) # 与上一行代码等价
print(sl.exists) # 返回是否存在 (bool)
sl.get_last_match() # 获取上次匹配的XMLElement
sl.get_text() # 获取组件名
sl.set_text("") # 清空输入框
sl.set_text("hello world") # 输入框输入 hello world
# 遍历所有匹配的元素
for el in d.xpath('//android.widget.EditText').all():
print("rect:", el.rect) # output tuple: (x, y, width, height)
print("center:", el.center())
el.click() # click operation
print(el.elem) # 输出lxml解析出来的Node
print(el.text)
XMLElement
的操作
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() # get element center position
x, y = el.offset(0.5, 0.5) # same as center()
# send click
el.click()
# 打印文本内容
print(el.text)
# 获取组内的属性, dict类型
print(el.attrib)
# 控件截图 (原理为先整张截图,然后再crop)
el.screenshot()
# 控件滑动
el.swipe("right") # left, right, up, down
el.swipe("right", scale=0.9) # scale默认0.9, 意思是滑动距离为控件宽度的90%, 上滑则为高度的90%
比较完整的例子
import uiautomator2 as u2
def main():
d = u2.connect()
d.app_start("com.netease.cloudmusic", stop=True)
# steps
d.xpath("//*[@text='私人FM']/../android.widget.ImageView").click()
d.xpath("下一首").click()
# 监控弹窗2s钟,时间可能大于2s
d.xpath.sleep_watch(2)
d.xpath("转到上一层级").click()
d.xpath("转到上一层级").click(watch=False) # click without trigger watch
d.xpath("转到上一层级").click(timeout=5.0) # wait timeout 5s
d.xpath.watch_background() # 开启后台监控模式,默认每4s检查一次
d.xpath.watch_background(interval=2.0) # 每2s检查一次
d.xpath.watch_stop() # 停止监控
for el in d.xpath('//android.widget.EditText').all():
print("rect:", el.rect) # output tuple: (left_x, top_y, width, height)
print("bounds:", el.bounds) # output tuple: (left, top, right, bottom)
print("center:", el.center())
el.click() # click operation
print(el.elem) # 输出lxml解析出来的Node
# 滑动
el = d.xpath('@com.taobao.taobao:id/fl_banner_container').get()
el.swipe("right") # 从右滑到左
el.swipe("left")
el.swipe("up") # 从下滑到上
el.swipe("down")
el.swipe("right", scale=0.9) # scale 默认0.9, 滑动距离为控件宽度的80%,� 滑动的中心点与控件中心点一致
el.swipe("up", scale=0.5) # 滑动距离为控件高度的50%
为了写起脚本来更快,我们自定义了一些简化的xpath规则
规则1
//
开头代表原生xpath
规则2
@
开头代表resourceId定位
@smartisanos:id/right_container
相当于
//*[@resource-id="smartisanos:id/right_container"]
规则3
^
开头代表正则表达式
^.*道了
相当于 //*[re:match(text(), '^.*道了')]
规则4
灵感来自SQL like
知道%
匹配知道
开始的文本, 相当于 //*[starts-with(text(), '知道')]
%知道
匹配知道
结束的文本,相当于 //*[ends-with(text(), '知道')]
%知道%
匹配包含知道
的文本,相当于 //*[contains(text(), '知道')]
规则5
另外来自Selenium PageObjects
$知道
匹配 通过d.xpath.global_set("alias", dict)
dict字典中的内容, 如果不存在将使用知道
来匹配
规则 Last
会匹配text 和 description字段
如 搜索
相当于 XPath //*[@text="搜索" or @content-desc="搜索"]
- 有时className中包含有
$
字符,这个字符在XML中是不合法的,所以全部替换成了-
# 所有元素
//*
# 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")]
如有其他资料,欢迎提Issues补充
别名定义 从1.3.4
版本不再支持别名
这种写法有点类似selenium中的PageObjects
# 这里是Python3的写法,python2的string定义需要改成 u"菜单" 注意前的这个u
d.xpath.global_set("alias", {
"菜单": "@com.netease.cloudmusic:id/qh", # TODO(ssx): maybe we can support P("@com.netease.cloudmusic:id/qh", wait_timeout=2) someday
"设置": "//android.widget.TextView[@text='设置']",
})
# 这里需要 $ 开头
d.xpath("$菜单").click() # 等价于 d.xpath()
d.xpath("$设置").click()
d.xpath("$菜单").click()
# 等价于 d.xpath("@com.netease.cloudmusic:id/qh").click()
d.xpath("$小吃").click() # 在这里会直接跑出XPathError异常,因为并不存在 小吃 这个alias
# alias_strict 设置项
d.xpath.global_set("alias_strict", False) # 默认 True
d.xpath("$小吃").click() # 这里就会正常运行
# 等价于
d.xpath('//*[@text="小吃" or @content-desc="小吃"]').click()
目前默认logging.INFO
调整方法
import logging
d.xpath.logger.setLevel(logging.DEBUG)